본문 바로가기

스터디/[모던 자바스크립트 deep dive] JS

#39. DOM(Document Object Model)

1. 노드

HTML 요소와 노드 객체

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

  • HTML 요소 간에는 중첩 관계 (콘텐츠 영역에 다른 HTML 요소도 포함되는 것) 에 의해 계층적인 부자 관계가 형성됨 -> 이를 반영하여 HTML 문서의 구성 요소인 HTML 요소를 객체화한 모든 노드 객체들을 트리 자료구조로 구성한다.
  • 트리 자료 구조 : 부모 노드와 자식 노드로 구성되어 노드 간의 계층적 구조(부자, 형제 관계)를 표현하는 비선형 자료구조
  • DOM : 노드 객체들로 구성된 트리 자료구조 (DOM 트리라고 부르기도 함)

노드 객체의 타입

  • 노드 객체는 총 12개의 종류가 있다.

문서 노드

  • DOM 트리 최상위에 존재하는 루트 노드
  • document 객체를 가리킴
    • document 객체 : 브라우저가 렌더링한 HTML 문서 전체를 가리키는 객체로서 전역 객체 window의 document 프로퍼티에 바인딩되어 있음
  • 문서 노드는 window.document 또는 document로 참조 가능
  • HTML 문서당 document 객체는 유일
  • document 객체는 DOM 트리의 루트 노드이므로 DOM 트리의 노드들에 접근하기 위한 진입점 역할을 담당

요소 노드

  • HTML 요소를 가리키는 객체
  • 문서의 구조를 표현

어트리뷰트 노드

  • HTML 요소의 어트리뷰트를 가리키는 객체
  • 어트리뷰트가 지정된 HTML 요소의 요소 노드와 연결되어 있음
  • 어트리뷰트를 참조하거나 변경하려면 먼저 요소 노드에 접근해야 함

텍스트 노드

  • HTML 요소의 텍스트를 가리키는 객체
  • 문서의 정보를 표현
  • 요소 노드의 자식 노드이며, 자식 노드를 가질 수 없는 리프 노드

노드 객체의 상속 구조

  • DOM을 구성하는 노드 객체는 자신의 구조와 정보를 제어할 수 있는 DOM API를 사용할 수 있다. 
    => 노드 객체는 자신의 부모, 형제, 자식을 탐색 가능, 자신의 어트리뷰트와 텍스트 조작 가능 

  • 위 그림처럼, 모든 노드 객체는 Object, EventTarget, Node 인터페이스를 상속받음
    • 추가로, 문서 노드는 Document, HTMLDocument 인터페이스를 상속받고, 어트리뷰트 노드는 Attr, 텍스트 노드는 CharacterData 인터페이스를 각각 상속받음. 요소 노드는 Element 인터페이스와 HTMLElement, 종류 별로 세분화된 HTMLhtmlElement, HTMLHeadElement, HTMLBodyElement ... 등의 인터페이스를 상속받음
  • ex) input 요소를 파싱하여 객체화한 input 요소 노드 객체는 HTMLInputElement, HTMLElement, Element, Node, EventTarget, Object의 prototype에 바인딩되어 있는 프로토 타입 객체를 상속받음

  • input 요소 노드 객체도 아래와 같은 기능들을 상속을 통해 제공 받는다.

 

* 노드 객체의 상속 구조는 개발자 도구의 Elements 패널 우측의 Properties 패널에서 확인 가능하다.

 

  • 모든 노드 객체가 공통적으로 갖는 기능 : EventTarget 인터페이스가 제공하는 이벤트 관련 기능 (EventTarget.addEventListener, EventTarget.removeEventListener 등), Node 인터페이스가 제공하는 트리 탐색 기능 (Node.parentNode, Node.childNodes, Node.previousSibiling, Node.nextSibiling 등), 노드 정보 제공 기능(Node.nodeType, Node.nodeName 등)
  • 요소 노드 객체는 HTML 요소가 갖는 공통적인 기능이 존재 
    • input 요소 노드 객체와 div 요소 노드 객체는 모두 HTML 요소의 스타일을 나타내는 style 프로퍼티 존재
  • HTML 요소의 종류에 따라 고유한 기능도 존재
    • input 요소 노드 객체는 value 프로퍼티가 필요하지만, div 요소 노드 객체는 value 프로퍼티가 필요하지 않음
      => 필요한 기능을 제공하는 인터페이스 (HTMLInputElement, HTMLDivElement 등) 가 HTML 요소의 종류에 따라 각각 다름
DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API(Application Programming Interface)로 제공한다. 이 DOM API를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.

 

2. 요소 노드 취득

HTML의 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야 한다.
DOM은 요소 노드를 취득할 수 있는 다양한 메서드를 제공한다.

