HTMLCollection & NodeList를 순회하면서 DOM요소를 삭제할 때 주의할 점🚨

허상범·2023년 1월 6일
0

문제해결로그

목록 보기
2/2
post-thumbnail

문제 상황
의도한 동작 & 코드 & 결과
원인 찾기
원인
해결 방법
추가 내용
정리
회고


문제 상황

💀 for-of 반복문으로 DOM요소를 모두 제거할 때 절반의 요소만 제거된다.

이전에는 모든 요소를 삭제하는 경우에 querySelectorAll 로 골라서 삭제하거나 아예 새로운 List를 만들어서 교체하는 방식으로 구현을 했었다. 이번에는 DOM을 학습하면서 한 번도 안써본 Element.Children, Node.removeChild(), Element.remove() 를 사용하는 과정에서 문제가 발생했다.


의도한 동작 & 코드 & 결과

의도한 동작

  • 한 일 모두 지우기 버튼 클릭시 checked된 아이템을 전부 화면에서 제거하기

코드

const list = document.querySelector('.list');
const btn = document.querySelector('button');
const items = list.children; // HTMLCollection

btn.addEventListener('click', removeCheckedItems);

function removeCheckedItems() {
  for (const item of items) {
    item.firstChild.checked && list.removeChild(item);
  }
}
<body>
  <ul id="world" class="list">
    <li class="item item0"><input type="checkbox" checked />할 일 0</li>
    <li class="item item1"><input type="checkbox" checked />할 일 1</li>
    <li class="item item2"><input type="checkbox" checked />할 일 2</li>
    <li class="item item3"><input type="checkbox" checked />할 일 3</li>
    <li class="item item4"><input type="checkbox" checked />할 일 4</li>
    <li class="item item5"><input type="checkbox" checked />할 일 5</li>
  </ul>
  <button type="button">한 일 모두 지우기</button>
</body>
  • itemsfor...of로 순회하면서 해당 input이 checked이면 list에서 해당 item을 제거도록 작성했다.
  • list.children은 콘솔에 찍어봤을 때 배열의 형태를 보여서 배열을 반환한다고 짐작했다. querySelectorAll()도 배열을 반환한다고 생각(forEach 메소드를 갖고 있어서)하고 있었어서 둘 다 배열을 반환한다고 생각했다. 그리고 결정적으로 for...of를 사용할 수 있어서 더 배열이라고 믿었다...
    • 스포: 둘 다 배열이 아니고 유사 배열이다!!! 각각 HTMLCollection, Static NodeList을 리턴한다.

의도와 다르게 동작하는 결과

  • 화면에서 전부 다 제거되는 것을 생각했는데 리스트의 중간 사이사이 요소만 삭제됐다.

원인 찾기

1. for-of문은 3번 반복됐다.

  • 6번 돌 것이라고 생각했었는데, 반복문 내부에 콘솔을 찍어보니 실제로 반복문은 3번만 돈 것을 확인할 수 있었다.

2. HTMLCollection는 배열이 아니다.

  • Element.children가 반환한 HTMLCollection가 배열이 아닐 수 있겠다는 생각이 들었다.
  • 바로 forEach를 써보니 에러가 났다.
  • 그리고 itemsisArray()로 확인해보니 false가 나왔다.
function removeCheckedItems() {
  items.forEach(item => {
    item.firstChild.checked && list.removeChild(item);
  });
}
```js Array.isArray(items) // false ```

3. HTMLCollection 은?

HTMLCollection 인터페이스는 요소의 문서 내 순서대로 정렬된 일반 컬렉션(arguments처럼 배열과 유사한 객체)을 나타내며 리스트에서 선택할 때 필요한 메서드와 속성을 제공합니다.
HTML DOM 내의 HTMLCollection은 문서가 바뀔 때 실시간으로 업데이트됩니다.

https://developer.mozilla.org/ko/docs/Web/API/HTMLCollection

  • MDN을 확인해보니 HTMLCollection 은 배열이 아니고 순서대로 정렬된 배열과 유사한 객체이다.
  • 속성은 length, 메서드로는 인덱스의 값을 반환하는 item()이 있는데 for...of 를 쓸 수 있게 억지로 만든 느낌?? 이다.
  • DOM 컬렉션 객체다. 유사배열객체이자 이터러블이다.

