본문 바로가기

CS/모던 자바스크립트 Deep Dive

39장 DOM(1)

반응형

2023년 5월 1일 677p~700p

 

39.1 노드

브라우저 렌더링 엔진은 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다.

DOM은 HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조다.

 

39.1 노드

39.1.1 HTML 요소와 노드 객체

HTML 요소는 HTML 문서를 구성하는 개별적인 요소를 의미한다.

HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환된다. 이때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로, HTML 요소의 텍스트 콘텐츠는 텍스트 노드로 변환된다.

* 비선형 자료구조 : 하나의 자료 뒤에 여러 개의 자료가 존재할 수 있는 자료구조(트리, 그래프)
* 선형 자료구조 : 하나의 자료 뒤에 하나의 자료만 존재하는 자료구조(배열, 스택, 큐, 링크드 리스트, 해시 테이블)

39.1.2 노드 객체의 타입

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="banana">Apple</li>
      <li id="banana">Banana</li>
      <li id="banana">Orange</li>
    </ul>
    <script>
      // getElementById 메서드는 언제나 단 하나의 요소 노드를 반환한다.
      // 첫 번째 li 요소가 파싱되어 생성된 요소 노드가 반환된다.
      const $elem = document.getElementById('banana');

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

렌더링 엔진은 위 HTML 문서를 파싱하여 다음과 같이 DOM을 생성한다.

노드 객체는 상속 구조를 가지고 총 12개 종류가 있다.

1. 문서 노드(document node)

  • DOM 트리의 최상위에 존재하는 루트 노드, document 객체를 가리킴
  • document 객체는 브라우저가 렌더링한 HTML문서 전체를 가리키는 객체
  • window.document 혹은 document로 참조 가능
  • script 태그로 자바스크립트 파일이 분리되어 있어도 전역 객체 window.document에 바인딩된 객체를 따름 (HTML 문서당 document객체는 유일)
  • document객체는 DOM트리의 루트 노드 이므로 DOM 트리의 노드들에 접근하기 위한 진입점 역할을 함

2. 요소 노드(element node)

  • HTML 요소를 가리키는 객체
  • 각 HTML 요소들은 상속관계를 가지므로, 요소 노드는 문소의 구조를 표현

3. 어트리뷰트 노드(attribute node)

  • HTML 요소의 어트리뷰트를 가리키는 객체
  • 지정된 HTML 요소의 노드와 연결되어 있음
  • 요소 노드에만 연결되어 있고 부모 노드를 가지지 않음
  • 따라서, 어트리뷰트 노드에 접근하려면 요소 노드에 먼저 접근 해야함

4. 텍스트 노드(text node)

  • HTML 요소의 텍스트를 가리키는 객체
  • 요소 노드의 자식 노드
  • 자식 노드를 가질 수 없는 리프 노드(leaf node) 즉, DOM 트리의 최종단
  • 따라서 , 텍스트 노드에 접근하려면 부모노드인 요소 노드에 접근 해야함

39.1.3 노드 객체의 상속 구조

DOM은 DOM을 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조이다.

DOM을 구성하는 노드 객체는 브라우저 환경에서 제공하는 호스트 객체다. 

노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 갖는다. 노드 객체의 상속 구조는 다음과 같다.

노드 객체는 Object, EventTarget, Node 인터페이스를 상속받는다.

예를 들어 input 요소 노드 객체를 보자. input 요소 노드 객체는 HTMLInputElement, HTMLEIement, Element, Node, EventTarget, Object의 prototype에 바인딩되어 있는 프로토타입 객체를 상속받는다.

즉 input 요소 노드 객체는 프로토타입 체인에 있는 모든 프로토타입의 프로퍼티나 메서드를 상속 받아 사용할 수 있다.

<!DOCTYPE html>
<html>
<body>
  <input type="text">
  <script>
    // input 요소 노드 객체를 선택
    const $input = document.querySelector('input');

    // input 요소 노드 객체의 프로토타입 체인
    console.log(
      Object.getPrototypeOf($input) === HTMLInputElement.prototype,
      Object.getPrototypeOf(HTMLInputElement.prototype) === HTMLElement.prototype,
      Object.getPrototypeOf(HTMLElement.prototype) === Element.prototype,
      Object.getPrototypeOf(Element.prototype) === Node.prototype,
      Object.getPrototypeOf(Node.prototype) === EventTarget.prototype,
      Object.getPrototypeOf(EventTarget.prototype) === Object.prototype
    ); // 모두 true
  </script>
