업데이트:

앞선 장에서 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다고 배웠다. 이번 장에서는 DOM을 자세히 알아볼 예정이다 !

[01] 노드

1) HTML 요소와 노드 객체

Untitled

Untitled

위 HTML 요소들은 요소 노드, 어트리뷰트 노드, 텍스트 노드로 각각 분류된다.

그리고 HTML 요소는 서로 중첩 관계를 가질 수 있다. 이 모든 HTML 요소를 객체화한 모든 노드 객체들을 트리 자료 구조로 구성한다.

트리 자료구조

Untitled

노드 객체들로 구성된 트리 자료구조를 DOM이라 한다. 그래서 DOM 트리라고 부르기도 한다.

2) 노드 객체의 타입

<!DOCTYPE html>
<html>
  <head>
		<meta charset = "URF-8">
		<link rel = "stylesheet" href="style.css">
	</head>
  <body>
		<ul>
			<li id="apple">Apple</li>
		  <li id="banana">Banana</li>
			<li id="orange">Orange</li>
		</ul>
		<script src="app.js"></script>
	</body>
</html>

Untitled

위 HTML 문서는 그림처럼 DOM이 생성된다.

그림에서 보면 노드객체는 종류도 있고 상속 구조도 갖는다.

노드의 12가지 타입 중 가장 중요한 4가지를 보자.

문서 노드

가장 최상위에 존재하는 루트 노드이고 document 객체를 가리킨다.

그래서 window.document로 참조가 가능하다.

자바스크립트 코드를 작성할 때 DOM 트리의 노드들에 접근하려면 진입점 역할인 document 객체를 꼭 거쳐야한다.

요소 노드

HTML 요소를 가리키는 객체 (<div> <head> 등등)

HTML 요소 간의 중첩에 의해 부자 관계를 가지고 문서의 구조를 표현한다.

어트리뷰트 노드

말 그대로 HTML 요소의 속성들을 가리키는 객체이다. ( id, rel, href, src, class 등등)

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

그래서 어트리뷰트 노드에 접근하려면 먼저 요소 노드에 접근해야 한다.

텍스트 노드

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

요소 노드가 문서의 구조를 표현한다고 했는데 텍스트 노드는 문서의 정보를 표현한다.

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

텍스트 노드에 접근하고 싶으면 먼저 부모 노드인 요소 노드에 접근해야 한다.

3) 노드 객체의 상속 구조

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

프로토타입 관점으로 보면 div 요소 노드 객체는 위 Object, EventTarget, Node, Element, HTMLElement를 상속받으므로 프로토타입 체인에 있는 모든 프로퍼티나 메서드를 상속받아 사용할 수 있다.

Untitled

특정한 노드 객체의 상속 구조를 알고 싶을 때는 개발자 도구 Elements > Properties 패널에서 보면 된다.

그렇다면 EventTarget, Node, Element 노드 객체의 기능은 무엇이 있을까?

먼저 EventTarget 객체는 이벤트에 관련된 기능을 제공한다. (addEventListener)

Node 객체는 트리 탐색 기능(parentNode, chiledNodes)이나 노드 정보 제공 기능을 제공한다.

HTMLElement 객체는 style 프로퍼티처럼 HTML 요소가 갖는 공통적인 기능을 제공한다.

요소 노드 객체 종류에 따라 기능이 다른데 이것들은 각자 HTMLxxxElment 에서 제공받는다.


지금까지 DOM 트리가 어떤 노드 객체로 상속관계가 구성 되어있고 각 노드 당 어떠한 기능을 하는지 알아보았다.

이렇게 DOM은 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API를 제공한다.

자바스크립트에서 동적으로 HTML을 조작할 수 있었던 이유도 이 DOM API 때문이다.

✍ 상속 관계를 아는 것도 중요하지만 더 중요한 것은 이 DOM API를 잘 활용하여 HTML을 동적으로 변경하는 방법을 익히는 것이다 !!

[02] 요소 노드 취득

자바스크립트에서 html을 동적으로 변경하려면 먼저 html의 요소 노드를 취득해야한다.

요소 노드를 취득하는 방법들은 다음과 같다.

  1. id 를 이용 : getElementById
    • id 어트리뷰트 값으로 요소 노드 한개를 선택, 복수개가 선택되면 첫번째 요소만 반환
    • 반환값 : HTMLElement를 상속받은 객체
  2. 태그 이름을 이용 : getElementsByTagName
  3. class 를 이용 : getElementByClassName
  4. CSS 선택자를 이용 : querySelector
    • css 셀렉터를 사용하여 요소 노드 한개를 선택
    • 반환값 : HTMLElement를 상속받은 객체

