자바스크립트 딥다이브 - DOM [노드 조회]

ChoiYongHyeun·2023년 12월 25일
0

가보자 가보자


HTMLCollection 과 NodeList

DOM 의 컬렉션 객체인 두 가지는 모두 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체 이다.

이전 정리에서 getElementsByClassName 을 이용 할 때에는 HTMLCollection , querySelector 를 이용 할 때에는 NodeList 에 담아져서 나왔다.

하지만 두 개 모두 유사 배열 객체이면서 이터러블한 객체라는 공통점은 같았다.

그럼 이 두가지의 차이는 뭘까 ?

Return Type Selector Properties
HTMLElement document.getElementById()
document.querySelector()
HTMLCollection document.getElementsByClassName()
document.getElementsByTagName()
ParentNode.children
Element.getElementsByTagName()
Element.getElementsByClassName()
NodeList document.getElementsByName()
document.querySelectorAll()
Element.querySelectorAll()
> `HTMLElement` 는 유사배열 객체가 아닌 하나의 객체만 담고 있는 것이다.

갸아악 이걸 다 외워야 하나요 ?

놉 차이만 알고 가자

HTMLCollection

해당 객체는 상태 변화를 실시간으로 반영하는 살아있는 DOM 컬렉션 객체이다.

따라서 HTMLCollection 객체를 live 객체라고 하기도 한다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .red {
        color: red;
      }
      .blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <ul id="fruits">
      <li class="red">orange</li>
      <li class="red">banana</li>
      <li class="red">apple</li>
    </ul>
    <script>
      const $liFromDocument = document.getElementsByClassName('red');
      console.log($liFromDocument);

      for (let i = 0; i < $liFromDocument.length; i += 1) {
        $liFromDocument[i].className = 'blue';
      }
    </script>
  </body>
</html>

해당 스크립트 테그를 보면 li 태그내의 class 어트리뷰트 값에 따라 폰트의 색상이 변경된다.

이 때 class 명이 redli 태그들을 $liFromDocument 에 담아주고 반복문을 순회하며 클래스 명을 className 이란 프로퍼티에 접근하여 변경해주었다.

그러면 예상대로라면 3개의 li 모두 class 명이 변경되면서 텍스트 명이 변경 되었을 거라고 생각이 든다.


그런데 결과물을 보면 그렇지 않다.

이유를 확인하기 위해서 반복문에서 반복문에 주가 되는 $liFromDocument 객체를 로그해보자

    <script>
      const $liFromDocument = document.getElementsByClassName('red');

      for (var i = 0; i < $liFromDocument.length; i += 1) { // i 상태를 추적하기 위해 var 사용
        console.log(`${i} 번째 반복문 시행`);
        console.log($liFromDocument);
        console.log(`다음 i : ${i + 1}`);
        $liFromDocument[i].className = 'blue';
      }
      console.log(`반복문이 종료된 후의 i : ${i}`);
      console.log($liFromDocument);
    </script>

나는 HTMLCollection 에 해당하는 $liFromDocument 객체를 재할당하지 않았음에도 불구하고 안에 들어있는 객체들의 class 명이 변경됨에 따라 객체도 동적으로 변경된 것이다.

그래서 두 번째 반복문에서는 banana , apple 만 남았는데 해당 객체에서 1번째 인덱스를 변경하고 나니 banana 만 변경이 안된 것이다.

정리

HTMLCollection 은 가져온 노드들의 특성이 변경됨에 따라 자동적으로 객체의 내용을 변경한다.

그럼 어떻게 해결할까 ?

반복문을 역순으로 하여 하나씩 내려오게 하거나, while 문을 이용해 객체가 모두 빌 때 까지 쓰는 방법이 있다.

그런데 그렇게 하면 또 ES6 정신에 안맞아 .,.

for 문 사용 지양하고 고차 함수인 배열 함수를 사용하여 코드의 가독성을 높이자고 했다.

<!--변경 전 코드-->
	<script>
      for (let i = 0; i < $liFromDocument.length; i += 1) {
        $liFromDocument[i].className = 'blue';
      }
	</script>
<!--변경 후 코드-->
    <script>
      const $liFromDocument = document.getElementsByClassName('red');

      [...$liFromDocument].forEach((elem) => (elem.className = 'blue'));
    </script>

스프레드 문법으로 배열 형태로 변경 한 후에 forEach 를 이용해주었더니 코드도 훨씬 깔끔하고

HTMLCollection 의 성질에 의해서 동적으로 변경될까 두려워 하지 않아도 된다. ㅋㅋ 굿 ~

NodeList