</body>
</html>

 

배열이 객체인 동시에 배열인 것처럼 input 요소 노드 객체도 다음과 같이 다양한 특성을 갖는 객체이며, 이러한 특성을 나타내는 기능들을 상속을 통해 제공받는다.

input 요소 노드 객체의 특성 프로토타입을 제공하는 객체
객체 Object
이벤트를 발생시키는 객체 EventTarget
트리 자료구조의 노드 객체 Node
브라우저가 렌더링할 수 있는 웹 문서의 요소(HTML, XML, SVG)를 표현하는 객체 Element
웹 문서의 요소 중에서 HTML 요소를 표현하는 객체 HTMLElement
HTML 요소 중에서 input 요소를 표현하는 객체 HTMLInputElement

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

 

39.2 요소 노드 취득

HTML의 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야 한다.

DOM은 요소 노드를 취득할 수 있는 다양한 메서드를 제공한다.

39.2.1 id를 이용한 요소 노드 취득

Document.prototype.getElementById 메서드는 아규먼트로 전달한 id 어트리뷰트 값을 갖는 하나의 요소 노드를 탐색하여 반환한다. id값은 HTML 문서 내에서 유일한 값이어야 하지만 여러 개 존재하더라도 에러가 발생하지 않기 때문에 관리를 잘 해야한다.

  • 인수로 전달된 id값을 갖는 첫 번째 요소 노드를 반환한다. (id 값이 중복되더라도 첫 번째 id값만 반환)
  • 요소가 존재하지 않는 경우 null을 반환한다.
<!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>
<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="banana">Apple</li>
      <li id="banana">Banana</li>
      <li id="banana">Orange</li>
    </ul>
    <script>
      // getElementById 메서드는 언제나 단 하나의 요소 노드를 반환한다.
      // 첫 번째 li 요소가 파싱되어 생성된 요소 노드가 반환된다.
      const $elem = document.getElementById('banana');

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

HTML 요소에 id 어트리뷰트를 부여하면 id 값과 동일한 이름의 전역 변수가 암묵적으로 선언되고 해당 노드 객체가 할당되는 부수 효과가 있다.

<!DOCTYPE html>
<html>
  <body>
    <div id="foo"></div>
    <script>
      // id 값과 동일한 이름의 전역 변수가 암묵적으로 선언되고 해당 노드 객체가 할당된다.
      console.log(foo === document.getElementById('foo')); // true

      // 암묵적 전역으로 생성된 전역 프로퍼티는 삭제되지만 전역 변수는 삭제되지 않는다.
      delete foo;
      console.log(foo); // <div id="foo"></div>
    </script>
  </body>
</html>

단 id 값과 동일한 이름의 전역 변수가 이미 선언되어 있으면 이 전역 변수에 노드 객체가 재할당되지 않는다.

<!DOCTYPE html>
<html>
  <body>
    <div id="foo"></div>
    <script>
      let foo = 1;

      // id 값과 동일한 이름의 전역 변수가 이미 선언되어 있으면 노드 객체가 재할당되지 않는다.
      console.log(foo); // 1
    </script>
  </body>
</html>

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

Document.prototype/Element.prototype.getElementsByTagName 메서드는 아규먼트로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환한다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
    <script>
      // 태그 이름이 li인 요소 노드를 모두 탐색하여 반환한다.
      // 탐색된 요소 노드들은 HTMLCollection 객체에 담겨 반환된다.
      // HTMLCollection 객체는 유사 배열 객체이면서 이터러블이다.
      const $elems = document.getElementsByTagName('li');

      // 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
      // HTMLCollection 객체를 배열로 변환하여 순회하며 color 프로퍼티 값을 변경한다.
      [...$elems].forEach(elem => { elem.style.color = 'red'; });
    </script>
  </body>
</html>