[getElementById와 querySelector, 어느 것을 사용할까? bobbohee](https://bobbohee.github.io/2021-02-12/getelementbyid-versus-queryselector)

여기서 궁금증이 생겼다. getElementById와 querySelector 중 어느 것을 사용하는 게 더 바람직할까?

✍ 상황에 따라 다르겠지만 우선 성능은 getElementById가 더 빠르다고 한다. 하지만 querySelector는 다양한 선택자를 사용하여 요소 객체를 가져올 수 있다는 장점이 있어 getElementById보다 나은 선택이 되는 경우가 많은 것 같다. 결론적으로, 주어진 상황에 따라 둘을 알맞게 사용하면 된다!


HTMLCollection과 NodeList

HTMLCollectionNodeList는 DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체이다. 둘 다 유사 배열 객체면서 이터러블이므로 for…of문과 스프레드 문법을 사용할 수 있다.

HTMLCollection은 getElementsByTagName, getElementByClassName 메서드가 반환하는 객체이다.

NodeList는 querySelecotr 메서드가 반환하는 객체이다.

<!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>
			const $elems = document.getElementsByClassNmae('red');
			console.log($elems); *// HTMLCollection(3) [li.red, li.red, li.red]*

			const $elems2 = document.querySelectorAll('.red');
			console.log($elems2); // *NodeList(3)* 
		</script>
	</body>
</html>

class 값이 red인 요소 노드를 모두 취득하고 이 정보를 담은 HTMLCollection 객체와 NodeList를 출력했다.

만약 이 객체를 순회하고 싶다면 배열로 변환하여 고차함수(forEach, map, filter, reduce)를 사용하는 것이 좋다.

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

[03] 노드 탐색

노드 탐색은 취득한 요소노드를 기점으로 DOM 트리의 부모, 형제, 자식 노드를 옮겨다니며 탐색하는 것을 말한다.

Untitled

위 그림에서 Node.prototypeElement.prototype이 제공하는 각각의 프로퍼티를 나타내었다.

모든 내용을 지금 정리하기보다는 노드를 탐색해야 하는 일이 있다면 이 챕터를 다시 돌아와서 공부하는 편이 좋을 것 같다 !

[04] 노드 정보 취득

  • Node.prototype.nodeType : 노드 객체가 요소, 텍스트, 문서 노드 타입 중 무엇인지 반환
  • Node.prototype.nodeName : 노드의 이름을 문자열로 반환

[05] 요소 노드의 텍스트 조작

노드 탐색, 노드 정보 프로퍼티는 모두 읽기 전용 접근자 프로퍼티였다.

nodeValue

nodeValue는 setter와 getter 모두 존재하는 접근자 프로퍼티라서 이 프로퍼티를 사용하면 참조와 할당이 모두 가능하다 !

<!DOCTYPE html>
<html>
	<body>
		<div id="foo">Hello</div>
	</body>
</html>
console.log(document.nodeValue); // null
const $foo = document.getElementById('foo').firstChild;		
console.log($textNode.nodeValue); // hello

$foo.nodeValue = 'world';
console.log($textNode.nodeValue); // world

nodeValue 프로퍼티를 참조하면 텍스트를 반환한다.

nodeValue 프로퍼티에 값을 할당하면 텍스트를 변경할 수 있다.

textContent

위에서 본 nodeValue는 사실 텍스트 노드만 출력하기 때문에 부모에서 자식 요소로 탐색하는 과정을 거쳐야 하는 번거로움이 있다.

그러므로 textContent를 사용하는 것이 더 간편하다.

만약 텍스트의 자식 노드가 있을 경우 HTML 마크업이 파싱되지 않고 그대로 출력된다.

console.log($foo.textContent === $foo.firstChild.nodeValue); // true

👩🏻‍🏫 추가로, 비슷한 동작을 하는 것 중에 innerText 프로퍼티도 있지만 css 순종적이므로 느리고 부작용이 발생할 수 있어 사용하지 않는 것이 좋다고 한다.

[06] DOM 조작

DOM 조작은 새로운 노드를 생성하여 DOM에 추가되거나 기존 노드를 삭제하는 것을 말한다.

이렇게 DOM 조작이 일어나면 리플로우와 리페인트가 발생하여 렌더링 성능에 영향을 주기도 한다.

1) innerHTML

먼저, HTML문서에 동적으로 어떠한 내용을 집어넣거나 변경하거나 삭제하고 싶을 때, innerHTML을 사용할 수 있다.

<!DOCTYPE html>
<html>
	<body>
		<ul id="fruits">
			<li class="apple">Apple</li>
		</ul>
	</body>
	<script>
		const $fruits = document.getElementById('fruits');
		$fruits.innerHTML += '<li class="banana">Banana</li>'; // 노드 추가(+=)
		$fruits.innerHTML = '<li class="banana">Banana</li>'; // 노드 교체(=)
		$fruits.innerHTML = ""; // 노드 삭제(="")
</html>

위와 같이 innerHTML 프로퍼티를 활용하면 간단하고 직관적으로 코드를 구현할 수 있다.

