HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API를 제공하는 트리 자료구조

HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체가 됨
태그사이에 다른 태그가 있을 수 있으므로 각각의 요소들은 중첩관계를 갖을 수 있다
요소 간의 부모-자식 관계를 반영하여 트리 자료 구조를 구성한다
노드 객체들로 구성된 트리 자료구조를 DOM이라 한다
<!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을 생성한다

문서 노드 (document node)
DOM 트리의 최상단에 존재하는 루트노드, document 객체를 가르킨다
document 객체는 브라우저가 렌더링한 HTML 문서 전체를 가르킴, 전역 객체의 document프로퍼티에 바인딩되어 있다-> document로 참조 가능
최상단에 있기 때문에 DOM 트리의 노드들에 접근하기 위한 진입점 역할을 함
요소 노드 (element node)
HTML 요소를 가르키는 객체
HTML 요소들의 중첩관계를 통해 문서의 구조를 표현한다
어트리뷰트 노드 (attribute node)
HTML 요소의 어트리뷰트를 가르키는 객체
어트리뷰트 노드는 해당 요소 노드에만 연결되어 있다
텍스트 노드 (text node)
HTML 요소의 텍스트를 가르키는 객체
해당 요소 노드의 자식 노드이며 말단 노드임
DOM을 구성하는 노드 객체는 프로토타입에 의한 상속 구조를 갖는다

