2024. 3. 27. 17:21ㆍ자바스크립트
DOM
HTML 문서가 파싱되면 HTML 요소들은 노드 객체로 변환된다. 그리고 변환된 노드 객체들은 트리 자료구조로 구성되는데 이것을 DOM이라고 한다. 여기서 노드 객체로 변환된다는 것이 무엇인지 간단하게 정리해보자.
<div class="greeting">Hello</div>
위와 같은 HTML 요소는 태그, 어트리뷰트, 텍스트 이렇게 총 3개로 분리할 수 있다.
태그는 파싱된 후 요소 노드로 변환된다. 어트리뷰트는 어트리뷰트 노드로 변한되며 텍스트는 텍스트 노드로 변환된다.
또 문서 노드라는 것이 있는데 이것은 DOM 트리의 루트노드로서 브라우저가 렌더링한 HTML 문서 전체를 가리키는 document 객체를 가리킨다.
그렇다면 트리 자료구조를 구성한다는 것은 무슨말일까?
<ul class="names"><li class="chanuk">찬욱</li></ul>
이렇게 ul 태그 안에 li 태그가 있는 경우 HTML 요소가 파싱되면 다음과 같은 구조로 변환된다.

이와 같이 HTML 요소가 파싱되어 노드 객체로 변환되고 계층적인 트리 구조를 이루는 것이 DOM이다.
지금 그림에서는 document 즉, 문서 노드가 생략되었지만 제일 상단에는 문서노드가 존재하고 이는 모든 노드들에 접근하기 위한 진입점이 된다.
노드의 상속구조
DOM은 단순히 계층구조를 구성하는 것을 넘어서 노드 요소들을 조작할 수 있는 기능도 제공하게 된다.
DOM API라고도 하는데 이를 통해 여러가지 동적 조작이 가능해진다. (참고로 호스트 객체이므로 NodeJS에서는 사용이 안된다.)
각 노드들은 다양한 인터페이스를 상속받게 되는데 이것을 외우는 것은 큰 의미가 없다.
중요한 것은 DOM API를 통해 노드 요소의 조작이 가능하다는 것이다.
간단히 정리하면 모든 노드들은 EventTarget 인터페이스를 상속받는다. 그리고 EventTarget 인터페이스는 Object 인터페이스를 상속받는다.
Object ⬅ EventTarget ⬅ Node ⬅ 문서노드, 요소노드, 어트리뷰트 노드, 텍스트 노드
뒤에서 정리하겠지만 노드 요소들에 이벤트 리스너를 등록해서 이벤트가 발생했을 때 콜백함수를 호출할 수 있는 것도 Node가 EventTarget을 상속받기 때문에 EventTarget 인터페이스에서 제공하는 프로퍼티와 메서드를 사용할 수 있기 때문이다.
DOM API
요소 노드 취득
먼저 노드들의 동적 조작을 하기 위해서는 노드들을 자바스크립트 코드에서 취득해야한다.
다양한 방법들이 있지만 CSS 선택자를 이용해 요소 노드를 취득하는 방법을 알아보자.
<html>
<head>
<title>DOM API</title>
<meta charset="utf-8" />
</head>
<body>
<ul id="todo-list">
<li id="one">알고리즘</li>
<li id="two">리액트</li>
<li id="three">TIL 작성</li>
</ul>
<script>
const $todos = document.querySelector('#todo-list');
</script>
</body>
</html>
const $todos = document.querySelector('#todo-list');
querySelector를 통해서 id가 todo-list인 ul 태그 노드를 취득했다.
querySelector는 CSS 선택자를 만족하는 요소가 여러 개 있는 경우에는 첫번째 요소 노드만 반환하게 된다.
따라서 여러개를 모두 취득하려면 querySelectorAll 을 사용해야한다. 이때 querySelectorAll은 NodeList를 반환하게 되는데 NodeList는 유사배열객체이면서 이터러블이다.
<html>
<head>
<title>DOM API</title>
<meta charset="utf-8" />
</head>
<body>
<ul id="todo-list">
<li id="one">알고리즘</li>
<li id="two">리액트</li>
<li id="three">TIL 작성</li>
</ul>
<script>
const $todos = document.querySelectorAll('ul > li');
console.log($todos);
</script>
</body>
</html>
DOM Collection Object
자바스크립트에서 여러 개의 결과값을 반환하기 위해서 객체를 사용하는데 DOM에서는 DOM 컬렉션 객체를 사용한다.
DOM 컬렉션 객체는 HTMLCollection과 NodeList가 있다. 두 객체 모두 유사 배열 객체이면서 이터러블 프로토콜을 준수한다.
두 객체의 차이점은 HTMLCollection 객체는 실시간으로 변경사항을 반영하는 살아있는 객체이기 때문에 예상치못한 오류가 발생할 수 있다. 가장 간단한 해결방법은 HTMLCollection을 사용하지 않는 것이다. querySelectorAll 메서드가 권장되는 것도HTMLCollection을 반환하는 것이 아닌 NodeList를 반환하기 때문이다.
<html>
<head>
<style>
.red {
color: red;
}
.blue {
color: blue;
}
</style>
</head>
<body>
<ul id="todo-list">
<li class="red">알고리즘</li>
<li class="red">리액트</li>
<li class="red">TIL 작성</li>
</ul>
<script>
const $todos = document.querySelectorAll('.red');
$todos.forEach((todo) => (todo.className = 'blue'));
</script>
</body>
</html>
위의 예시와 같이 NodeList는 forEach 메서드를 사용할 수 있고 그 외 다양한 메서드를 제공받는다.
하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 마찬가지로 살아있는 객체이다.
참 왔다리 갔다리 한다...
따라서 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection과 NodeList 객체를 배열로 변환하여 사용하는 것을 권장한다. 둘다 유사 배열 객체이면서 이터러블이기 때문에 스프레드나 Array.from으로 쉽게 배열로 바꿀 수 있다.
그리고 배열로 바꾸었을때 더 다양한 메서드를 사용할 수 있는 장점이 있다.
노드 탐색
자주 사용되는 탐색 메서드에 대해 살펴보자.
<html>
<head>
<style>
.red {
color: red;
}
.blue {
color: blue;
}
</style>
</head>
<body>
<ul id="todo-list">
<li class="red" id="algorithm">알고리즘</li>
<li class="red">리액트</li>
<li class="red">TIL 작성</li>
<li class="red">프로젝트</li>
<li class="red">함수형 프로그래밍</li>
</ul>
<script>
// parentNode
const $algorithm = document.querySelector('#algorithm');
console.log($algorithm.parentNode); // ul 태그
// firstElementChild, lastElementChild
const $todoList = document.querySelector('#todo-list');
$todoList.firstElementChild.className = 'blue';
$todoList.lastElementChild.className = 'blue';
// hasChildNodes(), childNodes, children
// childNodes : 텍스트 노드까지 모두 반환 (띄워쓰기 포함)
// children : 요소 노드만을 반환
if ($todoList.hasChildNodes()) {
console.log($todoList.childNodes);
console.log($todoList.children);
for (const a of $todoList.children) {
console.log(a.nodeType);
}
}
// previousSibling, nextSibling : 텍스트 노드를 포함한 모든 형제노드 탐색 (띄워쓰기 포함)
// previousElementSibling, nextElmentSibling : 요소 노드만을 탐색
console.log($todoList.firstElementChild.previousSibling); //'\n'
console.log($todoList.firstElementChild.nextSibling); // '\n'
console.log($todoList.firstElementChild.previousElementSibling); // null
console.log($todoList.firstElementChild.nextElementSibling); // 리액트
</script>
</body>
</html>
Element가 들어가면 텍스트 노드를 제외한 요소 노드만을 반환하게 된다.
노드 조작
텍스트 노드 조작
텍스트 노드를 조작하기 위해서는 텍스트 노드의 부모 노드인 요소 노드에 접근을 해야한다.
querySelector를 통해 부모 노드인 요소 노드를 선택하고, firstChild를 통해 텍스트 노드에 접근한다.
<html>
<head>
<style>
.red {
color: red;
}
.blue {
color: blue;
}
</style>
</head>
<body>
<ul id="todo-list">
<li class="red" id="algorithm">알고리즘</li>
<li class="red" id="react">리액트</li>
<li class="red" id="til">TIL 작성</li>
<li class="red" id="project">프로젝트</li>
<li class="red" id="functional-programming">함수형 프로그래밍</li>
</ul>
<script>
const algorithm = document.querySelector('#algorithm');
console.log(algorithm.nodeName);
console.log(algorithm.nodeType); // 1이면 Element node 요소노드
const textNode = algorithm.firstChild;
console.log(textNode.nodeName);
console.log(textNode.nodeType); // 3이면 텍스트 노드
console.log(textNode.nodeValue);
textNode.nodeValue = '알 고 리 즘 ✅';
</script>
</body>
</html>
어트리뷰트 노드 조작
<html>
<head>
<style>
.red {
color: red;
}
.blue {
color: blue;
}
#project {
background-color: lightgreen;
}
</style>
</head>
<body>
<ul id="todo-list">
<li class="red done" id="algorithm">알고리즘</li>
<li class="red" id="react">리액트</li>
<li class="red done" id="til">TIL 작성</li>
<li class="red" id="project">프로젝트</li>
<li class="red" id="functional-programming">함수형 프로그래밍</li>
</ul>
<script>
const elems = document.querySelectorAll('li');
[...elems].forEach((elem) => {
console.log(elem.className); // 클래스의 갯수가 복수인 경우에는 문자열을 반환
if (elem.className === 'red') elem.className = 'blue';
});
[...elems].forEach((elem) => {
console.log(elem.classList); // DOMTokenList를 반환
if (elem.classList.contains('blue')) elem.classList.replace('blue', 'red');
});
const til = document.querySelector('#til');
console.log(til.id);
til.id = 'project';
</script>
</body>
</html>
className은 요소노드의 모든 클래스를 문자열로 반환한다. 클래스가 여러 개이면 공백으로 구분된다.
classList는 요소노드의 모든 클래스를 DOMTokenList 형태로 반환을 한다. classList는 add, remove, item, toggle, contains, replace 메서드를 제공한다.
id는 요소노드의 id 어트리뷰트에 접근할 수 있다.
<!DOCTYPE html>
<html>
<body>
<input type="text" />
<script>
const input = document.querySelector('input[type=text]');
if (!input.hasAttribute('value')) input.setAttribute('value', 'hello!');
console.log(input.getAttribute('value'));
input.removeAttribute('value');
</script>
</body>
</html>
어트리뷰트가 존재하는지 확인하고 만들고 제거하는 메서드들이다. 직관적인 표현으로 쉽게 사용할 수 있다.
HTML 콘텐츠 조작
textContent와 innerHTML 두 가지 방식의 차이점에 대해 분석해보자.
textContent는 말 그대로 텍스트 요소만 가져오는 것이다.
<!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>
<script>
const ul = document.querySelector('ul');
console.log(ul.textContent);
/*
Seoul
London
Newyork
Tokyo
띄워쓰기와 자식 텍스트 모두를 포함한 텍스트 요소들을 가져온다.
*/
const one = document.getElementById('one');
console.log(one.textContent);
one.textContent += ', Korea';
one.textContent = '<h1>Heading</h1>';
</script>
</body>
</html>
ul 태그를 선택하고 textContent를 하니까 개행문자를 포함한 자식 노드들의 모든 텍스트 노드들까지 전부 가져온다.
그리고 textContent에 접근해서 텍스토 노드의 값을 변경할 수 있다. 단, 마크업은 문자열로 출력된다.
innerText 프로퍼티를 사용해도 요소의 텍스트 콘텐츠에만 접근할 수 있다. 하지만 비표준이며 CSS에 순종적이기 때문에 사용하지 않는 것이 좋다.
<!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>
<script>
const ul = document.querySelector('ul');
console.log(ul.textContent);
/*
<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>
*/
const one = document.getElementById('one');
one.innerHTML = '<em class="blue">, Korea</em>';
</script>
</body>
</html>
innerHTML은 해당 요소의 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득한다. 이때 마크업을 포함한다.
따라서 innerHTML 프로퍼티를 사용해서 마크업이 포함된 새로운 컨텐츠를 지정할 수 있다.
하지만 마크업이 포함된 콘텐츠를 추가하는 것은 크로스 스크립팅 공격에 취약하다.
innerHTML 프로퍼티로 script 태그를 추가해 자바스크립트가 실행되도록 할 수 있기 때문이다.
DOM 조작 방식
그렇다면 innerHTML에 마크업을 적는 대신 새로운 콘텐츠를 추가하는 방법은 무엇이 있을까?
DOM을 직접 조작하는 방법이 있는데 다음과 같은 순서로 진행이 된다.
- createElement(tagName)을 사용해 새로운 요소 노드를 생성한다.
- createTextNode(text)를 사용해 텍스트 노드를 추가한다.
- appendChild(Node)를 통해 새로운 노드를 DOM 트리에 추가하거나 removeChild(Node)를 통해 DOM 트리에 노드를 제거할 수 있다.
<!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>
<script>
const newElem = document.createElement('li');
const newText = document.createTextNode('Beijing');
newElem.appendChild(newText);
const container = document.querySelector('ul');
container.appendChild(newElem);
const removeElem = document.getElementById('one');
container.removeChild(removeElem);
</script>
</body>
</html>
insertAdjacentHTML을 사용해서 노드를 원하는 위치에 넣어줄 수 있다.
insertAdjacentHTML은 위치와 문자열을 인자로 받는데 위치에는 beforebegin, afterbegin, beforeend, afterend가 올 수 있다. 그리고 두번째 인자의 텍스트를 HTML 요소로 변환해서 위치에 삽입한다.

