#1 DOM part.1

정지훈·2020년 12월 22일
0

DOM이란

DOM이란 렌더링 엔진은 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다. DOM은 HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API,즉 프로퍼티와 메서드를 제공하는 트리 자료 구조이다.

트리 자료구조는 노드들의 계층 구조로 이뤄진다. 즉 트리자료 구조는 부모 노드와 자식 노드로 구성되어 노드간의 계층적 구를 표현하는 비선형 자료구조 자료구조는 하나의 자료뒤에 여러 개의 자료가 존재할 수 있는 자료구조다. 트리 자료구조는 하나의 최상위 노드에서 시작한다. 최상위 노드는 부모노드가 없으며 루트 노드라 한다. 루트 노드는 0개 이상의 자식 노드를 갖고 자식 노드가 없는 노드를 리프 노드라 한다.

노드 객체들로 구성된 트리 자료구조를 DOM이라 한다.

노드 객체의 타입

렌더링 엔진은 HTML 문서를 파싱하여 DOM 트리를 생성한다.

HTML요소 사이의 공백은 Text노드가 된다.

이처럼 DOM은 노드 객체의 계층적인 구조로 구성된다. 노드 객체는 종류가 있고 상속 구조를 갖는다. 노드 객체는 총 12개의 종류가 있다. 이 중에서 중요한 노드 타입은 다음과 같이 4가지이다.

1. 문서 노드
2. 요소노드
3. 어트리뷰트 노드
4. 텍스트 노드

문서 노드

문서노드는 DOM트리의 최상위에 존재하는 루트 노드이다.
즉 document 객체를 가리키고 브라우저가 렌더링한 HTML 문서 전체를 가리키는 객체로서 전역 객체 window의 document 프로퍼티에 바인딩 되어 있다.

document 객체는 DOM트리의 루트 노드이므로 DOM 트리의 노드들에 접근하기 위한 진입점 역할을 담당한다. 즉 요소,어트리뷰트,텍스트 노드에 접근하려면 문서 노드를 통해야 한다.

요소 노드

요소노드는 HTML 요소를 가리키는 객체다. 요소 노드는 HTML 요소 간의 중첩에 의해 부자 관계를 가지며, 이 부자 관계를 통해 정보를 구조화 한다. 따라서 요소노드는 문서의 구조를 표현한다.

어트리뷰트 노드

어트리뷰트 노드는 HTML 요소의 어트리뷰트를 가리키는 객체다.
어트리뷰트 노드는 어트리뷰트가 지정된 HTML 요소의 요소 노드와 연결되어 있다. 단, 요소 노드는 부모 노드와 연결되어 있지만 어트리뷰트 노드는 부모 노드와 연결되어 있지 않고 요소 노드에만 열결되어 있다.

즉 어트리뷰트 노드는 부모 노드가 없으므로 요소노드의 형제 노드는 아니다. 따라서 어트리뷰트 노드에 접근하여 어트리뷰트를 참조하거나 변경하려면 먼저 요소노드에 접근해야 한다.

텍스트 노드

텍스트 노드는 HTML 요소의 텍스트를 가리키는 객체다. 요소 노드가 문서의 구조를 표현한다면 텍스트 노드는 문서의 정보를 표현한다고 할 수 있다.
텍스트 노드는 요소 노드의 자식 노드이며 자식 노드를 가질 수 없는 리프노드다.

즉 텍스트 노드는 DOM 트리의 최종단이다. 따라서 텍스트 노드에 접근하려면 먼저 부모 노드인 요소 노드에 접근해야한다.

노드 객체도 자바스크립트 객체기 때문에 prototype으로 상속 구조를 갖고 있다.

모든 노드 객체는 Object, EventTarget,Node 인터페이스를 상속받고 문서노드는 Document, HTMLDocument 인터페이스를 상속 받고
어트리뷰트 노드 또한 Attr, 텍스트 노드도 CharacterData 인터페이스를 각각 상속 받는다.

DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류 즉 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공한다. 이 DOM API를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.

요소 노드 취득

id를 이용한 요소 노드 취득

Document.prototype.getElementById 메서드를 이용해서 인수로 전달한 id 어트리뷰트 값을 갖는 하나의 요소 노드를 탐색하여 반환한다.

따라서 반드시 문서 노드인 document를 통해 호출해야 한다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
    <script>
      // id 값이 'banana'인 요소 노드를 탐색하여 반환한다.
      // 두 번째 li 요소가 파싱되어 생성된 요소 노드가 반환된다.
      const $elem = document.getElementById('banana');

      // 취득한 요소 노드의 style.color 프로퍼티 값을 변경한다.
      $elem.style.color = 'red';
    </script>
  </body>
</html>

위 와같이 id값이 banana인 요소 노드를 탐색해서 반환하고 취득한 요소 노드의 style.color 프로퍼티 값을 변경할 수 있다.

태그 이름을 이용한 요소 노드 취득

Document.prototype.getElementsByTagName,
Element.prototype.getElementsByTagName
메서드는 인수로 전달한 태그이름을 갖는 모든 요소 노드들을 탐색하여 반환한다. 이 메서드는 여러개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다.

즉 이 객체는 유사배열 객체이면서 이터러블이다.

여기서 잠깐..

이렇게 이용하는 여러가지 태그를 가져오는 메서드를 일일이 이용하는 것 보다는 id,class,요소를 가져 올 수 있는 메서드가 있다.

그것은 querySelector과 querySelectorAll을 쓰면 된다.

