프론트엔드 라이브 코딩이나 화이트보드 테스트에서 자주 나오는 과제 중 하나가 DOM API를 직접 구현해보는 문제입니다. 그중 대표적인 것이 document.getElementsByClassName을 직접 만들어보라는 과제입니다. 이번 글에서는 이 문제를 어떻게 접근하면 좋을지 베이스케이스 → 엣지케이스 → 효율성 → 가독성 순서로 풀어봅니다.
문제 이해
목표는 주어진 className을 가진 모든 요소를 찾아 배열로 반환하는 함수입니다.
기본적으로는 DOM 트리를 순회하면서 조건에 맞는 요소를 모으면 됩니다.
function getElementsByClassName(className) {
const bodyChildrenEls = document.body.children;
const result = [];
const stack = [...bodyChildrenEls];
while (stack.length) {
const currentElement = stack.pop();
const currentElementChildren = currentElement.children;
for (let i = 0; i < currentElementChildren.length; i++) {
const currentChildrenEl = currentElementChildren[i];
stack.push(currentChildrenEl);
}
if (currentElement.classList.contains(className)) {
result.push(currentElement);
}
}
return result;
}
1. 베이스케이스
- 최상위 요소에만 클래스가 붙은 경우
- DOM이 비어있을 때 (<body></body>)
- 찾는 클래스가 없는 경우
function getElementsByClassName(className) {
const bodyChildrenEls = document.body.children;
const result = [];
for (let i = 0; i < bodyChildrenEls.length; i++) {
const currentEl = bodyChildrenEls[i];
const currentElClassName = currentEl.className;
if (currentElClassName === className) {
result.push(currentEl);
}
}
return result;
}
2. 앳지 케이스
- 클래스가 여러개 붙은 경우
- 중첩된 요소일 경우
- 중첩 댑스가 깊어질 경우 ... 등 그 외에 실시간으로 추가되는 조건들 .. ?
2-1. 중첩된 자식까지 순회 (DFS / Stack)
모든 하위 노드까지 탐색할 수 있도록 stack을 도입합니다.
function getElementsByClassName(className) {
const bodyChildrenEls = document.body.children;
const result = [];
const stack = [...bodyChildrenEls];
while (stack.length) {
const currentElement = stack.pop();
const children = currentElement.children;
// 자식 요소 스택에 추가
for (let i = 0; i < children.length; i++) {
stack.push(children[i]);
}
if (currentElement.className === className) {
result.push(currentElement);
}
}
return result;
}
✅ 동작: 중첩 구조까지 탐색 가능
❌ 문제: className이 "a b"처럼 여러 개일 경우 정확히 일치하지 않음
2-2 다중클래스 대응 ("a b")
function getElementsByClassName(className) {
const bodyChildrenEls = document.body.children;
const result = [];
const stack = [...bodyChildrenEls];
while (stack.length) {
const currentElement = stack.pop();
const children = currentElement.children;
for (let i = 0; i < children.length; i++) {
stack.push(children[i]);
}
// classList.contains로 특정 클래스만 검사
if (currentElement.classList.contains(className)) {
result.push(currentElement);
}
}
return result;
}
✅ 동작: <div class="a b"> 에서 getElementsByClassName("a") 호출 시 정상적으로 매칭됨
3. 효율성 (시간복잡도 & 공간복잡도)
- 시간복잡도: 모든 노드를 한 번씩 방문하므로 O(N) (N은 DOM 노드 수)
- 공간복잡도: 스택과 결과 배열에 최대 O(N)까지 저장 가능
4. 가독성
면접 후반에는 코드의 깔끔함도 평가 요소가 될 수 있음
- stack → pendingNodes
- result → matchedElements
- currentElementChildren → children
+ 유사배열 정리
자바스크립트를 쓰다 보면 배열처럼 보이는데 배열은 아닌 값들을 종종 만납니다. 흔히 유사배열(array-like) 이라고 부르는 것들입니다.
유사배열이란?
유사배열은 length 속성과 인덱스 번호를 가진 객체입니다. 겉모습은 배열 같지만 실제 배열 메서드(map, forEach, filter 등)는 사용할 수 없습니다.
대표적인 예시:
- 함수 안의 arguments
- document.querySelectorAll 같은 DOM API 결과값
- HTMLCollection
function example() {
console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3 }
}
example(1, 2, 3);
const divs = document.querySelectorAll('div');
console.log(divs); // NodeList(3) [div, div, div]
배열과의 차이
배열은 Array.prototype을 상속받아 다양한 메서드를 쓸 수 있습니다. 반면 유사배열은 단순 객체라 메서드를 직접 사용하면 에러가 납니다.
const arr = [1, 2, 3];
arr.forEach(x => console.log(x)); // 정상 동작
const divs = document.querySelectorAll('div');
divs.forEach(x => console.log(x));
// TypeError: divs.forEach is not a function
유사배열 → 배열 변환 방법
1. Array.from()
ES6에서 도입된 가장 간단하고 직관적인 방법입니다.
const divs = document.querySelectorAll('div');
const divArray = Array.from(divs);
divArray.forEach(div => console.log(div));
2. 스프레드 연산자 [... ]
더 간결하게 배열로 만들 수 있습니다.
const argsToArray = (...args) => {
const arr = [...arguments];
console.log(arr);
};
argsToArray(1, 2, 3); // [1, 2, 3]
3. Array.prototype.slice.call()
ES6 이전에 많이 쓰이던 방식입니다.
function oldSchool() {
const arr = Array.prototype.slice.call(arguments);
console.log(arr);
}
oldSchool('a', 'b', 'c'); // ['a', 'b', 'c']
그 외 실제 활용 예시
DOM 조작
querySelectorAll로 선택한 결과를 배열로 변환해 map을 쓸 수 있습니다.
const buttons = document.querySelectorAll('button');
// 유사배열 그대로는 map 불가
// buttons.map(...) -> 에러 발생
// 변환 후 사용
const texts = [...buttons].map(btn => btn.innerText);
console.log(texts);
함수 인자 처리
arguments 객체를 배열로 바꾸면 편리하게 조작할 수 있습니다.
function sum() {
const nums = Array.from(arguments);
return nums.reduce((acc, cur) => acc + cur, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
타입 판별하기
유사배열인지 배열인지 헷갈릴 때는 Array.isArray()를 쓰면 됩니다.
Array.isArray([1, 2, 3]); // true
Array.isArray(document.querySelectorAll('div')); // false
'Frontend > 🔨 JS' 카테고리의 다른 글
| Three.js 로 움직이는 지구본 만들기 (0) | 2022.10.22 |
|---|---|
| [js library] AOS 스크롤 애니메이션 구현하기 (0) | 2022.08.10 |
| [Javascript plugin] Masonry Layout 적용하기 (0) | 2022.07.07 |
댓글