일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 리액트
- TypeScript
- kubernetes
- node.js
- 자바스크립트
- es6
- 이더리움
- 이슈
- 클라우드
- 파이썬
- 백엔드
- 블록체인
- 컴퓨터공학
- HTML
- BFS
- AWS
- 프론트엔드
- docker
- 웹
- 쿠버네티스
- k8s
- 백준
- next.js
- 가상화
- CSS
- 솔리디티
- JavaScript
- 알고리즘
- react
- 타입스크립트
- Today
- Total
즐겁게, 코드
HTMLCollection과 NodeList, 너흰 누구니? 본문
바닐라 자바스크립트를 활용해 DOM에 접근할 때는 HTMLCollection
, NodeList
등의 DOM 요소들의 컬렉션을 다루게 될 때가 있는데요, 이번 글을 통해 두 컬렉션의 성질을 간단히 알아보도록 하겠습니다.
HTMLCollection
먼저 HTMLCollection
은 getElementsByClassName
과 getElementsByTagName
메서드를 통해 얻을 수 있는 객체인데요, 한번 간단한 예제를 작성해 보겠습니다.
<!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
프로퍼티를 통해 얻은 NodeList
는 HTMLCollection
과 동일하게 살아 있는 객체처럼 동작해 이런 경우에는 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
,getElementsByTagName
은HTMLCollection
이라는 DOM 컬렉션을 반환합니다.querySelectorAll
은NodeList
라는 DOM 컬렉션을 반환합니다.HTMLCollection
은 노드의 상태 변화를 실시간으로 감지하고,NodeList
는 노드를 정적으로 관리합니다.- 다만
childNodes
가 리턴한NodeList
는 정적임이 보장되지 않습니다. - 따라서,
HTMLCollection
또는NodeList
를 그대로 사용할 때는 부작용 예방 차원 및 고차 함수 지원을 위해Array
로 치환해 사용할 것을 권장합니다.
'💬 언어 > Javascript' 카테고리의 다른 글
&& 연산자에 대한 간단한 고찰 (1) | 2021.12.10 |
---|---|
Promise.all 로 비동기 로직 개선하기 (0) | 2021.11.04 |
자바스크립트 메모리 누수 방지 기법 (0) | 2021.09.12 |
얼어붙어라! - 객체 불변화하기 (0) | 2021.08.26 |
keys, values, entries 메서드 사용하기 (0) | 2021.08.15 |