자바스크립트 딥다이브 - DOM [노드 개념과 노드 탐색]

ChoiYongHyeun·2023년 12월 25일
0

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

노드

HTML 요소와 노드 객체

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

<div class = 'greeting'>Hello</div>

라는 태그가 있을 때

  • <div> : 시작태그
  • class : 어트리뷰트 이름
  • gretting : 어트리뷰트 값
  • Hello : 콘텐츠
  • </div> : 종료 태그

로 나눌 수 있다.

렌더링 엔진은 해당 태그를 파싱하여 DOM을 구성하는 요소 노드 객체로 변환한다.

이 때

  • 태그의 종류를 나타내는 시작 태그와 종료태그요소 노드
  • 어트리뷰트를 나타내는 어트리뷰트 이름과 값어트리뷰트 노드
  • 콘텐츠를 나타내는 콘텐츠콘텐츠 노드로 객체가 생성된다.

이렇게 생성된 객체를 계층적 구조에 따라 트리 구조를 만든다.

노드 객체의 타입

그래서 다음과 같은 DOM 이 생성 되었을 때 노드 별 특징을 정리해보자

문서 노드 (Document)

DOM 트리의 최상단에 존재하는 루트 노드로서 document 객체를 가리킨다.

document 객체는 브라우저가 렌더링 한 HTML 문서 전체를 가리키는 객체로서 전역 객체 windowdocument 프로퍼티에 바인딩 되어 있다.

브라우저 환경의 모든 자바스크립트 코드는 script 태그 별로 분리되어 있어도 하나의 전역 객체인 window를 공유한다.

즉, 문서당 document 객체는 하나로 유일하다.

모든 트리 노드의 최상단 노드로 다른 노드들에 접근하기 위한 진입점역할을 한다.

요소 노드 (html , body , head , ul ...)

요소를 가리키는 객체로, 부모, 자식, 형제 관계를 가지며, 이러한 정보를 구조화 한다.
요소노드는 문서의 구조를 표현한다.

어트리뷰트 노드 (class , id)

어트리뷰트가 지정된 요소 노드와 연결 되어 있다.

하지만 어트리뷰트 노드는 부모 노드가 존재하지 않으며 오로지 요소노드와만 연결되어 있다.

그럼 요소노드의 형제노드인가 ?

같은 부모 노드를 공유하지 않기 때문에 형제노드라고 보기는 어렵다.

따라서 어트리뷰트 노드에 접근하기 위해서는 필수적으로 해당 어트리뷰트 노드와 연결되어 있는 요소 노드에 접근해야 한다.

텍스트 노드

텍스트 노드는 HTML 요소의 텍스트를 가리키는 객체이다.

요소 노드의 자식 노드이며 자식 노드를 가질 수 없는 리프 노드이다.

따라서 텍스트 노드에 접근하기 위해서는 요소 노드에 접근해야 한다.

노드 객체의 상속 구조

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

이를 통해 자신의 계층 구조를 이용해 부모, 자식, 형제 노드를 탐색 할 수 있으며 어트리뷰트와 택스트를 조작 할 수 있다.

DOM 을 구성하는 노드 객체는 표준 빌트인 객체가 아닌 브라우저 환경에 따라 추가적으로 제공되는 호스트 객체이다.

호스트 객체

호스트 객체(Host Object)는 JavaScript 환경에 따라 추가적으로 제공되는 객체로, 브라우저 환경에서는 DOM이나 BOM(Browser Object Model)과 관련된 객체들이 여기에 해당합니다. 호스트 객체는 JavaScript 엔진이 아닌 호스트 환경에서 제공되는 객체로, 브라우저에서 실행되는 JavaScript 코드에서는 브라우저가 이를 제공합니다.

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