Document.prototype.getElementById 메서드로 취득
인수로 전달한 id 어트리뷰트 값과 일치하는 요소 노드를 반환
만약 해당 id를 갖는 요소가 여러개면 맨 처음 노드를 반환
일치하는 요소 노드가 없으면 null 반환
HTML 요소에 id 어트리뷰트를 부여하면 id값과 동일한 이름의 전역 변수가 암묵적으로 생성되고 해당 노드 객체가 할당되는 부수 효과 발생
만약 id값과 동일한 전역 변수가 이미 있으면 이 변수에 노드 객체가 할당되지는 않음
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li> <!--글씨 색깔 오렌지 -->
<li id="orange">Orange2</li>
</ul>
<script>
const $elem = document.getElementById('orange');
$elem.style.color = 'orange';
let apple = '사과';
console.log(orange); // orange 노드 객체
console.log(apple); // '사과'
console.log(banana); // banana 노드 객체
</script>
</body>
</html>
Document.prototype.getElementsByTagName 메서드로 취득
인수로 전달한 태그인 모든 요소 노드를 갖는 HTMLCollection 객체를 반환
해당 요소 노드가 없으면 빈 HTMLCollection 객체 반환
HTMLCollection 객체는 유사 배열 객체,이터러블임
모든 노드 선택시 '*'
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="apple">Apple</li> <!--글씨 색깔 보라색 -->
<li id="banana">Banana</li> <!--글씨 색깔 보라색 -->
<li id="orange">Orange</li> <!--글씨 색깔 보라색 -->
</ul>
<script>
const $elems = document.getElementsByTagName('li');
console.log($elems); // 모든 li 요소 노드를 갖는 HTMLCollection 객체
[...$elems].forEach(x=>x.style.color = 'purple');
</script>
</body>
</html>
Element.prototype.getElementsByTagName 메서드는
특정 요소 노드에서 호출하여 해당 요소 노드의 자식들 중에서 탐색하여 반환
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li> <!--글씨 색깔 토마토 -->
<li id="banana">Banana</li> <!--글씨 색깔 토마토 -->
<li id="orange">Orange</li> <!--글씨 색깔 토마토 -->
</ul>
<ul>
<li>li태그</li> <!--글씨 색깔 그대로 -->
</ul>
<script>
const $fruits = document.getElementById('fruits');
const $listFromFruits = $fruits.getElementsByTagName('li');
[...$listFromFruits].forEach(x=>x.style.color = 'tomato');
</script>
</body>
</html>
인수로 전달한 class 값을 갖는 모든 요소들을 갖는 HTMLCollection 객체를 반환
인수로 전달하는 class 값들은 공백으로 여러개의 class 지정 가능
document.getElementsByClassName('food instant');
-> class가 food인 요소 노드 + class가 instant인 요소 노드가 아니고
class가 food 와 instant 둘다 가지는 요소 노드를 탐색
인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 반환
없으면 null 반환
인수로 전달한 CSS 선택자를 만족하는 모든 노드를 구하려면
모든 요소 노드를 담은 NodeList 객체를 반환
NodeList 객체는 유사 배열 객체,이터러블임
id 어트리뷰트가 있는 요소 노드를 취득하는 경우는 getElementById 사용하고 그 외에만 querySelector 사용 ( querySelector가 더 느림)
<!DOCTYPE html>
<html>
<body>
<ul>
<li>pizza</li>
<li>noddle</li>
<li>rice</li>
</ul>
<script>
const $elems=document.querySelectorAll('ul > li');
[...$elems].forEach(x=>x.style.color = 'blue')
</script>
</body>
</html>
Element.prototype.matches 메서드
인수로 전달한 CSS 선택자로 해당 요소 노드를 취득할 수 있는지 true,false 반환
이벤트 위임을 사용할 때 유용
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="pizza">pizza</li>
<li>noddle</li>
<li>rice</li>
</ul>
<script>
const $elems=document.querySelector('ul > li');
console.log($elems.matches('#pizza')); // true
console.log($elems.matches('li')); // true
console.log($elems.matches('.pizza')); // false
</script>
</body>
</html>
HTMLCollection은 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체
NodeList는 경우에 따라 live 객체로 동작, 대부분은 non-live
<!DOCTYPE html>
<head>
<style>
.red{
color : red;
}
.blue{
color : blue;
}
</style>
</head>
<html>
<body>
<ul>
<li class="red">pizza</li>
<li class="red">noddle</li>
<li class="red">rice</li>
</ul>
<script>
const $elems=document.getElementsByClassName('red');
for(let i = 0;i<$elems.length;i++){
$elems[i].className='blue';
}
</script>
</body>
</html>
$elems에는 class명이 red인 li요소 3개가 담겨있는 HTMLCollection 객체가 할당
for문을 통해 각각 li요소들의 class명을 blue로 바꿔줄 때 발생하는 일
이런 일을 방지하려면 HTMLCollection 객체를 배열로 변환하여 사용하는게 좋다
querySelectorAll 메서드는 NodeList 객체를 반환
NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 non-live 객체
하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 live 객체임
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>pizza</li>
<li>noddle</li>
</ul>
<script>
const $fruits= document.getElementById('fruits');
const {childNodes} = $fruits;
console.log(childNodes);
for(let i = 0;i<childNodes.length;i++){
$fruits.removeChild(childNodes[i]);
}
console.log(childNodes);
</script>
</body>
</html>
$fruits는 live 객체이므로 자식노드를 제거할 때마다 새롭게 갱신되어 모든 자식노드가 삭제되지 않는다
결국 배열로 변환해서 사용하는 것이 좋다
노드 탐색 프로퍼티는 모두 접근자 프로퍼티이다
getter만 있는 읽기 전용 접근자 프로퍼티