getElementsByTagName 메서드가 반환하는 DOM 컬렉션 객체인 HTMLCollection 객체유사 배열 객체이면서 이터러블이다.

39.2.3 class를 이용한 요소 노드 취득

Document.prototype/Element.prototype.getElementsByClassName 메서드는 아규먼트로 전달한 class 어트리뷰트 값을 갖는 모든 요소 노드들을 탐색하여 반환한다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li class="fruit apple">Apple</li>
      <li class="fruit banana">Banana</li>
      <li class="fruit orange">Orange</li>
    </ul>
    <script>
      // class 값이 'fruit'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환한다.
      const $elems = document.getElementsByClassName('fruit');

      // 취득한 모든 요소의 CSS color 프로퍼티 값을 변경한다.
      [...$elems].forEach(elem => { elem.style.color = 'red'; });

      // class 값이 'fruit apple'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환한다.
      const $apples = document.getElementsByClassName('fruit apple');

      // 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
      [...$apples].forEach(elem => { elem.style.color = 'blue'; });
    </script>
  </body>
</html>

39.2.4 CSS 선택자를 이용한 요소 취득 : querySelector / querySelectorAll

/* 전체 선택자: 모든 요소를 선택 */
* { ... }
/* 태그 선택자: 모든 p 태그 요소를 모두 선택 */
p { ... }
/* id 선택자: id 값이 'foo'인 요소를 모두 선택 */
#foo { ... }
/* class 선택자: class 값이 'foo'인 요소를 모두 선택 */
.foo { ... }
/* 어트리뷰트 선택자: input 요소 중에 type 어트리뷰트 값이 'text'인 요소를 모두 선택 */
input[type=text] { ... }
/* 후손 선택자: div 요소의 후손 요소 중 p 요소를 모두 선택 */
div p { ... }
/* 자식 선택자: div 요소의 자식 요소 중 p 요소를 모두 선택 */
div > p { ... }
/* 인접 형제 선택자: p 요소의 형제 요소 중에 p 요소 바로 뒤에 위치하는 ul 요소를 선택 */
p + ul { ... }
/* 일반 형제 선택자: p 요소의 형제 요소 중에 p 요소 뒤에 위치하는 ul 요소를 모두 선택 */
p ~ ul { ... }
/* 가상 클래스 선택자: hover 상태인 a 요소를 모두 선택 */
a:hover { ... }
/* 가상 요소 선택자: p 요소의 콘텐츠의 앞에 위치하는 공간을 선택
   일반적으로 content 프로퍼티와 함께 사용된다. */
p::before { ... }

Document.prototype/Element.prototype.querySelector 메서드는 아규먼트로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환한다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
    <script>
      // class 어트리뷰트 값이 'banana'인 첫 번째 요소 노드를 탐색하여 반환한다.
      const $elem = document.querySelector('.banana');

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

 

Document.prototype/Element.prototype.querySelectorAll 메서드는 아규먼트로 전달한 CSS 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환한다. querySelectorAll 메서드는 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 NodeList 객체를 반환한다. NodeList 객체는 유사 배열 객체이면서 이터러블이다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
    <script>
      // ul 요소의 자식 요소인 li 요소를 모두 탐색하여 반환한다.
      const $elems = document.querySelectorAll('ul > li');
      // 취득한 요소 노드들은 NodeList 객체에 담겨 반환된다.
      console.log($elems); // NodeList(3) [li.apple, li.banana, li.orange]

      // 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
      // NodeList는 forEach 메서드를 제공한다.
      $elems.forEach(elem => { elem.style.color = 'red'; });
    </script>
  </body>
</html>

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

Element.prototype.matches 메서드를 사용하여 아규먼트로 전달한 CSS 선택자를 통해 특정 요소 노드를 취득할 수 있는지 확인한다.

이벤트 위임을 사용할 때 유용한 방법이다.

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

39.2.6 HTMLCollection과 NodeList

DOM 컬렉션 객체인 HTMLCollection과 NodeList는 DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체이다. HTMLCollection과 NodeList는 모두 유사 배열 객체이면서 이터러블이다. 따라서 for...of 문으로 순회할 수 있으며 스프레드 문법을 사용하여 간단히 배열로 변환할 수 있다.