모든 노드 객체는 공통적으로 Object => EventTarget => Node 의 프로토타입을 상속 받는다.

  • 문서 노드는 Document , HTMLDocument 를 상속 받는다.
  • 요소 노드는 Element => HTMLElement 를 상속 받는다. (이후엔 요소 별 프로토타입을 상속 받는다.)
  • 어트리뷰트 노드는 Attr을 상속 받는다.
  • 텍스트 노드는 CharcterData를 상속 받는다.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <script>
      const $input = document.querySelector('input');
      console.log(Object.getPrototypeOf($input) === HTMLInputElement.prototype);
      console.log(
        Object.getPrototypeOf(HTMLInputElement.prototype) ===
          HTMLElement.prototype,
      );
      console.log(Object.getPrototypeOf(HTMLElement.prototype)) ===
        Element.prototype;
      console.log(Object.getPrototypeOf(Element.prototype) === Node.prototype);
      console.log(
        Object.getPrototypeOf(Node.prototype) === EventTarget.prototype,
      );
      console.log(
        Object.getPrototypeOf(EventTarget.prototype) === Object.prototype,
      );
    </script>
  </body>
</html>

상속 관계를 하단에서부터 상단으로 올라가며 확인해보면 다음과 같다.

이렇게 프로토타입 상속 관계를 통하여 요소 노드 별 공통되는 프로퍼티나 메소드를 효과적으로 관리 할 수 있다.

예를 들어 input 태그와 같이 Ojbect => EventTarget => Node => Element => HTMLElement => HTMLInputElement 의 프로토타입 체인을 따르는 태그만 가지고 있는 메소드는

HTMLInputElement 의 프로토타입을 통해 상속 받는다.

하지만 input 태그 뿐이 아니라 다른 div , span , p ... 등 많은 요소 태그들이 공통적으로 가지는 프로퍼티나 메소드 (예를 들어 모든 요소 태그들은 style 이란 프로퍼티를 갖는다.)들은 상위 프로토타입 체인인 Element 의 프로토타입을 상속 받는다.

Element 뿐이 아니라 다른 노드를 탐색해야 하는 기능은 상위 프로토타입인 Node 의 프로토타입을 통해, 또 이벤트를 발생 시킬 수 있는 기능은 상위 프로토타입인 EventTarget 의 프로토타입을 통해

효과적으로 프로퍼티와 메소드를 관리 한다.

정리

HTML 문서의 내용을 DOM 을 통해 삽입, 삭제, 탐색 및 수정이 가능한 자료구조로 생성하고 , 이런 자료구조를 통해 프로토타입 체인을 구성하여 공통된 프로퍼티나 메소드를 효과적으로 관리한다.

제일 중요한 내용은 DOM 을 통해 노드들에 접근하여 DOM 을 자바스크립트를 통해 리플로우, 리페인트 할 수 있도록 하는 DOM API를 제공한다는 점이다.


요소 노드 취득

DOM API 를 통해 요소 노드에 접근하여 삽입, 삭제 및 수정을 할 수 있다고 했다.

요소 노드 취득은 HTML 요소를 조작 하는 시작점이다.

DOM 은 요소 노드 취득을 하기 위한 다양한 메소드를 제공한다.

id 를 이용한 요소 노드 취득

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

포인트

  • 모든 요소 노드를 탐색 할 수 있게 하기 위하여 가장 최상단 노드인 Document 의 프로토타입 메소드를 사용한다.
  • id 는 단 하나 가질 수 있기 때문에 하나만 탐색하여 반환한다.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">orange</li>
    </ul>
    <script>
      const $elem = document.getElementById('apple');
      $elem.style.color = 'red';
    </script>
  </body>
</html>

idapple 인 요소 노드를 탐색하여 해당 요소 노드 객체의 style 프로퍼티에 접근하여 값을 변경하였다.

만약 해당 id 가 중복되더라도, 가장 먼저 탐색 되는 id 만 찾는다.

만약 존재하지 않는 id 를 가져오려 하면 null 값이 반환된다.

id 요소로 생성한 요소 노드는 동일한 이름의 전역 변수가 암묵적으로 선언되고, 해당 노드 객체가 선언된 전역 변수에 바인딩 된다

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="foo">foo!~~!!!!~!!</div>
    <script>
      console.log(foo === document.getElementById('foo')); // true
      delete foo;
      console.log(foo); //     <div id="foo">foo!~~!!!!~!!</div>

    </script>
  </body>