그렇다면 innerHTML, DOM 조작방식, insertAdjacentHTML의 장단점을 파악해보자.
innerHTML은 DOM 조작방식에 비해 빠르고 간편하지만 XSS공격에 취약점이 있어서 사용자로부터 입력받은 콘텐츠를 추가할 때 주의해야한다. 그리고 간편하게 문자열로 정의한 요소를 DOM에 추가할 수 있지만 HTML을 재파싱하기 때문에 비효율적이다.
DOM 조작방식은 특정 노드 한개를 DOM에 추가할 때 적합하지만 여러개를 추가하는 경우 코드가 길어질 수 있다.
insertAdjacentHTML은 간편하게 문자열로 정의한 요소를 원하는 위치에 삽입할 수 있지만 innerHTML과 마찬가지로 XSS공격에 취약점이 있다.
따라서 XSS공격에 취약점이 있는 innerHTML, insertAdjacentHTML보다 textContent, DOM 조작방식을 권유한다.
Style
style 프로퍼티를 이용하면 inline 스타일 선언이 가능하다.
스타일은 인라인, 외부 파일, 내부 style 태그를 통해 선언할 수 있는데 우선순위는 인라인 > 내부 > 외부 순서이다.
그리고 window.getComputedStyle을 통해 인자로 주어진 요소의 모든 CSS 프로퍼티 값을 받을 수 있다. 그 중 원하는 프로퍼티에만 접근하려면 getPropertyValue("원하는 프로퍼티")를 사용하면 된다.
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px;
height: 50px;
background-color: red;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="box"></div>
<h1>HELLO</h1>
<script>
const title = document.querySelector('h1');
title.style.color = 'blue';
const box = document.querySelector('.box');
const width = getComputedStyle(box).getPropertyValue('width');
console.log(width); // 100px
</script>
</body>
</html>
'자바스크립트' 카테고리의 다른 글
이벤트 (0) | 2024.04.23 |
---|---|
네이티브 객체 vs 호스트 객체 (0) | 2024.03.26 |
브라우저의 렌더링 과정 (0) | 2024.03.25 |
호이스팅 (0) | 2024.03.06 |
스코프 (0) | 2024.03.03 |