기존 HTMLCollection 의 문제 (live 객체)를 해결하기 위해 다른 메소드들에서는 NodeList 를 반환하는 경우가 있다.

이 때 이런 객체들은 non-live 객체 이기 때문에 상태 변경을 반영하지 않는다.

하지만 종종 NodeList 를 반환하는 객체들 중에서 상태 변경을 반영하는 live 객체 형태의 NodeList 를 반환하는 경우도 있다.

그러니 웬만하면 받은 자료 구조를 배열 형태로 변경하여 맘 편하게 배열의 고차함수도 쓰고, 통일성을 유지하자


노드 탐색

어떤 조건에 따라 하위 노드들을 자료구조에 담아왔다면

그 자료구조에서 이동하며 형제 노드, 부모 노드, 자식 노드 등을 탐색 할 필요가 있다.

계속하여 배열 형태로 가져와 탐색하는 것은 트리 구조의 장점을 살리지 못한다.

트리 구조의 형태가 주는 장점을 살리기 위해 Node , Element 인터페이스는 트리 탐색 프로퍼티를 제공한다.

프로퍼티명에 Element 가 들어간 프로퍼티는 Element.prototype 이 제공하고 나머지는 Node.prototype 이 제공한다.

노드 탐색 프로퍼티는 모두 접근자 프로퍼티 이면서 getter 만 있는, 오로지 접근만 가능한 읽기 전용 접근자 프로퍼티이다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        padding: 10px;
      }
      .Parent div {
        border: 1px solid black;
      }
      .Parent div {
        border: 1px solid hotpink;
      }
    </style>
  </head>
  <body>
    <div class="Parent">
      <div class="Child1">1번자식</div>
      <div class="Child2">
        2번자식 (parentNode)
        <div>1번 손자 (previousElementSibling)</div>
        <div id="pick">2번 손자 (선택된 노드)</div>
        <div>3번 손자 (nextElementSibling)</div>
      </div>
      <div class="Child3">3번자식</div>
    </div>
    <script>
      const $picknode = document.getElementById('pick');

      $picknode.style.background = 'orange';
      $picknode.parentNode.style.background = 'yellow';
      $picknode.previousElementSibling.style.background = 'green';
      $picknode.nextElementSibling.style.background = 'hotpink';
    </script>
  </body>
</html>

다음과 같은 중첩 관계에서 ChildNode 를 제외하고 node 들을 프로퍼티를 통해 접근하는 모습을 볼 수 있다.

의아하다. 노드를 탐색 할 때 Element 가 프로퍼티 명에 붙고 안붙고는 왜 필요할까 ?

공백 텍스트 노드

언급하지 않았지만 HTML 요소 사이에 존재하는 스페이스, 탭, 줄바꿈, 공백 문자는 값이 공백인 텍스트 노드를 생성한다.

공백 텍스트 노드를 없애고 싶다면 줄바꿈이나 개행, 스페이스를 하지 않으면 되지만 그럼 가독성이 너무 안좋아진다.

그렇다면 위처럼 요소 노드들에 접근하고 싶은데 텍스트 노드에 접근하고 싶지 않다면 어떻게 해야 할까 ?

아니면 텍스트 노드에 요소하고 싶다면 ?

이를 위해 요소 탐색 할 수 있는 메소드가 Node , Element 2가지에서 제공한다.

자식 노드 탐색

프로퍼티 설명
Node.childNodes 해당 요소의 모든 자식 노드를 포함하는 실시간 NodeList를 반환합니다. (텍스트 노드 포함)
Node.firstChild 해당 요소의 첫 번째 자식 노드를 반환하며, 자식 노드가 없으면 null을 반환합니다. (텍스트 노드 포함)
Node.lastChild 해당 요소의 마지막 자식 노드를 반환하며, 자식 노드가 없으면 null을 반환합니다. (텍스트 노드 포함)
Element.prototype.children 해당 요소의 모든 자식 요소를 포함하는 실시간 HTMLCollection을 반환합니다. (텍스트 노드 미포함)
Element.prototype.firstElementChild 해당 요소의 첫 번째 자식 요소를 반환하며, 자식 요소가 없으면 null을 반환합니다. (텍스트 노드 미포함)
Element.prototype.lastElementChild 해당 요소의 마지막 자식 요소를 반환하며, 자식 요소가 없으면 null을 반환합니다. (텍스트 노드 미포함)

이렇게 테이블로 확인하니 차이가 명확하게 보인다.

