● DOM
- DOM(Document Object Model)로 HTML을 Object요소처럼 조작할 수 있는 Model이다. 즉, DOM은 HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 프로퍼티와 메서드를 제공하는 트리 자료구조이다.
HTML요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환된다. 이때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로, HTML 요소의 텍스트 콘텐츠는 텍스트 노드로 변환된다.
위의 예제를 보면, 요소 노드 안에는 텍스트 노드도 있고 어트리뷰트 노드도 있다. 이처럼 노드는 중첩 관계를 갖는다. 이런 중첩 관계에 의해 계층적인 부자 관계(parent-child)가 형성된다.
트리 자료구조는 노드들의 계층구조로 이루어진다. 즉, 트리자료구조는 부모 노드, 자식노드로 구성되어 노드 간의 계층적 구조를 표현하는 비선형 자료구조를 말한다. 트리 자료구조는 하나의 최상의 노드에서 시작한다. 최상위 노드는 부모 노드가 없으며, 루트 노드라 말한다. 루트 노드는 0개 이상의 자식노드를 갖는다. 자식 노드가 없는 노드를 리프 노드라 한다.
● 노드의 객체의 타입
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-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>
렌더링 엔진은 위 코드를 파싱하여 아래와 같은 DOM을 생성한다. HTML요소 사이의 개행이나 공백은 텍스트 노드가 된다. 하지만 아래에서는 생략되어있다.
이처럼 DOM은 노드 객체의 계층적인 구조로 구성된다. 노드 객체는 총 12개의 종류가 있다. 그 중에서도 중요한 노드는 아래와 같다.
- 문서 노드: DOM트리의 최상위에 존재하는 루트 노드로서, document객체를 가르킨다. document 객체는 브라우저가 렌더링한 HTML 문서 전체를 가리키는 객체로서, 전역 객체 window의 document 프로퍼티에 바인딩되어있다. 따라서 window.document 또는 document로 참조할 수 있다. 브라우저 환경의 모든 자바스크립트는 script태그에 의해 분리되어 있어도 하나의 전역 객체 window를 공유한다. 따라서 모든 자바스크립트 코드는 전역 객체 window의 document 프로퍼티에 바인딩되어 있는 하나의 document객체를 바라본다. 즉, HTML 문서당 document 객체는 유일하다.
- 요소 노드 : HTML 요소를 가르키는 객체이다. 요소 노드는 HTML 간의 중첩에 의해 부자관계를 가지며, 이 부자관계를 통해 정보를 구조화한다. 따라서 요소 노드는 문서의 구조를 표현한다고 할 수 있다.
- 어트리뷰트 노드: 어트리뷰트 노드는 HTML의 어트리뷰트를 가리키는 객체이다. 어트리뷰트 노드는 어트리뷰트가 지정된 HTML 요소의 요소 노드와 연결되어 있다. (ex) href 어트리뷰트노드는 a 요소와 연결되어 있다.) 요소 노드는 부모와 연결되어 있지만, 어트리뷰트 노드는 부모 노드와 연결되어 있지 않고, 요소노드에만 연결되어 있다. 즉, ul > ol > ol의 class 가 있다고 해보자. ol 노드요소는 부모 요소인 ul 요소와 연결되어있지만, ol class 어트리뷰트 노드는 부모노드가 아니라 ol인 요소노드에만 연결되어있다. 따라서 어트리뷰트 노드에 접근하려면 요소노드에 먼저 접근하여 어트리뷰트 노드를 참조하거나 변경해야한다.
- 텍스트 노드 : 텍스트 노드는 HTML요소의 텍스트를 가리키는 객체다. 요소 노드가 문서의 구조를 표현한다면, 텍스트 노드는 문서의 정보를 표현한다고 할 수 있다. 텍스트 노드는 요소 노드의 자식 노드이며, 텍스트 노드는 자식을 가질 수 없는 리프 노드 이다. 즉, 텍스트 노드는 DOM트리의 최종단이다. 따라서 텍스트 노드에 접근하려면 먼저 부모 노드인 요소 노드에 접근해야한다.
4가지 노드 타입 이외에도 comment 노드, DocumentType노드, DocumentFragment노드, 등 총 12개가 있다.
● 노드의 객체의 상속 구조
DOM을 구성하는 노드 객체는 표준 빌트인 객체가(String, Number, Boolean, Object, Array...) 아니라 브라우저 환경에서 추가적으로 제공하는 호스트 객체다. 하지만 노드 객체도 자바스크립트의 객체이므로, 프로토타입에 의한 상속구조를 갖는다.
위 그림과 같이 모든 노드 객체는 Object, EventTarget, Node라는 인터페이스를 상속받는다. 추가적으로 문서 노드는 Document, HTMLDocument 인터페이스를 상속받고 요소노드는 Element, 어트리뷰트 노드는 Attr, 텍스트 노드는 CharacterData 인터페이스를 각각 상속받는다. 또한 요소 노드는 HTMLElement, HTMLHeadElemt, HTMLBodyElement.. 등의 인터페이스를 상속받는다.
예를 들어 input 요소를 파싱해서 객체화한 input 요소 노드 객체는 HTMLInputElement, HTMLElement, Element, Node, EventTarget, Object의 prototype에 바인딩되어 있는 프로토타입 객체를 상속받는다.
input 요소 노드 객체의 특성 | 프로토타입을 제공하는 객체 |
객체 | Object |
이벤트를 발생시키는 객체 | EventTarget |
트리 자료구조의 노드 객체 | Node |
브라우저가 렌더링할 수 있는 웹 문서의 요소(HTML, XML, SVG)를 표현하는 객체 | Element |
웹 문서의 요소 중에서 HTML 요소를 표현하는 객체 | HTMLElement |
HTML요소 중에서 input 요소를 표현하는 객체 | HTMLInputElement |
노드 객체에는 노드 객체의 종류, 즉 노드 타입에 상관없이 모든 노드 객체가 공통으로 갖는 기능도 있고, 노드 타입에 따라 고유한 기능도 있다. 예를 들어 모든 객체는 공통적으로 이벤트를 발생시킬 수 있다. 이벤트 관련한 기능 (EventTarget.addEventListener, EventTarget.removeEventListener 등)은 EventTarget 인터페이스가 제공한다. 모든 노드 객체는 트리 자료구조의 노드로서 공통적으로 트리탐색 기능(Node.parentNode, Node.childNodes, Node.previousSibling, Node.nextSibling)등 이나 노드 정보 제공이 필요하다. 이 같은 노드 관련 기능은 Node 인터페이스가 제공한다.
DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공한다. DOM API를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.
● 요소 노드 취득
- id를 이용한 요소 노드 취득
<body>
<div id="foo"></div>
</body>
<script>
const foo = document.getElementById('foo')
</script>
id가 같은 HTML요소가 여러개 있다고 해도, 에러가 발생하지 않는다. 하지만 인수로 전될된 Id 값을 갖는 첫번째 요소 노드만 반환한다. 즉, getElementById는 언제나 단 하나의 요소 노드를 반환한다. 값이 없으면 null로 반환한다.
또한, HTML요소에 id 어트리뷰트를 부여하면 id값과 동일한 이름의 전역변수가 암묵적으로 선언되고 해당 노드 객체가 할당되는 부수효과가 있다.
<body>
<div id="foo"></div>
</body>
<script>
console.log(foo)
// <div id="foo></div> 가 출력됨 ㄷ ㄷ
</script>
근데 기존에 foo가 선언된적 있으면 전역 변수에 노드객체가 할당되지 않는다.
<body>
<div id="foo"></div>
</body>
<script>
let foo = 1;
console.log(foo)
// 1 출력됨
</script>
- 태그 이름을 이용한 요소 노드 취득
Document.prototype/Element.prototype.getElementsByTagName 메서드를 사용한다.
복수형이므로 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다. 이 객체는 유사 배열 객체이면서 이터러블이다. 위에서 말한 것처럼 getElementsByTagName 메서드는 2가지로 나뉜다.
- Document.prototype.getElementsByTagName
- Element.prototype.getElementsByTagName
이 둘의 차이는 DOM 전체에서 요소 노드를 탐색하여 반환하느냐, 특정 요소 노드의 자손 중에서 요소 노드를 탐색하여 반환하느냐의 차이이다. 즉, document.getElementsByTagName은 문서 전체에 있는 요소들을 가져오고, Element.getElementsByTagName은 특정 요소 노드 안에 있는 자손 노드중에서 요소노드를 탐색하여 반환한다.
// DOM 전체에서 탐색
const $lisFromDocument = documents.getElementsByTagName('li');
// 특정 요소 노드 자손 중에서 요소 노드 탐색
const $fruits = documnet.getElementById('fruits');
const $lisFromFruits = $fruits.getElementsByTagName('li');
- class를 이용한 요소 노드 취득
Document.prototype / Element.prototype.getElementsByClassName 메서드를 사용
--> getElementsByTagName 메서드와 마찬가지로 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다. 만약 인수로 전달된 class 값을 갖는 요소가 존재하지 않는 경우 빈 HTMLCollection 객체를 반환한다.
- CSS 선택자를 이용한 요소 노드 취득 (querySelector, querySelectorAll)
<!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.querySelector 메서드는 인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환한다.
<!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>
Document.prototype/Element.prototype.querySelectorAll 메서드는 인수로 전달한 CSS 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환한다. querySelectorAll 메서드는 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 NodeList 객체를 반환한다. NodeList 객체는 유사 배열 객체이면서 이터러블이다.
querySelector, querySelectorAll 메서드는 getElementById, getElementsBy*** 메서드보다 다소 느린 것으로 알려져 있다. 하지만 CSS 선택자 문법을 사용하여 좀 더 구체적인 조건으로 요소를 취득할 수 있고, 일관된 방식으로 요소 노드를 취득할 수 있다는 장점이 있다. 따라서 id 어트리뷰트가 있는 요소 노드를 취득하는 경우 getElementById를 쓰고 이외의 경우에는 querySelector, querySelectorAll을 사용하는 것을 권장한다.
- HTMLCollection과 NodeList
DOM 컬렉션 객체인 HTMLCollection과 NodeList는 DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체이다. HTMLCollection과 NodeList는 모두 유사 배열 객체이면서 이터러블이다. 따라서 for...of 문으로 순회할 수 있으며 스프레드 문법을 사용하여 간단히 배열로 변환할 수 있다.
HTMLCollection과 NodeList의 중요한 특징은 노드 객체의 상태 변화를 실시간으로 반영하여 살아 있는 객체라는 것이다. HTMLCollection은 언제나 live 객체로 동작한다. NodeList는 특정 경우에만 live 객체로 동작한다.
다음 예제를 통해 살아있는 객체가 무엇인지 보자
<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>
위 예제는 HTMLCollection 객체를 for 문으로 순회화며 class 값을 'red'에서 'blue'로 변경했다.하지만 두 번째 요소는 바뀌지 않았다. 이유는 다음과 같다. $elems.length는 3이므로 3번 반복된다.
- 첫 번째 반복 (i==0)
$elems[0] li의 첫 번째 요소가 'blue'로 변경된다. 이때 첫 번째 li 요소가 변경되었으므로 getElementsByClassName 메서드의 인자로 전달한 'red'와는 더는 일치하지 않기 때문에 HTMLCollection 객체의 $elems에서 실시간으로 제거된다. - 두 번째 반복 (i==1)
첫 번째 반복에서 li 요소가 삭제되었으므로 뒤에 남아 있던 리스트들은 한 칸씩 앞으로 밀리게 된다. 따라서
$elems[0]은 두 번째 li 요소가 되고 $elems[1]은 세 번째 il요소가 된다. 즉 세 번째 li의 값이 바뀌고 또한 제거된다. - 세 번째 반복 (i==2)첫 번째, 세번째 li 요소가 제거되고 $elems에는 두 번째 li 태그만 남아있게 되어 $elem.length가 1이 된다. 조건식에 맞지 않으므로 반복이 종료된다.
따라서 for문을 역방향으로 순회하는 방법으로 회피할 수 있다. 아니면 while문을 사용해서 객체가 남아있지 않을 때까지 무한반복하는 방법으로 회피할 수도 있다.
for (let i = $elem.length-1; i >= 0; i--){
$elem[i].className = 'blue';
}
// while 문으로 HTMLCollection에 요소가 남아 있지 않을 때까지 무한 반복
let i = 0;
while ($elems.length > i) {
$elems[i].className = 'blue';
}
아니면 배열의 고차함수를 사용할 수 있다. 이방식이 난 더조은듯..
// 유사 배열 객체이면서 이터러블인 HTMLCollection을 배열로 변환하여 순회
[...$elems].forEach(elem => elem.className = 'blue');
또한 querySelectorAll이 반환하는 NideList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체로 querySelectorAll을 활용하여 해결할 수도 있다.
하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 동일하게 실시간 객체로 동작하므로 주의가 필요하다.
<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에서 삭제한다.
// removeChild 메서드가 호출될 때마다 NodeList 객체인 childNodes가 실시간으로 변경된다.
// 따라서 첫 번째, 세 번째 다섯 번째 요소만 삭제된다.
$fruits.removeChild(childNodes[i]);
}
// 예상과 다르게 $fruits 요소의 모든 자식 노드가 삭제되지 않는다.
console.log(childNodes); // NodeList(2) [li, li]
</script>
</html>
따라서 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장한다. 또한 고차 함수를 사용하여 순회한다.