이 글은 '이웅모'님의 '모던 자바스크립트 Deep Dive' 책을 통해 공부한 내용을 정리한 글입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.
브라우저의 렌더링 엔진은 웹 문서를 로드한 후, 파싱하여 웹 문서를 브라우저가 이해할 수 있는 구조로 구성하여 메모리에 적재하는데 이를 DOM이라 한다.
즉, 모든 요소와 요소의 어트리뷰트, 텍스트를 각각의 객체로 만들고 이 객체들간 관계를 표현할 수 있는 트리 구조로 구성한 것이 DOM이다. 자바스크립트로 DOM을 동적으로 제어할 수 있으며 변경된 DOM은 렌더링에 반영된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.active {
color: red;
}
.non-active {
color: blue;
}
</style>
</head>
<body>
<h1>Tree</h1>
<section>
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
</section>
</body>
</html>
위 코드를 DOM Tree 그림으로 나타내면 다음과 같다.
DOM에서 모든 요소, 어트리뷰트, 텍스트는 하나의 객체이며 Document 객체의 자식이다. 요소의 중첩관계는 객체의 트리로 구조화하여 부자관계를 표현한다. DOM Tree는 다음과 같이 네 종류로 구분할 수 있다.
자바스크립트 등의 프로그래밍 언어가 DOM에 접근하고 수정할 수 있는 방법을 제공하는데 일반적으로 프로퍼티와 메서드를 갖는 자바스크립트 객체로 제공된다. 이를 DOM API라 부른다.
하나의 요소를 선택하는 메서드의 이름은 보통 Element
로, 복수의 요소를 선택하는 메서드의 이름은 보통 Elements
로 지어져있다.
// id가 hello인 요소를 선택
const el = document.getElementById('hello');
// section -> div -> li 요소 중 class가 active인 요소를 선택
const el = document.querySelector('section div li.active');
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
// 태그 네임이 h2인 요소들을 복수 선택 -> 유사배열의 형태로 반환
const el = document.getElementsByTagName('h2');
// HTMLCollection(3) [h2.active, h2.non-active, h2.non-active]
// 0: h2.active
// 1: h2.non-active
// 2: h2.non-active
// length: 3
[...el].forEach(el => el.className = 'hello');
class
어트리뷰트 값으로 요소 노드를 모두 선택<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.getElementsByClassName('non-active');
// HTMLCollection(2) [h2.non-active, h2.non-active]
// 0: h2.non-active
// 1: h2.non-active
// length: 2
[...el].forEach(el => el.className = 'active');
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelectorAll('.non-active');
// NodeList(2) [h2.non-active, h2.non-active]
// 0: h2.non-active
// 1: h2.non-active
// length: 2
[...el].forEach(el => el.className = 'active');
위 getElementsByTagName
과 getElementsByClassName
은 유사배열의 형태로 값을 반환하고 실시간으로 노드의 상태 변경을 반영한다. 따라서 다음과 같은 코드는 의도한대로 어트리뷰트 값 변경이 되지 않는다.
<div class="a"></div>
<div class="a"></div>
<div class="a"></div>
const el = document.getElementsByClassName('a');
// el
// HTMLCollection(3) [div.a, div.a, div.a]
// 0: div.a
// 1: div.a
// 2: div.a
// length: 3
for (let i = 0; i < elems.length; i++) {
el[i].className = 'b';
}
// 의도한 값
// <div class="b"></div>
// <div class="b"></div>
// <div class="b"></div>
// 실제로 반영된 값
// <div class="b"></div>
// <div class="a"></div>
// <div class="b"></div>
위 코드를 보면 의도한 값과 달리 실제로 반영된 값이 다른 것을 확인할 수 있다. 그 이유는 다음과 같다. 코드의 동작 순서를 보자.
el.length === 3
이므로 3번의 반복문이 실행될 예정이다.i === 0
일 때, 첫 요소 div.a
의 class 어트리뷰트 값이 a
에서 b
로 변경된다. 이때 el
은 실시간으로 노드의 상태 변경을 반영하는 HTMLCollection 객체이므로 getElementsByClassName
메서드 인자로 지정한 조건과 더 이상 부합하지 않아 반환값에서 실시간으로 제거된다. 즉, el.length === 2
가 된다.i === 1
일 때, 첫 요소가 제거되어 el[1]
은 세 번째 요소의 div.a
의 class 어트리뷰트 값이 a
에서 b
로 변경된다. 이 역시 HTMLCollection 에서 제거된다.i === 2
일 때, 두 번째 요소만 남아 있지만, el.length === 1
이므로 반복문을 종료하게 된다. 따라서 두 번째 요소의 class 어트리뷰트 값은 변경되지 않는다.위와 같은 특성이 있기에, 반복문을 사용할 경우 주의가 필요하다. 위 문제는 아래와 같은 방법으로 해결할 수 있다.
index
를 0으로 고정 후 length
값을 조건으로 무한 반복querySelectorAll
메서드를 사용하여 non-live인 NodeList를 반환<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('.active');
console.log(el.parentNode);
// <div>
// <h2 class="active">A</h2>
// <h2 class="non-active">B</h2>
// <h2 class="non-active">C</h2>
// </div>
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.firstChild); // #text (#text.data = "\n ")
console.log(el.lastChild); // #text (#text.data = "\n ")
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.firstElementChild); // <h2 class="active">A</h2>
console.log(el.lastElementChild); // <h2 class="non-active">C</h2>
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.hasChildNodes()); // true
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.childNodes);
// NodeList(7) [text, h2.active, text, h2.non-active, text, h2.non-active, text]
// 0: text
// 1: h2.active
// 2: text
// 3: h2.non-active
// 4: text
// 5: h2.non-active
// 6: text
// length: 7
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.children);
// HTMLCollection(3) [h2.active, h2.non-active, h2.non-active]
// 0: h2.active
// 1: h2.non-active
// 2: h2.non-ac
// length: 3
<div>
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('.active');
console.log(el.nodeValue); // null
console.log(el.nodeName); // H2
console.log(el.nodeType); // 1 (element)
const elText = el.firstChild;
console.log(elText.nodeValue); // A
console.log(elText.nodeName); // #text
console.log(elText.nodeType); // 3 (text)
<div class="abc def">
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.className); // abc def
console.log(el.className.split(' '); // ['abc', 'def']
add
, remove
, item
, toggle
, contains
, replace
메소드를 제공<div class="abc def">
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.classList);
// DOMTokenList(2) ['abc', 'def', value: 'abc def']
// 0: "abc"
// 1: "def"
// length: 2
// value: "abc def"
마크업 콘텐츠를 추가하는 행위는 XSS(Cross-Site Scripting Attacks)
에 위챡하므로 주의가 필요하다.
<div class="abc def">
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.textContent);
//
// A
// B
// C
//
const el = document.querySelector('h2.active');
console.log(el.textContent); // A
<div class="abc def">
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.innerText);
// A
// B
// C
const el = document.querySelector('h2.active');
console.log(el.innerText); // A
<div class="abc def">
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.innerHTML);
//
// <h2 class="active">A</h2>
// <h2 class="non-active">B</h2>
// <h2 class="non-active">C</h2>
//
const el = document.querySelector('h2.active');
console.log(el.innerHTML); // A
// 마크업이 포함된 컨텐츠를 직접 추가하는 것은 XSS에 취약
el.innerHTML = '';
el.innerHTML += "<script>alert('XSS');</script>";
조작 순서는 다음과 같다.
<div class="abc def">
<h2 class="active">A</h2>
<h2 class="non-active">B</h2>
<h2 class="non-active">C</h2>
</div>
const newEl = document.createElement('h2'); // <h2></h2>
const newText = document.createTextNode('D'); // "D"
const parentEl = document.querySelector('div');
newEl.appendChild(newText);
parentEl.appendChild(newEl);
// parentEl.removeChild(newEl);
const el = document.querySelector('h2.active');
el.insertAdjacentHTML('beforeend', '<p>is from Korea</p>')
innerHTML
과 insertAdjacentHTML()
은 XSS에 취약하다. 텍스트를 추가 또는 변경시에는 textContent
, 새로운 요소의 추가 또는 삭제시에는 DOM 직접 조작 방식을 사용할 것을 권장한다.
style 프로퍼티를 사용하면 inline 스타일 선언을 생성할 수 있다.
const el = document.querySelector('h2.active');
el.style.color = 'yellow';
Element
, 두번째 인자는 일치시킬 의사요소 (보통 null
)const el = document.querySelector('h2.active');
el.style.color = 'yellow';
console.log(window.getComputedStyle(el, null).getPropertyValue('color')); // rgb(255, 255, 0)