하지만 innerHTML에는 치명적 단점들이 있다.

첫째는, 크로스 사이트 스크립팅 공격에 취약하다는 것이다.

둘째는, innerHTML이 동작할 때 내부적으로 기존 자식 노드까지 모두 제거하고 다시 처음부터 자식 노드를 생성하여 DOM에 반환하기 때문에 효율적이지 못하고 느리다는 것이다.

마지막으로, innerHTML 프로퍼티로 새로운 요소를 삽입할 때 위치를 지정할 수 없다.

이렇게나 단점이 많기 때문에 웬만하면 사용하지 않는 것이 좋다고 한다.

2) insertAdjacentHTML 메서드

insertAdjacentHTML 메서드는 innerHTML과 비슷한 기능을 하지만 성능이 월등이 우수한 메서드이다.

기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.

const one = document.getElementById('one');

// 마크업이 포함된 요소 추가
one.insertAdjacentHTML('beforeend', '<em class="blue">, Korea</em>');

afterbegin, beforbegin, beforeend, afterend를 이용하여 원하는 위치에 노드를 삽입할 수 있다.

<!DOCTYPE html>
<html>
	<body>
		<!-- beforebegin -->
		<div id="foo">
			<!-- afterbegin -->
			text
			<!--beforeend-->
		</div>
		<!--afterend-->
		</body>

3) createElement / createtTextNode / appendChild

이 세가지 메서드를 조합하여 문서에 노드를 추가할 수 있다.

먼저 createElement로 요소 노드를 생성하고, createTextNode로 텍스트 노드를 생성한다.

appndChild를 통해 이 텍스트 노드를 요소 노드에 담고 마지막으로 원래 DOM에 추가하려는 부분의 요소 노드에 담아주면 된다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
    </ul>
	</body>
   <script>
      const $fruits = document.getElementById('fruits');
		
			// 1. li 요소 노드 생성
      const $li = document.createElement('li'); 

			// 2. 텍스트 노드 Banana 생성
      const textNode = document.createTextNode('Banana'); 

			// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
      $li.appendChild(textNode);

			// 4. fruits 요소 노드의 마지막 자식으로 $li 요소 노드를 추가
      $fruits.appendChild($li); 
   </script>
</html>

Untitled

리플로우와 리페인트는 마지막에 요소 노드를 DOM에 추가할 때 한 번 일어나므로 innerHTML 보다는 훨씬 빠르다.

4) DocumentFragment

복수의 노드를 추가할 때 DocumentFragment라는 객체를 사용하면 여러 리스트를 담을 부모 요소를 따로 만들지 않고 효율적으로 사용이 가능하다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits"></ul>
  </body>
    <script>
      const $fruits = document.getElementById('fruits');
		
			// 1. li 요소 노드 생성'
      const $fragment = document.createDocumentFragment();

      ['apple', 'banana', 'orange'].forEach(text=> {
        const $li = document.createElement('li');
 
        const textNode = document.createTextNode(text); 

        $li.appendChild(textNode);

        $fragment.appendChild($li)
      })
      
      $fruits.appendChild($fragment); 
    </script>
  
</html>

Untitled

비어있는 노드 DocumentFragment에다가 복수의 요소 노드들을 추가한 다음, DocumentFragment를 기존 DOM에 추가한다. 이때 빈객체는 사라지고 HTML 문서에 내가 원하는 요소들만 딱 집어넣을 수 있다.

여러 개의 요소 노드를 DOM에 추가하는 경우 DocumentFragment사용을 지향하자 !

5) 노드 삽입/이동/복사/교체

마지막 위치에 삽입

appendChild는 항상 마지막 자식에 추가된다.

지정한 위치에 노드 삽입

  • insertBefore(newNode, childNode)

두 번째 인수인 childNode 앞에 첫 번째 인수로 전달받은 노드를 원하는 위치에 삽입할 수 있다.

노드 이동

appendchildinsertBefore 이용하여 DOM에 다시 추가하면 노드를 원하는 위치로 이동시킬 수 있다.

노드 복사

  • Node.prototype.cloneNode([deep: true | false])

true를 인수로 전달하면 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성하고, false를 인수로 전달하면 얕은 복사하여 노드 자신만의 사본을 생성한다.

노드 교체

  • Node.prototype.replaceChild(newChild, oldChild)
const $fruits = document.getElementById('fruits');
const $newChild = document.createElement('li');
$newChild.textContent = 'Banana';

$fruits.replaceChild($newChild, $fruits.firstElementChild);

첫번째 인수가 두번째 인수 자리에 들어가 교체가 된다.

노드 삭제

  • Node.prototype.removeChild(child)
const $fruits = document.getElementById('fruits');

$fruits.removeChild($fruits.lastElementChild); // fruits 요소 노드의 마지막 요소를 DOM에서 삭제

댓글남기기