Notice
Recent Posts
Recent Comments
관리 메뉴

즐겁게, 코드

HTMLCollection과 NodeList, 너흰 누구니? 본문

💬 언어/Javascript

HTMLCollection과 NodeList, 너흰 누구니?

Chamming2 2021. 10. 24. 15:15

바닐라 자바스크립트를 활용해 DOM에 접근할 때는 HTMLCollection, NodeList 등의 DOM 요소들의 컬렉션을 다루게 될 때가 있는데요, 이번 글을 통해 두 컬렉션의 성질을 간단히 알아보도록 하겠습니다.

HTMLCollection

먼저 HTMLCollectiongetElementsByClassNamegetElementsByTagName 메서드를 통해 얻을 수 있는 객체인데요, 한번 간단한 예제를 작성해 보겠습니다.

<!DOCTYPE html>
  <head>
    <style>
      .red {
        color: red;
      }
      .blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <ul id="fruits">
      <li class="red">Apple</li>
      <li class="red">Banana</li>
      <li class="red">Orange</li>
    </ul>
    <script>
      const $fruits = document.getElementsByClassName("red");
      console.log($fruits);
    </script>
  </body>
</html>

document.getElementsByClassName 함수를 통해 class="red" 속성을 갖는 요소들을 획득한 모습인데요, 자세히 보면 이 컬렉션은 일반적인 배열이 아닌 HTMLCollection 이라는 프로토타입을 기반으로 구현되었다는 것을 알 수 있습니다.

실제로 HTMLCollection 는 배열이 아닌 유사 배열 객체 인데요, HTMLCollection 과 배열과의 가장 큰 차이는 바로 "살아 있다" 는 점입니다.

살아 있는 객체?

🚨 주의! 이제부터 다룰 내용은 매우 신기합니다!

살아 있는 객체라는 표현을 사용한 까닭은 내부의 DOM 노드들이 정적으로 존재하는 것이 아닌, 마치 살아 있는 것처럼 노드 객체의 상태 변화를 실시간으로 감지하고 반영하는 객체이기 때문입니다.

 

위의 예제에서는 class="red" 를 갖는 DOM 노드들이 담긴 컬렉션을 얻을 수 있었는데요, 한번 li 요소들의 클래스명을 "red" 에서 "blue" 로 바꿔 보도록 하겠습니다.

<!DOCTYPE html>
  <head>
    <style>
      .red {
        color: red;
      }
      .blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <ul id="fruits">
      <li class="red">Apple</li>
      <li class="red">Banana</li>
      <li class="red">Orange</li>
    </ul>
    <script>
      const $fruits = document.getElementsByClassName("red");
      for (let i = 0; i < $fruits.length; i++) {
        // li 요소들의 클래스명을 "blue" 로 변경합니다.
        $fruits[i].className = "blue";
      }
    </script>
  </body>
</html>

자, 어떻게 되었을까요?

신기하게도 두 번째 요소를 제외한 첫 번째와 세 번째 요소의 색만 파랗게 변했고, 개발자 도구를 봐도 두 번째 요소는 클래스명이 변하지 않은 모습입니다.

이유는 바로 HTMLCollection 이 살아 있는 객체이기 때문에 이런 일이 일어난 것인데요, 일반적인 배열과는 다르게 HTMLCollection 은 내부 원소(노드)에 변화가 생기면 이를 즉시 반영합니다.

위에서 일어난 동작을 천천히 살펴 보겠습니다.

// A. class="red" 인 요소의 HTMLCollection 을 획득합니다.
// $fruits = [li.red, li.red, li.red]
const $fruits = document.getElementsByClassName("red");

// B. 1번째 루프 : HTMLCollection의 0번째 노드의 클래스명을 "blue" 로 변경합니다.
// 따라서 살아 있는 객체인 $fruits의 1번째 요소는 HTMLCollection에서 제거됩니다.
// $fruits = [li.red, li.red], i = 1