Document.prototype.querySelector 메서드는 인수로 전달한 CSS 선택자를 만족하는 하나의 요소 노드를 탐색해서 반환하고

Document.prototype.querySelectorAll은 말 그대로 모든 요소 노드를 탐색하여 반환하고 여러개의 요소노드 객체를 갖는 DOM 컬렉션 객체인 NodeList 객체를 반환한다.

즉 이 객체도 유사 배열 객체이면서 이터러블한 객체이다.
(만약 인수로 전달한 CSS 선택자가 문법에 맞지 않는 경우 DOMException 에러가 발생한다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
    <script>

      const $elems = document.querySelectorAll('ul > li');

      console.log($elems); // NodeList(3) [li.apple, li.banana, li.orange]

 
      $elems.forEach(elem => { elem.style.color = 'red'; });
    </script>
  </body>
</html>

가독성은 좋게 id는 getElementById 메서드를 이용하고 나머지는 querySelector,querySelectorAll을 쓰자.
*하지만 이건 제 개인 생각일 뿐 쓰고 싶은데로 쓰는데 이런 방법이 쉬운 방법..

NodeList는 forEach 메서드를 제공한다. 이것만 빼면 유사 배열 객체 이므로 map이나 reduce 같은 Array.prototype 메서드를 사용하지 못한다.

사용하려면 스프레드 문법으로 배열로 풀어서 사용하면 된다.

특정 요소 노드를 취득할 수 있는지 확인

Element.prototype.matches 메서드는 인수로 전달한 CSS 선택자를 통해 특정 요소 노드를 취득할 수 있는지 확인 합니다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
  </body>
  <script>
    const $apple = document.querySelector('.apple');

    // $apple 노드는 '#fruits > li.apple'로 취득할 수 있다.
    console.log($apple.matches('#fruits > li.apple'));  // true

    // $apple 노드는 '#fruits > li.banana'로 취득할 수 없다.
    console.log($apple.matches('#fruits > li.banana')); // false
  </script>
</html>

matches메서드는 이벤트 위임을 사용할 때 유용합니다.

HTMLCollection과 NodeList

HTMLCollection과 NodeList는 DOM APU가 여러개의 결과값을 반환하기 위한 DOM 컬렉션 객체이다.

이 모두 유사 배열 객체이므로 for..of를 사용할 수 있다.

물론 for문을 통해 순회도 가능하다.

하지만 왜 그냥 for문을 쓰지 배열로 바꿔서 사용을 할까?
이건 바로 이 두개에 중요한 특징인 살아있는 객체라는 것이다.

HTMLCollection

일단 HTMLCollection은 언제나 live 객체로 동작한다. 단 NodeList는 대부분의 경우 노드 객체의 상태 변화를 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하지만 경우에 따라 live 객체로 동작 할 때 가 있다.

<!DOCTYPE html>
<head>
  <style>
    .red { color: red; }
    .blue { color: blue; }
  </style>
</head>
<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>

이렇게 하면 모두다 blue의 클래스를 가진 파란색 글씨로 출력이 되길 바라지만 그렇지 않고 아래와 같이 출력이 된다.

예상대로 동작하지 않은 이유는 $elems.length는 3이므로 3번 반복이 된다.

i === 0 일경우 첫번째 요소가 class 값이 'red'에서 'blue'로 변경된다. 이때 red에서 blue로 변경되었으니 $elems에서 실시간으로 제거가 된다. 그래서 실시간으로 상태를 변경을 반영하는 살아있는 DOM 컬렉션 객체라고 한다.

그리고 두번째 반복에서 i === 1일 경우 첫번째 반복에서 li요소는 제거가 되어서 세번째 요소가 된다. 그리고 마찬가지로 blue로 바뀌고 제거가 되고
마지막 i === 2 일 경우 2번째 요소만 남아 있다 그리고 $elems.length는 1이므로 for문의 조건식이 false로 평가되어 반복이 종료되어서 이렇게 된다.

이를 방지하기 위해서는 for문을 역방향으로 순회하거나 wile문으로 남아있지 않을 때까지 무한 반복 하는 방법으로도 회피할 수 있다.

하지만 이는 좀 머리를 써야하는 부분이다. 그래서 차라리 더 나은 해결책은 HTMLCollection 객체를 사용하지 않거나 쓰고 싶다면 배열로 변환해서 배열의 고차 함수를 사용하는 것 이 더 좋은 해결 방식이다.

[...$elems].forEach(elem => elem.className = 'blue');

NodeList

HTMLCollection 객체의 부작용을 해결하기 위해 getElementsByTagName, getElementsByClassName 메서드 대신 querySelectorAll 메서드를 사용하는 방법이 있다.

이 메서드는 NodeList 객체를 반환하고 이때 NodeList 객체는 실시간으로 노드 객체의 상태를 반영하지 않는 non-live 객체다.

그리고 좋은점은 Array.prototype.forEach메서드와 같이 NodeList.prototype.forEach 메서드를 상속받아 사용할 수 있다.

그 외에도 item, entries,keys,values를 제공한다.

하지만 NodeList가 모두 non-live객체로 동작하는 것만이 아니다. 라이브 객체로 동작하는 일도 있다,

그것은 childNodes 프로퍼티가 반환하는 NodeList객체는 live객체로 동작하므로 주의가 필요하다.

즉 배열로 만들어 줘서 사용하는 것이 좋다.

출저: https://poiemaweb.com/fastcampus/dom

0개의 댓글