getElementsByClassName 함수 구현하기 (+자바스크립트에서 유사배열과 배열, 변환 방법)

    프론트엔드 라이브 코딩이나 화이트보드 테스트에서 자주 나오는 과제 중 하나가 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
    반응형

    댓글