공백 문자(탭,줄바꿈 등)는 공백 텍스트 노드를 생성함
노드를 탐색할 때 공백 문자에 의해 생성된 공백 텍스트 노드 주의
Node.prototype.hasChildNodes 메서드로 확인
true,false 반환
텍스트 노드도 포함해서 자식 노드의 존재를 확인함
텍스트 노드 제외 하려면 children.length, childElementCount 프로퍼티로
<!DOCTYPE html>
<html>
<body>
<ul id="foods">
</ul>
<script>
const $foods = document.getElementById('foods');
console.log($foods.hasChildNodes()); // true
console.log($foods.children.length); // 0
console.log($foods.childElementCount); // 0
</script>
</body>
</html>
텍스트 노드는 요소 노드의 자식이기 때문에 firstChild 프로퍼티로 접근 가능
Node.prototype.parentNode 프로퍼티로 탐색
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li class="apple">apple</li>
<li class="banana">banana</li>
<li class="orange">orange</li>
</ul>
<script>
const $orange = document.querySelector('.orange');
console.log($orange.parentNode); // ul#fruits
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div id="box">안녕하세요</div>
<script>
const $box= document.getElementById('box');
const $textNode = $box.firstChild;
console.log($box.nodeName,$box.nodeType); // DIV 1
console.log(document.nodeName,document.nodeType); // #document 9
console.log($textNode.nodeName,$textNode.nodeType); // #text 3
</script>
</body>
</html>
Node.prototype.nodeValue 프로퍼티는 setter,getter 둘 다 존재하는 접근자 프로퍼티 -> 값 갱신이 가능
Node.prototype.nodeValue는 노드 객체의 값을 반환
노드 객체의 값 = 텍스트 노드의 텍스트
즉 텍스트 노드가 아니면 null 반환
<!DOCTYPE html>
<html>
<body>
<div id="box">안녕하세요</div>
<script>
const $box= document.getElementById('box');
const $textNode = $box.firstChild;
console.log($box.nodeValue); // null
console.log($textNode.nodeValue); // '안녕하세요'
setTimeout(()=>{
$textNode.nodeValue = '안녕히계세요';
console.log($textNode.nodeValue); // '안녕히계세요'
},3000);
</script>
</body>
</html>
Node.prototype.textContent 프로퍼티도 setter,getter 있는 접근자 프로퍼티
요소 노드의 텍스트와 모든 자손 노드의 텍스트를 취득,변경
즉 요소 노드의 콘텐츠 영역 내에 있는 모든 텍스트를 반환
요소 노드의 textContent 프로퍼티에 새로운 문자열을 할당 시 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가됨
<!DOCTYPE html>
<head>
<style>
.box1{
background-color: red;
width: 100px;
height: 100px;
}
.box2{
background-color: blue;
height: 80px;
}
.box3{
background-color: green;
height: 60px;
}
</style>
</head>
<html>
<body>
<div class="box1">
첫번째
<div class="box2">
두번째
<div class="box3">
젤 안쪽
</div>
</div>
</div>
<script>
const $box = document.querySelector('.box1');
setTimeout(()=>$box.textContent = '새로운 내용',3000);
</script>
</body>
</html>

