[자바스크립트 필기] #21 - DOM이란?
*이 필기는 웹 프로그래밍 튜토리얼에서 보고 작성합니다.
DOM은 HTML 문서의 계층적 구조와 정보를 표현하며 프로퍼티와 메서드들은 제공하는 API이다.
1. 노드란
HTML요소는 HTML 문서를 구성하는 개별적인 요소를 말한다.
이 HTML 요소가 엔진에 의해 DOM을 구성하는 노드 객체로 변환된다. 이때 HTML 요소 간의 부자 관계를 반영해 HTML 요소를 객체화한 모든 노드 객체들을 트리 자료 구조로 구성한다.
이렇게 노드 객체들로 구성된 트리 자료구조를 DOM이라 한다.
이렇게해서 노드 타입에 따라 필요한 기능을 모은 DOM api가 있고 이 DOM api를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.
2. 요소 노드 얻기
우선 HTML의 구조나 내용, 스타일을 조작하려면 무조건 그 요소 노드를 취득해야한다. 이 노드들을 취득하는데에도 여러가지 방법이 있다.
id를 이용한 요소 노드 취득
<!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>
</head>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
// id 값이 'banana'인 요소 노드를 탐색하여 반환한다.
// 두 번째 li 요소의 요소 노드가 반환된다.
const $elem = document.getElementById('banana');
// 획득한 요소 노드의 style.color 프로퍼티 값을 변경한다.
$elem.style.color = 'red';
</script>
</body>
</html>
- Document.prototype.getElementById 메서드이다.
- 언제나 단 하나의 요소를 반환한다.
- 존재하지 않으면 null을 반환한다.
태그 이름을 이용한 요소 노드 취득
<!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>
</head>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
// 태그 이름이 li인 요소 노드를 모두 탐색하여 반환한다.
// 탐색된 요소 노드들은 HTMLCollection 객체에 담겨 반환된다.
const $elem = document.getElementsByTagName('li');
// 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
// HTMLCollection 객체를 배열로 변환하여 순회하며 프로퍼티 변경을 한다.
[...$elem].forEach(elem => { elem.style.color = 'red'; });
</script>
</body>
</html>
- Document.prototype/Element.prototype.getElementsByTagName 메서드이다.
- 인수로 전달된 태그 이름을 갖는 모든 노드들을 탐색해 반환한다.
- 여러개의 노드 객체를 갖는 HTMLCollection 객체를 반환한다.
- 이 객체는 유사 배열 객체이면서 이터러블이다.
class를 이용한 요소 노드 취득
<!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>
</head>
<body>
<ul>
<li class="fruit apple">Apple</li>
<li class="fruit banana">Banana</li>
<li class="fruit orange">Orange</li>
</ul>
<script>
// class 값이 'fruit'인 요소 노드를 모두 찾아 HTMLCollection 객체에 담아 반환
const $elems = document.getElementsByClassName('fruit');
// 취득한 요소들의 CSS color 프로퍼티 값을 변경한다.
[...$elems].forEach(elem => { elem.style.color = 'red'; });
</script>
</body>
</html>
- Document.prototype/Element.prototype.getElementsByClassName 메서드이다.
- 주어진 class 어트리뷰트 값을 갖는 모든 요소 노드들을 탐색해서 반환한다.
- HTMLCollection 객체에 담아서 반환한다.
CSS 선택자를 이용한 요소 노드 취득
CSS 선택자는 스타일을 적용하고자 하는 HTML 요소를 특정할 때 사용하는 문법이다.
<!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>
</head>
<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');
// 취득한 요소 프로퍼티 값 변경
$elem.style.color = 'red';
</script>
</body>
</html>
- Document.prototype/Element.prototype.querySelector 메서드이다.
- 이 메서드는 인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색해 반환한다.
- 여러 개이면 그 중 첫번째 것을 반환한다.
- 인수로 전달한 CSS 선택자를 만족하는 모든 노드들을 반환하는Document.prototype/Element.prototype.querySelectorAll 메서드도 있다.
- 이 메서드는 NodeList 객체를 반환한다. 이것도 유사 배열 객체이면서 이터러블이다.
이러한 querySelector 메서드는 구체적인 방법으로 요소 노드를 취득할 수 있다.
따라서
- id 어트리뷰트가 있는 요소를 취득할 때는 getElementById 메서드를 사용하고
- 그 외의 경우에는 querySelector/querySelectorAll 메서드를 사용하는 것이 권장된다.
HTMLCollection과 NodeList
이러한 객체들은 실시간으로 요소 노드들의 상황이 반영되기 때문에 이 객체를 그대로 쓰기에는 위험성이 따른다. 따라서 안전하게 사용하기 위해 배열로 변환하여 사용하는 것이 권장된다.
스프레드 문법을 통해 간단하게 배열로 바꿔서 사용할 수 있다!
<!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>
</head>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</liclass=>
</ul>
<script>
const $fruits = document.getElementById('fruits');
// childNodes 프로퍼티는 NodeList 객체를 반환한다.
const { childNodes } = $fruits;
// 스프레드 문법으로 배열로 바꾼다.
[...childNodes].forEach(childNode => {
$fruits.removeChild(childNode);
});
// $fruits 요소의 모든 자식 노드가 삭제되었다.
console.log(childNodes); // NodeList []
</script>
</body>
</html>
3. 노드 탐색
요소 노드를 취득하고 나서 그 노드를 기준으로 부모, 형제, 자식 노드 등으로 옮겨 다녀야할 때가 있다.
이러한 경우를 위해 트리 탐색 프로퍼티가 제공된다.
공백 텍스트 노드
HTML 문서의 공백 문자는 공백 텍스트 노드를 생성한다. 따라서 이것에 주의해야한다.
자식 노드 탐색
프로퍼티 | 설명 |
Node.prototype.childNodes | 자식 노드들을 모두 탐색해 NodeList에 담아 반환한다. 요소 노드뿐만 아니라 텍스트 노드도 포함될 수 있다. |
Element.prototype.children | 자식 노드 중에서 요소 노드만 모두 탐색해 HTMLCollection 객체에 담아 반환한다. |
Node.prototype.firstChild | 첫 번째 자식 노드를 반환한다. |
Node.prototype.lastChild | 마지막 자식 노드를 반환한다. |
Element.prototype.firstElementChild | 첫 번째 자식 요소 노드를 반환한다. |
Element.prototype.lastElementChild | 마지막 자식 요소 노드를 반환한다. |
자식 노드 존재 확인
Node.prototype.hasChildNodes 메서드를 사용해서 요소 노드와 텍스트 노드를 포함해 자식 노드를 존재하는지 확인할 수 있다.
- 존재하면 true
- 존재하지 않으면 false
요소 노드의 텍스트 노드 탐색
요소 노드의 텍스트 노드는 요소 노드의 자식 노드이다. 따라서 firstchild 프로퍼티로 접근할 수 있다.
부모 노드 탐색
Node.prototype.parentNode 프로퍼티를 사용한다.
형제 노드 탐색
프로퍼티 | 설명 |
Node.prototype.previousSibling | 형제 노드에서 자신의 이전 형제 노드를 반환한다. 텍스트 노드일 수도 있다. |
Node.prototype.nextSibling | 형제 노드에서 자신의 다음 형제 노드를 반환한다. 텍스트 노드일 수도 있다. |
Element.prototype.previousElementSibling | 형제 노드에서 자신의 이전 형제 요소 노드를 반환한다. |
Element.prototype.nextElementSibling | 형제 노드에서 자신의 다음 형제 요소 노드를 반환한다. |
4. 요소 노드의 텍스트 조작
nodeValue
nodeValue 프로퍼티는 setter와 getter가 모두 존재하기 때문에 참조와 할당이 모두 가능하다.
이 프로퍼티를 참조하면 노드 객체의 값을 반환한다. 여기서 이 값은 텍스트 노드의 텍스트를 말하기 때문에 텍스트 노드가 아닌 요소 노드의 nodeValue 프로퍼티를 참조해봤자 null 밖에 안나온다.
textContent
textContent 프로퍼티는 setter와 getter가 모두 존재하기 때문에 참조와 할당이 모두 가능하다.
요소 노드의 textContent 프로퍼티를 참조하면 요소 노드의 시작 태그와 종료 태그 사이의 텍스트를 모두 반환한다.
요소 노드의 textContent 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식이 삭제되고 문자열이 텍스트로 추가된다.
*이와 비슷한 innerText 프로퍼티가 있는데 모종의 이유로 사용되지 않는다.
- CSS에 순종적이여서 CSS에 의해 비표시될 수도 있다.
- textContent보다 느리다.
5. 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>
</head>
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');
// 1. 요소 노드 생성
const $li = document.createElement('li');
// 2. 텍스트 노드 생성
const textNode = document.createTExtNode('Banana');
// 3. 텍스트 노드를 $li 노드의 자식으로 추가
$li.appendChild(textNode);
// 4. $li 요소를 #fruits 노드의 마지막 자식으로 추가
$fruits.appendChild($li);
</script>
</body>
</html>
이렇게 생성하고 삽입할 수 있다.
좀 더 자세히 알아보자
요소 노드 생성
Document.prototype.createElement(tagName) 메서드는 요소 노드를 생성하여 반환한다.
// 1. 요소 노드 생성
const $li = document.createElement('li');
이렇게 생성된 요소 노드는 DOM에 속하지 않고 홀로 존재하는 상태이다. 따라서 따로 DOM 추가해주는 행동이 필요하다.
텍스트 노드 생성
Document.prototype.createTextNode(text) 메서드는 텍스트 노드를 생성해 반환한다.
// 2. 텍스트 노드 생성
const textNode = document.createTextNode('Banana');
이렇게 된 텍스트 노드는 요소 노드의 자식 노드로 추가된 상태가 아니라 홀로 존재하는 상태이다. 따라서 따로 요소 노드의 자식 노드로 추가시키는 작업이 필요하다.
텍스트 노드를 요소 노드의 자식 노드로 추가
Node.prototype.appendChild(childNode) 메서드는 인수로 받은 노드를 이 메서드를 호출한 노드의 마지막 자식으로 추가한다.
// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
여기까지 해도 아직까진 요소노드와 그 텍스트 노드가 연결되었을 뿐, DOM에는 연결되지 않았다. 그래서 마지막으로 DOM에 추가시키는 작업을 하면 된다.
*위 예제처럼 요소 노드에 자식노드가 없는 경우에는 텍스트 노드를 만들어서 붙이는 것보단, textContent 프로퍼티를 이용하는 것이 더 편하다.
요소 노드를 DOM에 추가
Node.prototype.appenChild 메서드를 사용해서 DOM에 추가해보자
// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);
6. 어트리뷰트
HTML 요소들은 여러 개의 어트리뷰트(속성)을 가질 수 있다. 이러한 어트리뷰트들은 HTML 문서가 파싱될 때 어트리뷰트 노드로 변환되고 요소 노드와 연결된다. 어트리뷰트 하나당 하나의 어트리뷰트 노드가 생성된다. 이러한 어트리뷰트들의 참조는 유사 배열 객체이자 이터러블인 NamedNodeMap 객체에 담겨서 요소 노드의 attributes 프로퍼티에 저장된다.
그래서 요소 노드의 Element.prototype.attributes 프로퍼티로 취득할 수 있다.
<!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>
</head>
<body>
<input id="user" type="text" value="lee">
<script>
// 요소 노드의 attribute 프로퍼티는 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴
// NamedNodeMap 객체를 반환한다.
const { attributes } = document.getElementById('user');
console.log(attributes);
console.log(attributes.id.value); // user
console.log(attributes.type.value); // text
console.log(attributes.value.value); // lee
</script>
</body>
</html>
이와 같이 attributes 프로퍼티를 통해서 접근해야하기 때문에 불편할 수도 있다.
그래서 Element.prototype.getAttribute/setAttribute 메서드를 사용해서 직접 어트리뷰트 값을 참조하거나 변경할 수 있다.
<!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>
</head>
<body>
<input id="user" type="text" value="lee">
<script>
const $input = document.getElementById('user');
// value 어트리뷰트 값을 취득
const inputValue = $input.getAttribute('value');
console.log(inputValue); // lee
// value 어트리뷰트 값을 변경
$input.setAttribute('value', 'foo');
console.log($input.getAttribute('value')); // foo
</script>
</body>
</html>
7. 스타일
HTMLElement.prototype.style 프로퍼티는 참조와 변경이 가능한 프로퍼티로 요소 노드의 인라인 스타일을 취득하거나 변경한다.
<!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>
</head>
<body>
<div style="color: red">Hello World</div>
<script>
const $div = document.querySelector('div');
// 인라인 스타일 취득
console.log($div.style);
// 인라인 스타일 변경
$div.style.color = 'blue';
// 인라인 스타일 추가
$div.style.width = '100px';
$div.style.height = '100px';
$div.style.backgroundColor = 'yellow';
</script>
</body>
</html>
이렇게 CSS 프로퍼티를 HTML 요소에 추가하거나 변경할 수 있다.
- 카멜 케이스를 써야한다. background-color -> backgroundColor
- 단위를 지정해야하는 CSS 프로퍼티의 값은 반드시 단위를 지정해야한다.
클래스 조작
.으로 시작하는 클래스 선택자를 이용해 미리 CSS class를 정의한 다음, 각 HTML 요소들의 class 어트리뷰트 값을 변경해 HTML 요소의 스타일을 변경할 수 있다.
먼저 className이다.
Element.prototype.className 프로퍼티로 class 어트리뷰트 값을 문자열 형태로 취득하거나 변경할 수 있다.
<!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">
<style>
.box {
width: 100px; height: 100px;
background-color: antiquewhite;
}
.red { color: red; }
.blue { color: blue; }
</style>
<title>Document</title>
</head>
<body>
<div class="box red">Hello World</div>
<script>
const $box = document.querySelector('.box');
// .box 요소의 class 어트리뷰트 값을 취득
console.log($box.className); // 'box red'
// .box 요소의 class 어트리뷰트 값 중에서 'red'만 'blue'로 변경
$box.className = $box.className.replace('red', 'blue');
</script>
</body>
</html>
다음에는 classList이다.
Element.prototype.classList 프로퍼티는 class 어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.
<!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">
<style>
.box {
width: 100px; height: 100px;
background-color: antiquewhite;
}
.red { color: red; }
.blue { color: blue; }
</style>
<title>Document</title>
</head>
<body>
<div class="box red">Hello World</div>
<script>
const $box = document.querySelector('.box');
// .box 요소의 class 어트리뷰트 정보를 담은 DOMTokenList 객체를 획득
// 이 객체는 실시간으로 상태를 반영하는 객체이다.
console.log($box.classList);
// .box 요소의 class 어트리뷰트 값 중에서 'red'만 'blue'로 변경
$box.classList.replace('red', 'blue');
</script>
</body>
</html>
이러한 DOMTokenList 객체는 유용한 메서드들을 제공한다.
- add(...className)
- remove(...className)
- item(index)
- contains(className)
- replace(oldClassName, newClassName)
- toggle(className[, force])
여기까지 입니다 - 틀린 점이 있다면 꼭 지적해주세요!