해당 포스팅에서는 DOM의 정의와 DOM을 동적으로 수정할 수 있는 DOM API에 대해 설명한다.
텍스트 파일로 만들어져 있는 웹 문서를 브라우저에 렌더링하려면 웹 문서를 브라우저가 이해할 수 있는 구조로 메모리에 올려야 한다. 브라우저의 렌더링 엔진은 HTML 문서를 로드한 후, 파싱하여 웹 문서를 브라우저가 이해할 수 있는 자료 구조인 DOM을 생성해 메모리에 적재한다.
DOM(Document Object Model)은 HTML 문서의 계층적 구조와 정보뿐만 아니라 HTML의 요소와 스타일링을 변경할 수 있는 프로그래밍 인터페이스로서 DOM API를 제공한다. 따라서 DOM API를 사용해 이미 생성된 DOM을 동적으로 조작할 수 있으며 변경된 DOM은 렌더링에 반영된다.
HTML 요소는 HTML 문서를 구성하는 개별적인 요소를 의미한다.
- div: tag
- class: attribute name
- greeting: attribute value
- Hello: contents
<div class="greeting">Hello/<div>
HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환된다. 이 때 HTML 요소의 attribute는 attribute node로, HTML 요소의 텍스트 콘텐츠는 text node로 변환된다.
아래의 HTML 문서를 렌더링 엔진이 파싱한다고 가정하자.
<!DOCTYPE html>
<html>
<head>
<style>
.red { color: #ff0000; }
.blue { color: #0000ff; }
</style>
</head>
<body>
<div>
<h1>Cities</h1>
<ul>
<li id="one" class="red">Seoul</li>
<li id="two" class="red">London</li>
<li id="three" class="red">Newyork</li>
<li id="four">Tokyo</li>
</ul>
</div>
</body>
</html>
렌더링 엔진의 파싱결과로 아래의 DOM이 생성된다. DOM은 노드 객체의 트리로 계층적 구조화되어 있기 때문에 DOM 트리라고 불린다. 아래 사진은 DOM 트리 예시이다.
DOM에서 모든 요소, 어트리뷰트, 텍스트는 하나의 객체이며 Document 객체의 자식이다. 요소의 상속관계는 객체의 트리로 구조화하여 부자관계를 표현한다. 노드 객체는 총 12 종류로, 가장 중요한 노드 타입은 다음 4가지다.
트리의 최상위에 존재하며 각각 요소, 어트리뷰트, 텍스트 노드에 접근하려면 문서 노드를 통해야 한다. 즉, DOM tree에 접근하기 위한 시작점(entry point)이다.
요소 노드는 HTML 요소를 표현한다. HTML 요소는 중첩에 의해 부자 관계를 가지며 이 부자 관계를 통해 정보를 구조화한다. 따라서 요소 노드는 문서의 구조를 서술한다고 말 할 수 있다. 어트리뷰트, 텍스트 노드에 접근하려면 먼저 요소 노드를 찾아 접근해야 한다. 모든 요소 노드는 요소별 특성을 표현하기 위해 HTMLElement 객체를 상속한 객체로 구성된다.
어트리뷰트 노드는 HTML 요소의 어트리뷰트를 표현한다. 어트리뷰트 노드는 해당 어트리뷰트가 지정된 요소의 자식이 아니라 해당 요소의 일부로 표현된다. 따라서 해당 요소 노드를 찾아 접근하면 어트리뷰트를 참조, 수정할 수 있다.
텍스트 노드는 HTML 요소의 텍스트를 표현한다. 텍스트 노드는 요소 노드의 자식이며 자신의 자식 노드를 가질 수 없다. 즉, 텍스트 노드는 DOM tree의 최종단이다.
DOM을 구성하는 노드 객체는 브라우저 환경에서 추가적으로 제공하는 호스트 객체이다. 하지만 노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 갖는다. 노드 객체의 상속 구조는 아래와 같다.
이러한 상속 구조로 각 노드 객체는 프로토타입 체인에 있는 모든 프로토타입의 DOM API를 상속받아 사용할 수 있다.
정리하자면 DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공한다. 이 DOM API를 통해 HTML의 구조나 내용 또는 스타일을 동적으로 조작할 수 있다.
Document.prototype.getElementById(id)
// id로 하나의 요소를 선택한다.
const elem = document.getElementById('one');
// 클래스 어트리뷰트의 값을 변경한다.
elem.className = 'blue';
Document.prototype/Element.prototype.querySelector(cssSelector)
// CSS 셀렉터를 이용해 요소를 선택한다
const elem = document.querySelector('li.red');
// 클래스 어트리뷰트의 값을 변경한다.
elem.className = 'blue';
Document.prototype/Element.prototype.getElementByTagName(tag)
// HTMLCollection을 반환한다.
const elems = document.getElementsByTagName('li');
Document.prototype/Element.prototype.getElementByClassName(class)
// HTMLCollection을 반환한다. HTMLCollection은 live하다.
const elems = document.getElementsByClassName('red');
Document.prototype/Element.prototype.querySelectorAll(cssSelector)
// querySelectorAll는 Nodelist(non-live)를 반환한다. IE8+
const elems = document.querySelectorAll('.red');
DOM 컬렉션 객체인 HTMLCollection과 NodeList는 DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체이다. HTMLCollection과 NodeList는 모두 유사배열 객체이면서 이터러블이다.
이 둘의 차이점은 노드 객체의 상태 변화를 실시간으로 반영할 수 있는지 여부이다. HTMLCollection의 경우 언제나 live 객체로 노드 객체의 상태 변화를 실시간으로 반영한다. 반면 NodeList는 대부분의 경우 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-liv 객체로 동작한다. 하지만 childNodes 프로퍼티가 반환하는 NodeList는 live 객체로 동작한다.
live로 노드 객체의 변화를 실시간으로 변화한다면, 컬렉션의 결과값을 for문으로 변화시킬 때 index 문제가 발생한다. NodeList는 기본적으로 non-live이지만 앞서 말했듯이 live 객체로 동작할 때가 있다.
따라서 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection과 NodeList를 배열로 변환하여 고차함수를 이용해 DOM을 수정하는 것을 권장한다.
Node, Element 인터페이스는 DOM 트리 상의 노드를 탐색할 수 있도록 트리 탐색 프로퍼티를 제공한다.
노드 탐색 프로퍼티는 getter만 존재하는 읽기 전용 접근자 프로퍼티이다. 따라서 프로퍼티에 값을 할당 시 에러없이 무시된다.
Node.prototype.childNodes
Element.prototype.children
Node.prototype.firstChild / lastChild
Node.prototype.firstElementChild / lastElementChild
Node.prototype.hasChildNodes
Element.prototype.children.length / Element.prototype.childrenElementCount
Node.prototype.parentNode
Node.prototype.previousSibling, nextSibling
Node.prototype.previousElementSibling, nextElementSibling
nodeValue
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello</div>
</body>
<script>
// 1. 텍스트를 변경할 요소 노드를 취득한다
const $foo = document.getElementById('foo');
console.log($foo.nodeValue); // null
// 2. firstChild 프로퍼티를 사용해 텍스트 노드를 탐색한다.
const $textNode = $foo.fisrtChild;
// 3. 탐색한 텍스트 노드의 값을 nodeValue 프로퍼티를 사용해 참조한다.
console.log($textNode.nodeValue); // Hello
// 4. 텍스트 노드의 값을 nodeValue 프로퍼티를 사용해 텍스트 노드의 값을 변경한다.
$textNode.nodeValue = 'World';
console.log($textNode.nodeValue); // World
</script>
</html>
textContent
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>World!</span></div>
</body>
<script>
// #foo 요소 노드의 텍스트를 모두 취득한다. 이 때 HTML 마크업은 무시된다.
console.log(document.getElementById('foo').textContent); // Hello World!
// #foo 요소 노드의 모든 자식 노드가 제거되고 할당된 문자열이 텍스트로 추가된다.
// 이 때 HTML 마크업이 파싱되지 않는다.
document.getElementById('foo').textContent = `Hi <span>there!</span>`;
console.log(document.getElementById('foo').textContent); // Hi <span>there!</span>
</script>
</html>
HTML 문서의 구성 요소인 HTML 요소는 여러 개의 어트리뷰트를 가질 수 있다.
HTML 요소의 동작을 제어하기 위한 추가적인 정보를 제공하는 HTML 어트리뷰트는 HTML 요소의 시작 태그에 정의한다.
<input id="user" type="text" value="ungmo2">
HTML 문서가 파싱될 때 HTML 요소의 어트리뷰트는 어트리뷰트 노드에 변환되어 요소 노드와 연결된다. 이 때 HTML 어트리뷰트당 하나의 어트리뷰트가 생성된다.
모든 어트리뷰트 노드의 참조는 유사배열 객체이자 이터러블인 NamedNodeMap 객체에 담겨 요소 노드의 attributes 프로퍼티에 저장된다.
Element.prototype.getAttribute/setAttribute(attributeName)
메서드를 이용하면 요소 노드에서 메서드를 통해 직접 HTML 어트리뷰트 값을 취득하거나 변경할 수 있다.
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<input id="user" type="text" value="ungmo2">My First CSS Example</input>
<script>
const $input = document.getElementById('user');
const inputValue = $input.getAttribute('value');
console.log(inputValue); // ungmo2
$input.setAttribute('value', 'foo');
console.log($input.getAttribute('value')); // foo
</script>
</body>
</html>
특정 어트리뷰트가 존재하는지 확인하려면 Element.prototype.hasAttribute(attributeName)
메서드를 이용하고, 특정 어트리뷰트를 삭제하려면 Element.prototype.removeAttribute(attributeName)
메서드를 이용한다.
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<input id="user" type="text" value="ungmo2">My First CSS Example</input>
<script>
const $input = document.getElementById('user');
// value 어트리뷰트 존재 확인
if ($input.hasAttribute('value')) {
// value 어트리뷰트 삭제
$input.removeAttribute('value');
}
console.log($input.hasAttribute('value')); // false
</script>
</body>
</html>
HTML 어트리뷰트는 HTML 요소의 초기 상태를 지정한다. HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다. 이 때 HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태는 어트리뷰트 노드에서 관리한다.
반면 요소 노드는 상태를 가지고 있으며, 사용자가 DOM을 조작할 시 상태가 변경된다.
즉 요소 노드는 2개의 상태(초기 상태, 최신 상태)를 관리해야 한다. 요소 노드의 초기 상태는 어트리뷰트 노드가 관리하며, 요소 노드의 최신 상태는 DOM 프로퍼티가 관리한다.
data 어트리뷰트와 dataset 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있다.
data 어트리뷰트는 data-접두사 다음에 임의의 이름을 붙여 사용한다. (ex. data-user-id, data-role)
data 어트리뷰트 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있다. dataset 프로퍼티는 HTML 요소의 모든 data 어트리뷰트 정보를 제공하는 DOMStringMap 객체를 반환한다.
DOMStringMap 객체는 data 어트리뷰트의 data-접두사 다음에 붙인 임의의 이름을 카멜 케이스로 변환한 프로퍼티를 가지고 있다. 이 프로퍼티로 data 어트리뷰트의 값을 취득하거나 변경할 수 있다.
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<ul class="users">
<li id="1" data-user-id="1" data-role="admin">Lee</li>
<li id="2" data-user-id="2" data-role="subscriber">Kim</li>
</ul>
<script>
const users = [...document.querySelector('.users').children];
const user = users.find(user => user.dataset.userId === '1');
console.log(user.dataset.role); // "admin"
user.dataset.role = "subscriber";
console.log(user.dataset); // [object DOMStringMap] { role: "subscriber",userId: "1"}
</script>
</body>
</html>
const four = document.getElementById('four');
// inline 스타일 선언을 생성
four.style.color = 'blue';
// font-size와 같이 '-'으로 구분되는 프로퍼티는 카멜케이스로 변환하여 사용한다.
// 만약 CSS 프로퍼티 이름을 그대로 사용하려면 대괄호 표기법을 이용한다.
four.style.fontSize = '2em';
// four.style['font-size'] = '2em';
Element.prototype.className 프로퍼티는 setter와 getter가 모두 존재하는 접근자 프로퍼티로, HTML 요소의 class 어트리뷰트 값을 취득하거나 변경한다.
className 프로퍼티를 참조하면 class 어트리뷰트의 값을 문자열로 변환
요소 노드의 className 프로퍼티에 문자열을 할당하면 class 어트리뷰트 값을 할당한 문자열로 변경
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px;
height: 100px;
background-color: antiquewhite;
}
.red {color: red;}
.blue {color: blue;}
</style>
</head>
<body>
<div class="box red">Hello World</div>
<script>
const $box = document.querySelector('.box');
// .box 요소의 class 어트리뷰트 값 취득
console.log($box.className); // "box red"
$box.className = $box.className.replace('red', 'blue');
console.log($box.className); // "box blue"
</script>
</body>
</html>
Element.prototype.classList 프로퍼티는 HTML 요소의 class 어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px;
height: 100px;
background-color: antiquewhite;
}
.red {color: red;}
.blue {color: blue;}
</style>
</head>
<body>
<div class="box red">Hello World</div>
<script>
const $box = document.querySelector('.box');
// .box 요소의 class 어트리뷰트 정보를 담은 DOMTokenList 객체 취득
// DOMTokenList는 노드 객체의 상태 변화를 실시간으로 반영하는 live 객체다.
console.log($box.classList);
// .box 요소의 class 어트리뷰트 값 중에서 red만 blue로 변경
$box.classList.replace('red', 'blue');
console.log($box.classList);
</script>
</body>
</html>
DOMTokenList 객체 메서드
add(...className)
add 메서드는 인수로 전달한 1개 이상의 문자열을 class 어트리뷰트 값으로 추가한다.
remove(...className)
remove 메서드는 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 class 어트리뷰트에서 삭제한다. 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 없으면 에러없이 무시된다.
item(index)
item 메서드는 인수로 전달한 index에 해당하는 클래스를 class 어트리뷰트에서 반환한다.
contains(className)
contains 메서드는 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 포함되어 있는지 확인한다.
replace(oldClassName, newClassName)
replace 메서드는 class 어트리뷰트에서 첫 번째 인수로 전달한 문자열을 두 번째 인수로 전달한 문자열로 변경한다.
toggle(classNmae[, force])
- toggle 메서드는 class 어트리뷰트에 첫 번째 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거하고, 존재하지 않으면 추가한다.
- 두 번재 인수로 불리언 값으로 평가되는 조건식을 전달할 수 있다. 조건식이 true일 시 class 어트리뷰트에 첫 번째 인수로 전달한 문자열을 추가하고, false일 시 제거한다.
style 프로퍼티는 인라인 스타일만을 반환하므로, HTML 요소에 적용되어 있는 모든 CSS 스타일을 참조해야 할 경우 getComputedStyle 메서드를 사용한다.
window.getComputedStyle(element, [, pseudo]) 메서드는 첫 번째 인수(element)로 전달한 요소 노드에 적용되어 있는 평가된 스타일을 CSSStyleDeclaration 객체에 담아 반환한다.
두 번째 인수(pseudo)로 :after, :before와 같은 의사 요소를 지정하는 문자열을 전달할 수 있다. 이때 의사 요소가 아닌 일반 요소를 지정할 시 두 번째 인수는 생략된다.
<!DOCTYPE html>
<html>
<head>
<style>
.box:before {
content: 'Hello';
}
</style>
</head>
<body>
<div class="box">Box</div>
<script>
const $box = document.querySelector('.box');
const computedStyle = window.getComputedStyle($box, ':before');
console.log(computedStyle.content); // Hello
</script>
</body>
</html>
(docs) 모던 자바스크립트 딥다이브 39강 DOM