HTMLCollection 과 NodeList
DOM 컬렉션 객체인 HTMLCollection 과 NodeList 는 DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체다.
두개 모두 유사 배열 객체이면서 이터러블이다. 따라서 둘 다 length 프로퍼티를 가지므로 객체를 배열처럼 접근할 수 있고 반복문을 돌 수 있습니다. 그러나 유사 배열 객체이기 때문에 자바스크립트에서 제공하는 배열 객체의 메소드는 사용할 수 없습니다. (ex. map, forEach, reduce 등등)
여기서! HTMLCollection과 NodeList의 중요한 특징은 노드 객체의 상태 변화를 실시간으로 반영하는 살아있는 객체 라는 것이다.
HTMLCollection
먼저 HTMLCollection은 getElementsByTagName, getElementByClassName 메서드를 통하여 얻을 수 있는 객체라고 볼 수 있다. 아래의 예제를 보면,
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.red { color: red;}
.blue {color: blue;}
</style>
</head>
<html lang="en"></html>
<body>
<ul id="fruits">
<li class="red">Apple</li>
<li class="red">Banana</li>
<li class="red">Orange</li>
</ul>
<script>
// class 값이 'red'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환
const $elems = document.getElementsByClassName("red");
// 이 시점에 HTMLCollection 객체에는 3개의 요소 노드가 담겨 있다.
console.log($elems); // HTMLCollection(3) [li.red, li.red, li.red]
</script>
</body>
</html>
document.getElementByClassName 함수를 통해 class="red" 의 속성을 갖는 요소들을 가져왔다.
하지만 콘솔창을 자세히 보면 일반적인 배열이 아니라 HTMLCollection 이라는 프로토타입을 기반으로 구현된 것을 알 수 있다.
위에서 보다시피 HTMLCollection 은 배열이 아닌 유사 배열 객체이다. 즉, HTMLCollection 과 배열의 가장 큰 차이점은 "살아 있다"는 것이다.
그럼, 객체가.. 살아있다..? 무슨의미일까
살아있는 객체 라는 의미는 내부의 DOM 노드들이 정적으로 존재하지않고, 마치 살아 있는 것처럼 노드 객체의 상태 변화를 실시간으로 감지하고 반영하는 객체이기 때문.
위 예제에서 class="red"를 갖는 DOM 노드들이 담긴 HTMLCollection을 얻었고, li 요소들의 class를 "blue"로 바꿔보면??
예상대로라면 li 요소의 class값이 "blue"로 변경되어 모든 li요소는 파란색으로 렌더링될 것이다. 과연 그럴까...?
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.red { color: red;}
.blue {color: blue;}
</style>
</head>
<html lang="en"></html>
<body>
<ul id="fruits">
<li class="red">Apple</li>
<li class="red">Banana</li>
<li class="red">Orange</li>
</ul>
<script>
// class 값이 'red'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환
const $elems = document.getElementsByClassName("red");
// 이 시점에 HTMLCollection 객체에는 3개의 요소 노드가 담겨 있다.
console.log($elems); // HTMLCollection(3) [li.red, li.red, li.red]
// HTMLCollection 객체의 모든 요소의 class 값을 'blue'로 변경한다.
for(let i=0; i< $elems.length; i++){
$elems[i].className='blue';
}
// HTMLCollection 객체의 요소가 3개에서 1개로 변경.
console.log($elems); // HTMLCollection(1) [li.red]
</script>
</body>
</html>
콘솔을 찍어보면??
사람일은 항상 예상대로만 흘러가는것은 아니기에 두번째 li요소만 class 값이 변경되지 않았다.
그럼 이유가 무엇일까??
이유는 HTMLCollection 이 살아있는 객체이기 때문이다.
⭐️ 일반적인 배열과 다르게 HTMLCollection은 내부 원소(노드)에 변화가 생기면 변화를 바로 반영 ⭐️
자세한 이유를 살펴보면, $elems.length는 3이므로 for문의 코드블록이 3번 반복.
A. 첫 번째 반복(i===0)
$elems[0] li의 첫 번째 요소가 'blue'로 변경된다. 이때 첫 번째 li 요소가 변경되었으므로 getElementsByClassName 메서드의 인자로 전달한 'red'와는 더는 일치하지 않기 때문에 HTMLCollection 객체의 $elems에서 실시간으로 제거된다.
B. 두 번째 반복 (i==1)
첫 번째 반복에서 li 요소가 삭제되었으므로 뒤에 남아 있던 리스트들은 한 칸씩 앞으로 밀리게 된다. 따라서 $elems[0]은 두 번째 li 요소가 되고 $elems[1]은 세 번째 li요소가 된다. 즉 세 번째 li의 값이 바뀌고 또한 제거된다.
C. 세 번째 반복 (i==2)
첫 번째, 세번째 li 요소가 제거되고 $elems에는 두 번째 li 태그만 남아있게 되어 $elem.length가 1이 된다. 따라서 i < $elems.length 조건식에 맞지 않으므로 반복이 종료된다. 결국 $elems에 남아있는 두번째 li요소의 class 값이 변경되지 않는다.
🚨 이처럼 HTMLCollection 객체는 실시간으로 노드 객체의 상태 변경을 반영하여 요소를 제거할 수 있기 때문에 HTMLCollection 객체를 for문으로 순회하며 노드 객체의 상태를 변경할 때는 주의해야한다.
위의 문제는 for문을 역방향으로 순회하는 방법으로 회피 가능!
for(let i=0; i< $elems.length; i++){
$elems[i].className='blue';
}
위의 반복문 원본을 아래의 반복문처럼 역방향으로 바꿔준다면 문제를 해결할 수 있다.
for(let i= $elems.length - 1; i >= 0; i--){
$elems[i].className='blue';
}
하지만, 제일 간단한 해결책은 문제의 근원인 HTMLCollection 객체를 사용하지 않는 것이다.
또한, 아래처럼 HTMLCollection 객체를 배열로 변환하면 유용한 배열의 고차함수(forEach, map, filter, reduce 등)을 사용할 수 있다.
// 유사 배열 객체이면서 이터러블인 HTMLCollection을 배열로 변환하여 순회
[...$elems].forEach(elem => elem.className = "blue");
NodeList
다음은 NodeList 객체이다.
NodeList 객체는 querySelectorAll 메서드가 반환하는 객체로 HTMLCollection객체의 부작용을 해결하기 위해 사용된다.
즉, NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체이다.
const $elems = document.querySelectorAll(".red");
for(let i = 0; i < $elems.length; i++){
$elems[i].className = "blue";
}
HTMLCollection 객체, 즉 document.getElementsByClassName 을 사용했을 때는 첫번째와 세번째 요소만 blue 컬러로 변했지만,
NodeList 객체, 즉 querySelectorAll 을 사용하면 모든 요소가 의도한대로 blue 컬러로 변하는걸 볼 수 있다.
⭐️⭐️ 하지만, HTMLCollection 이나 NodeList 객체는 예상과 다르게 동작할 때가 있어 실수하기 쉽다.
따라서 노드 객체의 상태 변경과 관계없이 DOM 컬렉션을 사용하려면 배열로 변환하여 사용하는 것을 권장!!
위에서 말했었지만, 배열로 변환하여 사용하면 배열의 유용한 고차함수(forEach, map, filter, reduce 등)을 사용할 수 있기 때문에
유사 배열 객체이면서 이터러블인 HTMLCollection, NodeList 객체는 스프레드 문법이나, Array.from 메서드를 사용하여 배열로 변환하여 사용!!
HTMLCollection | NodeList |
DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체 | |
유사 배열 객체이면서 이터러블 | |
배열로 변환 후 사용을 매우~~~~~ 권장!! | |
getElementsByTagName, getElementsByClassName | querySelectorAll |
Live 객체 | 대부분 Non-live 객체 ⭐️ (ChildNodes 프로퍼티는 Live 객체) |
forEach 사용 불가 | forEach 사용 가능 |