id를 이용한 요소 노드 취득

  • Document.prototype.getElementById 메서드는 인수로 전달한 id 어트리뷰트 값을 갖는 하나의 요소 노드를 탐색하여 반환한다.
    • getElementByld 메서드는 Document.prototype의 프로퍼티
    • getElementByld 메서드는 언제나 단 하나의 요소 노드를 반환함

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

  • Document.prototype / Element.prototype.getElementsByTagName 메서드는 인수로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환한다.
    • getElementsByTagName  메서드는 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체 (유사 배열 객체이자 이터러블) 를 반환한다.
  • HTML 문서의 모든 요소 노드를 취득하려면 getElementsByTagName 메서드의 인수로 '*'를 전달한다.
// 모든 요소 노드를 탐색하여 반환한다. 
const $all = document.getElementsByTagNameC'*'); 
// → HTMLCoiLection(8) [html, head, body, ul, li#appie, ii#banana, li#orange, script, apple:
li#apple, banana: li#banana, orange: li#orange]
  • Document.prototype에 정의된 getElementsByTagName 메서드
    • DOM의 루트 노드인 문서 노드, 즉 document를 통해 호출하며 DOM 전체에서 요소 노드를 탐색하여 반환한다.
  • Element.prototype에 정의된 getElementsByTagName 메서드
    • 특정 요소 노드를 통해 호출하며, 특정 요소 노드의 자손 노드 중에서 요소 노드를 탐색하여 반환한다.
<!D0CTYPE html> 
<html> 
	<body> 
		<ul id="fruit"> 
			<li>Apple</li> 
			<li>Banana</li> 
			<li>orange</li> 
		</u1> 
		<ul> 
			<li>HTML</ii> 
		</ul> 
		<script> 	
		// DOM 전체에서 태그 이름이 li인 요소 노드를 모두 탐색하여 반환한다. 
		const $lisFromDocument = document.getElementsByTagName('li'); 
		console.log($lisFromDocument); // HTMLCollection(4) [li, li, li, li]
        
		// ul#fruits 요소의 자손 노드 중에서 태그 이름이 li인 요소 노드를 모두 탐색하여 반환한다.
		const $fruits = document.getElementById('fruits'); 
		const $lisFromFruits = $fruits.getElementsByTagName('li');
		console.log($lisFromFruits); // HTMLCollection(3) [li, li, li]
		</script> 
	</body> 
</html>

 

class를 이용한 요소 노드 취득

  • Document.prototype / Element. prototype.getElementsByClassName 메서드는 인수로 전달한 class 어트리뷰트 값(이하 class 값) 을 갖는 모든 요소 노드들을 탐색하여 반환한다.
    • getElementsByClassName 메서드는 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다.
  • Document.prototype에 정의된 getElementsByClassName 메서드
    • DOM의 루트 노드인 문서 노드, 즉 document를 통해 호출하며 DOM 전체에서 요소 노드를 탐색하여 반환한다.
  • Element.prototype에 정의된 getElementsByClassName 메서드
    • 특정 요소 노드를 통해 호출하며, 특정 요소 노드의 자손 노드 중에서 요소 노드를 탐색하여 반환한다.

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

  • css 선택자는 스타일을 적용하고자 하는 HTML 요소를 특정할 때 사용하는 문법이다.