getElementsByTagName, getElementByClassName 메서드가 반환하는 HTMLCollection언제나 live 객체로 동작한다. NodeList는 특정 경우에만 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>

https://ghost4551.tistory.com/221

결과 화면 예상대로 동작하지 않는다.

위 예제는 HTMLCollection 객체를 for 문으로 순회화며 class 값을 'red'에서 'blue'로 변경했다.하지만 두 번째 요소는 바뀌지 않았다. 이유는 다음과 같다. 

  1. 첫 번째 반복 (i==0)
    $elems[0] li의 첫 번째 요소가 'blue'로 변경된다. 이때 첫 번째 li 요소가 변경되었으므로 getElementsByClassName 메서드의 인자로 전달한 'red'와는 더는 일치하지 않기 때문에 HTMLCollection 객체의 $elems에서 실시간으로 제거된다.
  2. 두 번째 반복 (i==1)
    첫 번째 반복에서 li 요소가 삭제되었으므로 뒤에 남아 있던 리스트들은 한 칸씩 앞으로 밀리게 된다. 따라서 $elems[0]은 두 번째 li 요소가 되고 $elems[1]은 세 번째 li요소가 된다. 즉, 세 번째 li의 값이 바뀌고 또한 제거된다.
  3. 세 번째 반복 (i==2)
    첫 번째, 세번째 li 요소가 제거되고 $elems에는 두 번째 li 태그만 남아있게 되어 $elem.length가 1이 된다. 조건식에 맞지 않으므로 반복이 종료된다.

이처럼 HTMLCollection 객체는 실시간으로 노드 객체의 상태 변경을 반영하기 때문에 for 문 순회를 통해 상태를 변경할 때는 주의해야 한다. 

이 문제는 for문 역순, while로 객체가 남아 있지 않을 때까지 무한 반복하는 방법을 사용하거나,

// while 문으로 HTMLCollection에 요소가 남아 있지 않을 때까지 무한 반복
let i = 0;
while ($elems.length > i) {
  $elems[i].className = 'blue';
}

HTMLCollection 객체를 사용하지 않는 고차함수로 해결할 수 있다.

// 유사 배열 객체이면서 이터러블인 HTMLCollection을 배열로 변환하여 순회
[...$elems].forEach(elem => elem.className = 'blue');

 

또한 querySelectorAll이 반환하는 NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체로, querySelectorAll을 활용하여 해결할 수도 있다.

하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 동일하게 실시간 객체로 동작하므로 주의가 필요하다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // childNodes 프로퍼티는 NodeList 객체(live)를 반환한다.
    const { childNodes } = $fruits;
    console.log(childNodes instanceof NodeList); // true

    // $fruits 요소의 자식 노드는 공백 텍스트 노드(39.3.1절 "공백 텍스트 노드" 참고)를 포함해 모두 5개다.
    console.log(childNodes); // NodeList(5) [text, li, text, li, text]

    for (let i = 0; i < childNodes.length; i++) {
      // removeChild 메서드는 $fruits 요소의 자식 노드를 DOM에서 삭제한다.
      // (39.6.9절 "노드 삭제" 참고)
      // removeChild 메서드가 호출될 때마다 NodeList 객체인 childNodes가 실시간으로 변경된다.
      // 따라서 첫 번째, 세 번째 다섯 번째 요소만 삭제된다.
      $fruits.removeChild(childNodes[i]);
    }

    // 예상과 다르게 $fruits 요소의 모든 자식 노드가 삭제되지 않는다.
    console.log(childNodes); // NodeList(2) [li, li]
  </script>
</html>

 

따라서 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장한다. 모두 유사 배열 객체이면서 이터러블이므로 스프레드 문법이나 Array.from 메서드를 사용하여 간단히 배열로 변환할 수 있고, 배열 변환 후 고차함수를 사용할 수 있다.

 

반응형

'CS > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글

39장 DOM(3)  (0) 2023.05.03
39장 DOM(2)  (0) 2023.05.02
38장 브라우저의 렌더링 과정  (0) 2023.04.29
34장 이터러블  (0) 2023.04.28
26장 ES6 함수의 추가 기능  (0) 2023.04.27