모두 자식 노드를 탐색하는 프로퍼티들을 가지고 있으나 Node 의 프로토타입들은 요소노드 뿐이 아니라 텍스트 노드를 포함하여 자식 노드를 찾으며 Element 의 프로토타입들은 텍스트 노드를 미포함하고 요소 노드만을 찾는다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        padding: 10px;
      }
      .Parent div {
        border: 1px solid black;
      }
      .Parent div {
        border: 1px solid hotpink;
      }
    </style>
  </head>
  <body>
    <div class="Parent">
      <div class="Child1">1번자식</div>
      <div id="Child2">
        2번자식 (parentNode)
        <div>1번 손자 (previousElementSibling)</div>
        <div id="pick">2번 손자 (선택된 노드)</div>
        <div>3번 손자 (nextElementSibling)</div>
      </div>
      <div class="Child3">3번자식</div>
    </div>
    <script>
      const $picknode = document.getElementById('Child2');
      console.log('Nodes.prototype.childNodes vs Element.prototype.childern');
      console.log($picknode.childNodes);
      console.log($picknode.children);
      console.log(
        'Nodes.prototype.firstChild vs Element.prototype.firstElementChild',
      );
      console.log($picknode.firstChild);
      console.log($picknode.firstElementChild);
      console.log(
        'Nodes.prototype.lastchild vs Element.prototype.lastElementChild',
      );
      console.log($picknode.lastChild);
      console.log($picknode.lastElementChild);
    </script>
  </body>
</html>

이렇게만 봐도 Node 의 프로토타입과 Element의 프로토타입의 차이를 명확하게 볼 수 있다.

자식 노드 존재 확인

그렇다면 위에서 자식 노드들의 존재여부를 확인하는 프로퍼티들도 존재한다.

Nodes.prototype.childNodes 는 텍스트 노드를 포함한 자식 노드들을 모두 유사 배열 객체 형태로 가져왔기 때문에

Nodes.prototype.haschildNodes 를 통해 확인 할 수 있다.

Nodes.prototype.childNodes 로 불러온 후 length 프로퍼티를 참고해도 된다.

Element.prototype.childern 에서는 텍스트 노드를 제외한 요소 노드만을 가진 자식 노드들을 유사배열 객체로 가져왔기 때문에

Element.prototype.childElementCount 프로퍼티를 사용할 수 있다.

혹은 Element.prototype.childern 으로 볼러온 후 length 프로퍼티를 참고해도 된다.

형제 노드 확인

DOM 의 노드 객체들 중 요소 노드 , 텍스트 노드 , 어트리뷰트 노드 가 존재한다고 했다.

이 때 트리 구조의 형제 노드 란 같은 부모 노드 (직속)를 공유하는 노드이기 때문에

어트리뷰트 노드 는 부모 노드가 존재하지 않는다.

그럼으로 형제 노드를 조회 할 때 어트리뷰트 노드 는 포함되지 않는다.

프로퍼티 설명
Node.previousSibling 현재 노드의 이전 형제 노드를 반환합니다. (텍스트 노드 포함)
Node.nextSibling 현재 노드의 다음 형제 노드를 반환합니다. (텍스트 노드 포함)
Element.prototype.previousElementSibling 현재 요소의 이전 형제 요소를 반환합니다. (텍스트 노드 미포함)
Element.prototype.nextElementSibling 현재 요소의 다음 형제 요소를 반환합니다. (텍스트 노드 미포함)
이것도 명확하다.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        padding: 10px;
      }
      .Parent div {
        border: 1px solid black;
      }
      .Parent div {
        border: 1px solid hotpink;
      }
    </style>
  </head>
  <body>
    <div class="Parent">
      <div class="Child1">1번자식</div>
      <div id="Child2">
        2번자식 (parentNode)
        <div>1번 손자 (previousElementSibling)</div>
        <div id="pick">2번 손자 (선택된 노드)</div>
        <div>3번 손자 (nextElementSibling)</div>
      </div>
      <div class="Child3">3번자식</div>
    </div>
    <script>
      const $picknode = document.getElementById('Child2');
      console.log(`Node.previousSibling`);
      console.log($picknode.previousSibling);
      console.log(`Element.previousElementSibling`);
      console.log($picknode.previousElementSibling);
      console.log('$picknode.nextSibling');
      console.log($picknode.nextSibling);
      console.log('$picknode.nextElementSibling');
      console.log($picknode.nextElementSibling);
    </script>
  </body>
</html>

모든 형제 노드들을 가져오는 방법이 없을까 했지만 없다고 한다.
아마 부모 노드에서 조회하는 방법이 존재하기 때문인 것 같다.

      const $picknode = document.getElementById('Child2');
      const parentnode = $picknode.parentNode;
      console.log(parentnode.children);

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글