/* 전체 선택자: 모든 요소를 선택 */ 
* { ... } 
/* 태그 선택자: 모든 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 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환한다.
    • 인수로 전달한 CSS 선택자를 만족시키는 요소 노드가 여러 개인 경우 첫 번째 요소 노드만 반환한다.
    • 인수로 전달된 CSS 선택자를 만족시키는 요소 노드가 존재하지 않는 경우 null을 반환한다.
    • 인수로 전달한 CSS 선택자가 문법에 맞지 않는 경우 DOMException 에러가 발생한다.
  • querySelectorAll 메서드는 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 NodeList 객체 (유사 배열 객체이면서 이터러블)를 반환한다
<!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에 정의된 querySelector, querySelectorAll 메서드
    • DOM의 루트 노드인 문서 노드, 즉 document를 통해 호출하며 DOM 전체에서 요소 노드를 탐색하여 반환한다.
  • Element.prototype에 정의된 querySelector, querySelectorAll 메서드
    • 특정 요소 노드를 통해 호출하며, 특정 요소 노드의 자손 노드 중에서 요소 노드를 탐색하여 반환한다.

* id 어트리뷰트가 있는 요소 노드를 취득하는 경우에는 getElementByld 메서드를 사용하고 그 외의 경우에는 querySelector, querySelectorAll 메서드를 사용하는 것을 권장

 

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

  • 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> 
   </u1> 
  </body> 
  <script> 
   const $apple = document.querySetector('.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>

 

HTMLCollection과 NodeList

DOM 컬렉션 객체인 HTMLCollection과 NodeList는 DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체다.
  • 둘 다 유사 배열 객체이면서 이터러블 -> for ... of 문으로 순회 가능, 스프레드 문법을 사용하여 간단히 배열로 변환 가능
  • 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체

HTMLCollection

getElementsByTagName, getElementsByClassName 메서드가 반환하는 HTMLCollection 객체는 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 DOM 컬렉션 객체다.
<!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>

 

* 3개 -> 0개를 예상했지만, 1개가 된 이유?

* 이 문제는 for 문을 역방향으로 순회하는 방법 / while문을 사용하는 방법으로 회피할 수 있다.

// for 문을 역방향으로 순회 
for (let i = $elems.length - 1; i >= 0; i--) { 
	$elems[i].className = 'blue' ; 
}

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

 

* 더 간단한 해결책 : HTMLCollection 객체를 배열로 변환하면 부작용을 발생시키는 HTMLCollection  객체를 사용할 필요가 없고. 유용한 배열의 고차 함수 (forEach, map, filter, reduce 등)를 사용할 수 있다.

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

 

NodeList

querySelectorAll 메서드는 DOM 컬렉션 객체인 NodeList 객체를 반환한다. 이때 NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 non-live 객체다.
// querySelectorAll은 DOM 컬렉션 객체인 NodeList를 반환한다. 
const $elems = document.querySelectorAll('.red'); 

// NodeList 객체는 "NodeList.prototype.forEach 메서드를 상속받아 사용할 수 있다. 
$elems.forEach(elem => elem.className = 'blue');
  • querySelectorAll이 반환하는 NodeList 객체는 NodeList.prototype.forEach 메서드를 상속받아 사용할 수 있다.
  • NodeList.prototype은 forEach 외에도 item, entries, keys, values 메서드를 제공한다.
  • NodeList 객체는 대부분의 경우 노드 객쳬의 상태 변경을 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객쳬로 동작한다. 하지만 childNode 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 같이 실시간으로 노드 객체의 상태 변경을 반영하는 live 객체로 동작하므로 주의가 필요하다.

* live 객체로 동작할 때는 예상과 다르게 동작할 수 있어 실수하기 쉽다. 따라서 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장한다.

  • HTMLCollection과 NodeList 객체는 모두 유사 배열 객쳬이면서 이터러블이므로 스프레드 문법이나 Array.from 메서드를 사용하여 간단히 배열로 변환할 수 있다.
<!DOCTYPE html> 
 <html> 
  <body> 
   <u1 id="fruits"> 
    <li>Apple</li> 
    <li>Banana</li> 
   </ul> 
  </body> 
  <script> 
	const $fruits = document.getElementByld('fruits'); 

 	// childNodes 프로퍼티는 NodeList 객체(live)를 반환한다. 
	const { childNodes } = $fruits; 
    	
	// 스프레드 문법을 시용하여 NodeList 객체를 배열로 변환한다. 
	[... childNodes].forEach(childNode => { 
	 $fruits.removeChild(childNode); 
	}); 

	// $fruits 요소의 모든 자식 노드가 모두 삭제되었다. 
	console.log(childNodes); // NodeList [] 
  </script> 
</html>

 

3. 노드 탐색

취득한 요소 노드를 기점으로 DOM 트리의 노드를 옮겨 다니며 부모, 형제, 자식 노드 등을 탐색하는 방법을 살펴보자!

  • Node, Element 인터페이스는 트리 탐색 프로퍼티를 제공한다.
    • Node.prototype이 제공하는 프로퍼티 : parentNode, previousSibling, firstChild, childNodes 프로퍼티
    • Element.prototype이 제공하는 프로퍼티 : previousElementSibling, nextElenientSibling과 children 프로퍼티
  • 노드 탐색 프로퍼티는 모두 접근자 프로퍼티이다.
  • 노드 탐색 프로퍼티는 setter없이 getter만 존재하여 참조만 가능한 읽기 전용 접근자 프로퍼티이다.

공백 텍스트 노드

텍스트 에디터에서 HTML 문서에 스페이스 키, 탭 키, 엔터 키 등을 입력하면 공백 문자가 추가된다.
<!DOCTYPE html> 
 <html> 
  <body> 
   <ul id="fruits"> 
    <li class="apple">Apple</li>
    <li class="banana">Banana</li> 
    <li class="orange">Orange</li> 
   </ul> 
  </body> 
 </html>

 

'스터디 > [모던 자바스크립트 deep dive] JS' 카테고리의 다른 글

#38. 브라우저의 렌더링 과정  (1) 2024.09.08
#37. Set과 Map  (0) 2024.09.07
#36. 디스트럭처링 할당 (구조 분해 할당)  (0) 2024.09.07
#35. 스프레드 문법  (0) 2024.09.07
#27. 배열  (0) 2024.08.18