innerText 프로퍼티는 textContent 프로퍼티와 유사하게 동작
but 사용하지 않는 것이 좋다
새로운 노드를 생성하여 DOM에 추가, 기존 노드를 삭제 or 교체하는 것
-> 리플로우와 리페인트가 발생한다
<!DOCTYPE html>
<html>
<body>
<div id="box">
</div>
<script>
const box = document.getElementById('box');
box.innerHTML = '<ul> <li>사과</li><li>바나나</li></ul>';
</script>
</body>
</html>
아래와 같이 에러 이벤트를 발생시켜 자바스크립트 코드가 실행되게 할 수 있음
<!DOCTYPE html>
<html>
<body>
<div id="box">
</div>
<script>
const box = document.getElementById('box');
box.innerHTML = '<img src="x">';
</script>
</body>
</html>
innerHTML의 문제점들
ul태그.innerHTML = '<li> 사과 </li>'; // 이렇게 하면 원래 있던 li태그들은 삭제됨
ul태그.innerHTML += '<li> 사과 </li>'; // 이렇게 해야됨
<ul>
<li> 1번</li>
<li> 3번</li>
</ul>
기존의 요소를 제거하지 않고 위치를 지정해 요소를 삽입할 때는 innerHTML 비추천
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="1">1번</li>
<li id="3">3번</li>
</ul>
<script>
document.getElementById('1').insertAdjacentHTML("afterend",'<li id="2">2번</li>');
</script>
</body>
</html>
노드를 직접 생성/삽입/삭제/치환할 수 있는 메서드를 DOM은 제공한다
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="1">1번</li>
<li id="2">2번</li>
</ul>
<script>
const table =document.querySelector('ul');
const liNode = document.createElement('li'); // 요소 노드 생성
const textNode = document.createTextNode('3번'); // 텍스트 노드 생성
liNode.appendChild(textNode); // 요소 노드에 자식으로 텍스트 노드 추가
table.appendChild(liNode); // table에 자식으로 요소 노드 추가 (이 때만 리플로우,리페인트 발생)
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="1">1번</li>
<li id="2">2번</li>
</ul>
<script>
const table =document.querySelector('ul');
['3번','4번','5번'].forEach(text=>{
const newNode=document.createElement('li');
newNode.textContent=text;
table.appendChild(newNode); // 리플로우,리페인트 3번이나 발생
});
</script>
</body>
</html>
컨테이너 요소를 미리 만들고 거기에 새롭게 생성한 노드를 추가하고 마지막에 컨테이너 요소를 추가하면
리플로우,리페인트는 한번만 발생하게 할 수 있다
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="1">1번</li>
<li id="2">2번</li>
</ul>
<script>
const table =document.querySelector('ul');
const container = document.createElement('div'); // 컨테이너 요소 생성
['3번','4번','5번'].forEach(text=>{
const newNode=document.createElement('li');
newNode.textContent=text;
container.appendChild(newNode); // 컨테이너 요소에 추가
});
table.appendChild(container); // 리플로우,리페인트 1번만 발생
</script>
</body>
</html>
하지만 li요소만 추가되는 것이 아니고 이를 감싸고 있는 div요소도 같이 추가되는 문제가 생김
컨테이너 요소를 만들 때 DocumentFragment 노드를 만들면 해결 가능
DocumentFragment를 DOM에 추가하면 자신은 제거되고 자식 노드만 DOM에 추가됨
Document.prototype.createDocumentFragment 메서드로 생성
const container = document.createElement('div'); // 일반 노드 만들지 말고
const container = document.createDocumentFragment(); // DocumentFragment로 만들자
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="1">1번</li>
<li id="3">3번</li>
</ul>
<script>
const table = document.querySelector('ul');
const newNode = document.createElement('li');
newNode.textContent = '2번';
table.insertBefore(newNode,table.lastElementChild);
const newNode2 = document.createElement('li');
newNode2.textContent = '4번';
table.insertBefore(newNode2,null);
</script>
</body>
</html>
DOM에 이미 존재하는 노드를 appendChild,insertBefore 메서드로 DOM에 추가하면 원래 있던 위치의 노드는 제거되고 새로운 위치로 노드를 추가함 = 노드가 이동
<!DOCTYPE html>
<html>
<body>
<ul>
<li>맨 위</li>
<li>맨 아래</li>
</ul>
<script>
const table = document.querySelector('ul');
const firstNode = table.firstElementChild;
table.appendChild(firstNode);
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<ul>
<li>원본</li>
</ul>
<script>
const table = document.querySelector('ul');
const newNode = document.createElement('li');
newNode.textContent = '교체본';
table.replaceChild(newNode,table.firstElementChild);
</script>
</body>
</html>
<input id="user" type="text" value="hustlekang">
HTML 요소의 시작 태그에 어트리뷰트 이름 = "어트리뷰트 값" 형식으로 정의
HTML 문서가 파싱될 때 각각의 어트리뷰트는 각각의 어트리뷰트 노드로 변환됨
HTML 요소의 어트리뷰트가 3개이면 어트리뷰트 노드도 3개
모든 어트리뷰트 노드의 참조는 NamedNodeMap 객체에 담겨 요소 노드의 attributes 프로퍼티에 저장
attributes 프로퍼티는 getter만 있는 접근자 프로퍼티
값에 접근 하려면 요소.attributes.어트리뷰트명.value로 접근
<!DOCTYPE html>
<html>
<body>
<input type="text" id="name" value="Aj">
<script>
const {attributes} = document.getElementById('name');
console.log(attributes.id.value);
console.log(attributes.type.value);
console.log(attributes.value.value);
</script>
</body>
</html>
attributes 프로퍼티를 통하지 않고 요소 노드에서 메서드로 바로 HTML 어트리뷰트 값 접근 가능
<!DOCTYPE html>
<html>
<body>
<input type="text" id="name" value="Aj">
<script>
console.log(document.getElementById('name').getAttribute('value'));
document.getElementById('name').setAttribute('value','changed');
</script>
</body>
</html>
요소 노드 객체에는 HTML 어트리뷰트에 대응하는 프로퍼티가 존재
이러한 DOM 프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 갖고 있음
DOM 프로퍼티는 setter,getter 둘다 있는 접근자 프로퍼티
<!DOCTYPE html>
<html>
<body>
<input type="text" id="name" value="Aj">
<script>
const input = document.getElementById('name');
console.log(input.type, input.id, input.value);
</script>
</body>
</html>
- HTML 어트리뷰트 : HTML 요소의 초기 상태를 지정하고 이는 변하지 않는다
- DOM 프로퍼티 : 요소 노드의 최신 상태를 관리
요소 노드의 초기값과 최신값 모두 관리를 해줘야 한다
초기값을 알아야 새로고침 했을 때 OK, 사용자 입력에 의한 최신값은 당연히 알아야 하고
단 모든 DOM 프로퍼티가 사용자 입력에 의해 변경된 최신 상태를 관리하지는 않는다
<input type="text" id="name" value="Aj">
id 프로퍼티는 사용자 입력과 아무런 관계가 없다
id 어트리뷰트와 id 프로퍼티는 항상 동일한 값을 유지한다
하나가 바뀌면 나머지도 바뀜
사용자 입력에 의한 상태변화와 관계있는 DOM 프로퍼티만 최신 상태 값을 관리
그 외의 사용자 입력과 관계없는 어트리뷰트와 DOM 프로퍼티는 항상 동일한 값으로 연동
대부분 1:1 대응이지만 아닌 얘들도 있음
getAttribute 메서드로 취득한 어트리뷰트의 값은 항상 문자열
DOM 프로퍼티로 취득한 최신 값은 문자열이 아닐 수도 있음
<!DOCTYPE html>
<html>
<body>
<input id="check" type="checkbox" checked>
<script>
const input = document.getElementById('check');
console.log(input.checked); // true
</script>
</body>
</html>
사용자 정의 어트리뷰트
data-사용자정의 어트리뷰트 이름 = "값" 형식으로 선언
HTMLElement.dataset 프로퍼티는 모든 data 어트리뷰트를 담은 DOMStringMap 객체 반환
DOMStringMap 객체는 사용자정의 어트리뷰트 이름을 카멜케이스로 변환한 프로퍼티를 갖는다
<!DOCTYPE html>
<html>
<body>
<ul>
<li data-user-id="1" data-sex="male">John</li>
<li data-user-id="2" data-sex="female">May</li>
</ul>
<script>
const users = [...document.querySelector('ul').children];
const 여자 = users.find(x => x.dataset.sex==='female');
console.log(여자.textContent); // May
console.log(여자.dataset.userId); // 2
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div style="width: 100px; height : 100px; background-color : red;"></div>
<script>
const $div = document.querySelector('div');
console.log($div.style); // CSSStyleDeclaration 객체
$div.style.backgroundColor = 'blue'; // 파란색으로 변환
$div.style['background-color'] = 'green'; // css 표기법으로 적용하고 싶을 때
</script>
</body>
</html>
요소의 class 어트리뷰트를 조작하여 다른 스타일을 적용
class 어트리뷰트에 대응하는 DOM 프로퍼티인 className,classList를 통해 적용
<!DOCTYPE html>
<html>
<head>
<style>
.box{
width: 100px;
height: 100px;
}
.green{
background-color: green;
}
</style>
</head>
<body>
<div class="box green"></div>
<script>
const $div = document.querySelector('div');
console.log($div.className); // box green
</script>
</body>
</html>
DOMTokenList 객체의 메서드
HTML과 DOM 표준은 W3C와 WHATWG가 협력으로 공통된 표준을 만들어 왔으나
2018년부터 구글, 애플, 마이크로소프트, 모질라로 구성된 WHATWG가 단일 표준을 제공하기로 두 단체가 합의
DOM은 현재 4개의 버전이 있음
WHATWG(Web Hypertext Application Technology Working Group)