</html>

태그 이름으로 취득하기

Document.prototype / Element.prototype.getElementsByTagName 메소드는 인수로 전달한 태그 이름을 갖는 모든 요소들을 탐색하여 반환한다.

여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li>orange</li>
      <li>banana</li>
      <li>apple</li>
    </ul>
    <script>
      $elems = document.getElementsByTagName('li');
      const colors = ['red', 'blue', 'orange'];
      console.log($elems);
      console.log(typeof $elems);
      [...$elems].forEach((elem, i) => {
        elem.style.color = colors[i];
      });
    </script>
  </body>
</html>

getElementByTagNames 메소드가 반환하는 HTMLCollection 객체는 유사 배열 객체이며 이터러블이다.

위에서 살펴보면 인덱스 역할을 하는 프로퍼티와 length 가 존재하는 것을 볼 수 있다.

모든 태그 이름을 가져오는 방법

getElementByTagName 의 인수에 * 를 넣어주면 모든 태그 이름을 가져 올 수 있다.

document.getElementbyTagName vs Element.getElementbyTageName

document 에서부터 탐색을 시작하는 getElementbyTagName 은 모든 문서 내에서 tag name이 인수와 같은 것을 가져왔다.

이렇게 해서 가져와진 HTMLCollection 객체는 Element.prototype.getElementsByTageName 를 상속 받아 사용 할 수 있다.

HTMLCollection 객체에서 getElementsByTageName 메소드를 사용하면 프로토타입 체인에서 가장 가까운 상위 체인이 Element 이기 때문에 Element 의 메소드가 호출된다.

class 를 이용한 요소 노드 취득

Document.prototype / Element.prototype.getElementsByClassName 은 인수로 전달 받은 class 명을 갖는 모든 요소 노드를 탐색하여 반환한다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul id="fruits">
      <li class="apple">apple</li>
      <li class="banana">banana</li>
      <li class="orange">orange</li>
    </ul>
    <div class="banana">banana</div>
    <script>
      const $AllBananaFromDocument = document.getElementsByClassName('banana');
      console.log($AllBananaFromDocument); // document 내에 존재하는 모든 클래스명 탐색
      const $fruits = document.getElementById('fruits'); // id 가 fruits 인 노드 탐색
      console.log(`$fruits : ${$fruits}`);
      console.log($fruits);
      const $BananaFromfruits = $fruits.getElementsByClassName('banana'); 
		// id 가 fruits 인 노드에서 class 명이 banana인 노드 탐색
      console.log($BananaFromfruits);
      [...$BananaFromfruits].forEach((elem) => (elem.style.color = 'yellow '));
    </script>
  </body>
</html>

프로토타입 상속을 이용하여 노드들을 탐색하는 모습을 볼 수 있다.

이 때 특징적인 것은 2개 이상의 노드를 취득하게 하는 getElementsByClassNameHMTLCollection 이란 객체 타입으로 찾아진다는 것이다. (유사배열 객체이며 이터러블한)

또한 document.get ... 메소드를 이용하면 문서 내에 존재하는 모든 노드를 찾는데

이 때 찾아서 반환된 HTMLCollection 객체에서 Element.get ... 메소드를 사용하면, 찾아진 객체 내에서만 탐색이 가능하단 점이다.

CSS 선택자를 이용한 요소 노드 취득

CSS 선택자는 스타일을 적용하고자 하는 HTML 요소를 특정 할 때 사용하는 문법이다.

* {} /* 모든 요소 취득 */
p {} /* 모든 p 태그 요소 취득 */
#foo {} /* id 가 foo 인 요소 취득 */
.bar {} /* class 가 bar 인 요소 취득 */
div p {} /* div 요소의 모든 하위 노드 중 p 요소 취득 */

p + ul {} /* 인접 형제 선택자 */
p ~ ul {} /* 일반 형제 선택자*/

처럼 CSS 의 선택자 문법을 이용하여 태그를 취득 할 수 있다.