// C. 2번째 루프 : i는 1이므로 [li.red, li.red]의 두 번째 요소의 클래스명 "blue"로 변경합니다.
// $fruits = [li.red], i = 1
// 따라서 $fruits에는 [li.red]가 하나 남은 채로 루프가 종료됩니다.
for (let i = 0; i < $fruits.length; i++) {
  $fruits[i].className = "blue";
}

이처럼 HTMLCollection의 내부 요소들을 실시간으로 감지하는 특성으로 인해 HTMLCollection에 반복문을 적용할 때는 주의해야 하는데요, 이 문제는 반복문을 역순으로 순회하거나 HTMLCollection을 배열로 치환해 해결할 수 있습니다.

// 1. 반복문을 역순으로 순회하기
const $fruits = document.getElementsByClassName("red");

// 이제 모든 요소들의 클래스명이 "blue" 가 됩니다.
for (let i = $fruits.length - 1; i >= 0; i--) {
  $fruits[i].className = "blue";
}

// 2. HTMLCollection을 배열로 치환하기
const $fruits = document.getElementsByClassName("red");
const arr = [...$fruits]

// 또는 forEach 등의 프로토타입 메서드를 활용할 수 있습니다.
for (let i = 0; i < arr.length; i++) {
  arr[i].className = "blue";
}

NodeList

다음은 NodeList 객체입니다.

NodeList 객체는 querySelectorAll 메서드가 반환하는 객체로 HTMLCollection이 갖고 있던 부작용을 해결하기 위해 등장했는데요, 즉 노드의 변경을 실시간으로 감지하지 않는다는 특징을 가진 컬렉션입니다.

const $fruits = document.querySelectorAll(".red");
for (let i = 0; i < $fruits.length; i++) {
  $fruits[i].className = "blue";
}

document.getElementsByClassName을 사용했을 때는 첫번째와 세번째 요소만 빨갛게 변했지만, document.querySelectorAll을 사용하니 모든 요소가 의도한 대로 파랗게 변한 모습입니다.

🤨 그럼 getElementsByClassName 대신 querySelectorAll 을 사용하는 것이 좋은 패턴인 걸까요?

꼭 그렇지만은 않습니다.

왜냐 하면 NodeList 역시 항상 정적인 객체임을 보장하지는 않는데요, childNodes 프로퍼티를 통해 얻은 NodeListHTMLCollection과 동일하게 살아 있는 객체처럼 동작해 이런 경우에는 NodeList 역시 HTMLCollection 이 갖고 있던 부작용을 그대로 갖게 됩니다.

 

또한, NodeList는 엄밀히 배열이 아니기 때문에 map, reduce 등 유용한 고차 함수들을 활용할 수 없다는 점도 단점입니다.

(*forEach의 경우, 배열의 forEach와 똑같이 동작하지만 배열의 것이 아닌 NodeList 객체가 독자적으로 가진 메서드입니다!)

해결책

가장 좋은 해결책은 getElementsByClassName 이나 querySelectorAll 을 사용해 얻은 DOM 컬렉션을 배열로 치환하는 방법입니다.

 

Array.from 또는 분해 할당을 사용해 컬렉션을 배열로 치환하면 언제나 내부 요소가 정적임이 보장됨과 동시에, forEach 등 배열의 프로토타입이 제공하는 여러 고차 함수를 활용할 수 있기 때문입니다.

const $fruits = document.querySelectorAll(".red");
const fruits = Array.from($fruits)
fruits.forEach(fruitDom => fruitDom.className = "blue")
// 또는 [$fruits].forEach(fruitDom => fruitDom.className = "blue")

TL;DR;

  • getElementsByClassName, getElementsByTagNameHTMLCollection 이라는 DOM 컬렉션을 반환합니다.
  • querySelectorAllNodeList 라는 DOM 컬렉션을 반환합니다.
  • HTMLCollection은 노드의 상태 변화를 실시간으로 감지하고, NodeList는 노드를 정적으로 관리합니다.
  • 다만 childNodes 가 리턴한 NodeList는 정적임이 보장되지 않습니다.
  • 따라서, HTMLCollection 또는 NodeList를 그대로 사용할 때는 부작용 예방 차원 및 고차 함수 지원을 위해 Array 로 치환해 사용할 것을 권장합니다.
반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