🚨 An HTMLCollection in the HTML DOM is live

  • HTMLCollection은 실시간으로 업데이트된다. DOM에 변경이 생기면 자동으로 업데이트가 되서 MDN에서는 adding, moving, or removing nodes를 할 때 복사본을 만들어서 하는 걸 추천한다.
  • 즉 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체다.

원인

HTMLCollectionLive한 속성 때문에 발생했다.

  • 6번 도는 것으로 예상했던 반복문은 실제로 3번 돌고(index 0, 1, 2) 난 이후 index 3의 값이 없기 때문에 종료됐다.
  • 중간에 요소가 삭제되면서 실시간으로 인덱스가 하나씩 앞으로 이동했기 때문에 하나씩 건너뛰어 삭제된 것이다. (위 그림에 X 표시)

해결 방법

1. Array로 변환 후 동작시키기

const list = document.querySelector('.list');
const btn = document.querySelector('button');
const items = list.children; // HTMLCollection

btn.addEventListener('click', removeCheckedItems);

// forEach
function removeCheckedItems1() {
  const arrayItems = Array.from(items);
  arrayItems.forEach(item => {
    item.firstChild.checked && list.removeChild(item);
  });
}

// for...of
function removeCheckedItems2() {
  const arrayItems = Array.from(items);
  for (const item of arrayItems) {
    item.firstChild.checked && list.removeChild(item);
  }
}
  • Array.from 으로 배열로 변환 후에 forEach, for...of 를 사용했다.
  • 재대로 작동하는 것을 확인할 수 있다.
    • 반복문에 콘솔을 찍어보면 돔에서 삭제되는 동안 배열은 변경되지 않는다.

2. Element.children 대신 querySelectorAll() 를 사용한다.

const list = document.querySelector('.list');
const btn = document.querySelector('button');
const items = list.querySelectorAll('.item'); // NodeList

btn.addEventListener('click', removeCheckedItems);

function removeCheckedItems() {
  items.forEach(item => {
    if (item.firstChild.checked) list.removeChild(item);
  });
}
  • querySelectorAll()Static NodeList 를 반환하는데 이것도 HTMLCollection와 같은 유사배열객체이지만 Live 속성이 없다.
  • NodeList는 내부 요소들을 순회할 수 있는 forEach 메소드를 가진다.

추가 내용

querySelectorAll()Static NodeList 를 반환한다.

https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll
https://developer.mozilla.org/en-US/docs/Web/API/NodeList

  • NodeListHTMLCollection과 같이 DOM 컬렉션 객체다. 유사배열 객체이면서 이터러블이다.
  • NodeList는 Live 와 Static 두 가지가 있다. Live는 위에서 본 HTMLCollection처럼 실시간 업데이트가 된다. Node.childNodesLive NodeList를 반환한다.
  • querySelectorAll()Static NodeList를 반환하는데 이름 그대로 정적이라서 어떠한 변경도 원본 리스트에 영향을 주지 못한다. 그래서 이번에 발생한 문제가 생기지 않는다.
  • HTMLCollection 보다 다양한 메소드를 갖는다. item(), entries(), forEach(), keys(), values()

정리

  • HTMLCollection NodeList는 DOM 컬렉션 객체이며 유사배열객체이고 이터러블이다.
  • HTMLCollection
    • 노드의 상태 변화를 실시간으로 업데이트하는 Live(살아있는) 객체이다.
  • NodeList
    • querySelectorAll()로 만들어졌을 경우에 Static
    • childNodes 프로퍼티로 반환한 경우 Live
  • DOM 컬렉션 객체를 안전&간편하게 이용하려면 배열로 변환해 사용하면 된다.
    • NodeListforEach 메소드가 있지만, 배열로 변환하면 다양한 고차함수까지도 안전하게 사용할 수 있다.

회고

제대로 아는 게 중요하는 걸 깨닫는다.
아직 경험이 적지만 현재 내 수준에서 겪는 문제들은 거의 정확하게 알지 못해서 발생하는 것 같다. 이번에도 단순히 콘솔에서 [] 으로 감싸져 있다고 배열이라고 짐작했다. mdn에 검색해보기만 하면 나오는 것인데... 하지만 이제 제대로 안다는 것이 기쁘고 문제를 해결한 것이 뿌듯하다.



Reference

https://developer.mozilla.org/en-US/docs/Web/API/NodeList
https://developer.mozilla.org/ko/docs/Web/API/HTMLCollection
https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll

0개의 댓글