-
[JS/DOM] 자바스크립트, 돔 조작 시 주의할 점 (Live Collection vs Static Collection)Frontend 2019. 6. 16. 02:04
<ul class="list"> <li>1번</li> <li>2번</li> <li>3번</li> </ul>
만약에 위 HTML코드에서 <li>태그를 자바스크립트를 이용하여 동적으로 제어하기 위해 돔을 가져온다고 해보자.
여러 가지 방법이 있겠지만 가장 흔하게 사용되는 방법은 아래와 같다.
const $li = document.querySelectorAll('.list li');
콘솔 창에 출력해보면 원하던대로 li 요소들을 잘 가져오는 것을 확인할 수 있다.
또한 querySelectorAll()을 이용하여 가져 온 li 요소들은 NodeList라는 이름의 객체로 반환된다.
const $li = document.querySelector('.list').children;
이번에는 다른 방법으로 li 요소에 접근했는 데, 부모 요소인 ul의 children 속성을 사용하여 탐색하였다.
이 경우에도 아까와 똑같이 li 요소를 출력하였으나, 콘솔 창을 잘 보면 NodeList가 아닌 HTMLCollection이 반환된 것을 확인할 수 있다.
NodeList
NodeList는 element.childNodes와 같은 속성이나 document.querySelectorAll()과 같은 메소드에 의해 반환되는 Node Collection이다. NodeList는 마치 Array와 비슷하게 생겼지만 배열과는 다르다. 우리는 배열과 같은 형태를 하고 있지만 배열이 아닌 객체들을 유사 배열이라고 부른다.
NodeList는 배열이 아니기 때문에 배열에 사용 가능한 메소드를 대부분 사용할 수 없다. 그렇기 때문에 NodeList에 배열 메소드를 사용하기 위해서는 NodeList를 실제 배열로 변환하는 등의 방법을 사용해야 한다.
(최신 브라우저에서는 NodeList에 forEach()와 같은 메소드를 지원하고 있다.)
중요한 점은 대개의 경우 NodeList는 라이브(Live) 콜렉션으로 DOM의 변경 사항을 실시간으로 반영한다. 그러나 document.querySelectorAll() 메소드에 의해 반환되는 NodeList는 정적(Static) 콜렉션으로 DOM의 변경 사항이 실시간으로 반영되지 않는다.
HTMLCollection
HTMLCollection 인터페이스는 요소의 문서 내 순서대로 정렬된 일반 컬렉션을 나타내며 NodeList와 마찬가지로 유사 배열이다. HTMLCollection은 현대적인 DOM의 이전 세대부터 존재하던 구성요소로 element.children과 같은 속성 또는
document.getElementsByClassName(), document.getElementsByTagName()과 같은 메소드에 의해 반환된다. HTMLCollection은 모두 문서가 바뀔 때 실시간으로 업데이트되는 라이브(Live) 콜렉션이다.
Live Collection vs Static Collection
노드 컬렉션이 정적인지 동적인지 생각하지 않고 돔을 조작하다보면 내가 원하는 대로 웹 페이지가 동작하지 않는 경우가 종종 생긴다. 특히 내가 변경한 사항때문에 노드 리스트의 길이가 실시간으로 변경될 때 버그가 많이 발생한다. 아래 예시를 살펴보자.
<ul class="list"> <li class="txt-blue">1번</li> <li class="txt-blue">2번</li> <li class="txt-blue">3번</li> </ul>
.txt-blue { color: blue; } .txt-red { color: red; }
원래의 li 태그에는 .txt-blue라는 클래스가 있었기 때문에 글자 색이 파란색이었다.
이제 모든 li 태그의 클래스명을 .txt-red로 변경하여 글자 색을 빨간색으로 변경하려고 한다.
const $li = document.getElementsByClassName('txt-blue'); for (let i = 0; i < $li.length; i++) { $li[i].className = 'txt-red'; }
위와 같이 코드를 작성하면 아무 문제 없이 모든 글자가 빨간색으로 바뀔 것 같지만
실행 결과는 생각과는 다르게 동작한다.
왜 이런 문제점이 발생하는 것일까?
바로 우리가 돔을 가져올 때 사용한 getElementByClassName() 메소드가 반환하는 HTMLCollection이
DOM의 변경사항을 실시간으로 반영하는 Live Collection이기 때문이다.
최초로 .txt-blue 클래스를 가진 요소들을 가져왔을 때, HTMLCollection의 length는 3이다.
그리고 우리는 i의 초기값을 0으로 설정하여 3보다 작을 때까지 루프를 돈다.
그 때, 첫 번째 요소의 클래스명을 .txt-red로 변경하는 순간,
더 이상 txt-blue 클래스를 가지고 있지 않기 때문에 HTMLCollection에서 즉시 삭제된다.
그러면 리스트의 length가 의도치 않게 2로 변경되고 요소들이 한 칸씩 앞으로 밀리게 되면서
위와 같은 버그가 발생하는 것이다.
해결방안
위 문제점을 해결하는 방법에는 여러가지가 있다.
const $li = document.getElementsByClassName('txt-blue'); for (let i = 0; i < $li.length; i++) { $li[i].className = 'txt-red'; i--; }
1. 루프를 돌릴 때, class명을 txt-red로 바꾼 후 i 변수를 1 감소시킨다.
한 칸씩 밀린 요소도 빠뜨리지 않고 처리할 수 있게 된다.
대신 이 경우에는 반복문이 종료한 후 $li를 출력하면 모든 요소가 제거된 빈 HTMLCollection이 출력된다.
const $li = document.getElementsByClassName('txt-blue'); for (let i = $li.length - 1; i >= 0; i--) { $li[i].className = 'txt-red'; }
2. 뒤에서부터 반복을 시작한다.
노드 컬렉션의 맨 마지막 요소부터 처리하기 때문에,
마지막 요소가 컬렉션에서 제거되더라도 다른 요소에 영향을 미치지 않는다.
이 경우에도 반복문이 종료한 후 $li는 모든 요소가 제거된 빈 HTMLCollection이 된다.
const $li = document.querySelectorAll('.txt-blue'); for (let i = 0; i < $li.length; i++) { $li[i].className = 'txt-red'; }
3. 정적인 컬렉션(Static Collection)으로 반복을 실행한다.
document.querySelectorAll() 메소드로 반환되는 NodeList는 정적 컬렉션으로
변경 사항이 실시간으로 반영되지 않기 때문에 우리가 원하는 대로 동작한다.
이 경우, 반복문이 종료된 후에도 $li에는 원래의 요소가 그대로 존재하며
단지 클래스명만 바뀐 상태로 저장되어있다.
반응형'Frontend' 카테고리의 다른 글
[JS/DOM] 자바스크립트, 문서 객체 모델(Document Object Model) 조작하기(3) - DOM Event (609) 2019.06.18 [JS/DOM] 자바스크립트, 문서 객체 모델(Document Object Model) 조작하기(2) - DOM Manipulation (734) 2019.06.16 [JS/클로져] 자바스크립트, 클로저를 이용한 버튼 클릭 시 나타나는 툴팁 (734) 2019.06.12 [JS/DOM] 자바스크립트, 문서 객체 모델(Document Object Model) 탐색하기(1) - DOM Traversing (967) 2019.06.10 [JS/클로져] 자바스크립트의 Lexical scoping과 Closure (2) (1808) 2019.06.10 COMMENT