Document.prototype / Element.prototype.querySelector 메소드를 이용할 수 있으며 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환한다.

  • CSS 선택자를 만족시키는 요소가 여러개일 경우엔 첫 번째 요소 노드만 반환한다.
  • CSS 선택자를 만족시키는 요소 노드가 존재하지 않으면 null 값을 반환한다.
  • CSS 선택자가 문법에 맞지 않는 경우 DOMException 에러가 발생한다.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul id="fruits">
      <li class="apple">apple</li>
      <li class="banana">banana</li>
      <li class="orange">orange</li>
    </ul>
    <div class="banana">banana</div>
    <script>
      const $bananaFromDocument = document.querySelector('.banana');
      console.log($bananaFromDocument);
      const $bananaFromdiv = document.querySelector('div.banana');
      console.log($bananaFromdiv);

      $bananaFromDocument.style.color = 'red';
      $bananaFromdiv.style.color = 'orange';
    </script>
  </body>
</html>

좀 더 복잡한 예제를 가지고 연습해봐야겠다.

<!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></style>
  </head>
  <body>
    <div class="InnerWraper">
      <div class="InnerItems">
        <div>
          <ul>
            <li id="orange">orange</li>
            <li id="banana">banana</li>
            <li id="apple">apple</li>
          </ul>
          <div id="fruits">
            <ul>
              <li id="orange">orange</li>
              <li id="banana">banana</li>
              <li id="apple">apple</li>
            </ul>
          </div>
        </div>
      </div>
    </div>
    <script>
      const $BananaFromDocument = document.querySelector('#banana');
      const $BananaFromfruits = document.querySelector('#fruits>ul>#banana');

      $BananaFromDocument.style.color = 'red';
      $BananaFromfruits.style.color = 'orange';
    </script>
  </body>
</html>

아 나는 도대체 사람들 코드를 구경하다 보이는 querySelector 가 뭘까 .. .저게 그 말로만 듣던 jQuery 인가 .. 그랬는데 CSS 선택자였다.!!

.querySelector 는 하나의 요소 노드만 가져온다고 했었다.

그럼 2개 이상의 노드를 가져오고 싶다면 ?

.querySelectorALL 을 사용하면 된다.

<!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></style>
  </head>
  <body>
    <div class="InnerWraper">
      <div class="InnerItems">
        <div>
          <ul>
            <li id="orange">orange</li>
            <li id="banana">banana</li>
            <li id="apple">apple</li>
          </ul>
          <div id="fruits">
            <ul>
              <li id="orange">orange</li>
              <li id="banana">banana</li>
              <li id="apple">apple</li>
            </ul>
          </div>
        </div>
      </div>
    </div>
    <script>
      const $liFromDocument = document.querySelectorAll('div li');
      const $bananaFromDocument = document.querySelectorAll('div #banana');
      const $fruitsFromDocument = document.querySelectorAll('#fruits li');

      [...$liFromDocument].forEach((li) => (li.style.color = 'blue'));
      [...$fruitsFromDocument].forEach((elem) => (elem.style.color = 'pink'));
      [...$bananaFromDocument].forEach(
        (banana) => (banana.style.color = 'orange'),
      );
    </script>
  </body>
</html>

querySelectorALL 을 이용하니 이전과 같은 유사배열 객체 형태로 받을 수 있음을 확인 할 수 있었다.

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

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul id="fruits">
      <li id="orange">orange</li>
      <li id="banana">banana</li>
      <li id="apple">apple</li>
    </ul>
    <script>
      const $apple = document.querySelector('#apple');
      const $banana = document.querySelector('#banana');
      const query = 'ul #apple';

      console.log(
        `apple 은 ${query} 로 선택 할 수 있는가 ? : ${$apple.matches(query)}`,
      );

      console.log(
        `banana 은 ${query} 로 선택 할 수 있는가 ? : ${$banana.matches(query)}`,
      );
    </script>
  </body>
</html>

이는 이벤트 위임을 할 때 유용하다.

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

0개의 댓글