39. DOM
- DOM은 HTML문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API(프로퍼티, 메서드)를 제공하는 트리 자료구조
39.1 노드
39.1.1 HTML요소와 노드 객체
- HTML요소 : HTML문서를 구성하는 개별적인 요소
중첩관계에 의한 부자관계가 형성되어 트리구조를 가지게 됨
1. 트리 자료구조
- 트리자료구조 : 노드들의 계층구조를 표현하는 비선형 자료구조
- DOM : 노드 객체들로 구성된 트리 자료구조
39.1.2 노드 객체의 타입
- 렌더링 엔진은 위 HTML문서를 파싱하여 다음의 DOM을 생성함
- 노드 객체는 총 12개의 종류가 있음
1. 문서 노드 (document node)
- DOM트리의 최상위에 존재하는 루트 노드로 document객체를 가리킴, DOM트리의 루트 노드
2. 요소 노드 (element node)
- HTML요소를 가리키는 객체, 문서의 구조를 표현함
3. 어트리뷰트 노드 (attribute node)
- HTML요소의 텍스트를 가리키는 객체, 문서의 정보를 표현함, 자식 노드를 가질 수 없는 리프 노드
39.1.3 노드 객체의 상속 구조
- DOM을 구성하는 노드 객체는 브라우저 환경에서 추가적으로 제공하는 호스트 객체로, 프로토타입에 의한 상속구조를 가짐
- 노드 객체의 상속구조는 개발자도구에서 확인할 수 있음
- DOM API를 통해 HTML구조나 내용 또는 스타일 등을 동적으로 조작할 수 있음
39.2. 요소 노드 취득
- HTML의 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야 함
39.2.1 id를 이용한 요소 노드 취득
- Document.prototype.getElementById메서드는 인수로 전달한 id값을 갖는 첫번째 요소 노드를 탐색하여 반환함
- 해당 요소가 존재하지 않을 경우 null반환
const $elem = document.getElementById('banana');
$elem.style.color = 'red';
39.2.2 태그 이름을 이용한 요소 노드 취득
- Document.prototype/Element.prototype.getElementsByTagName메서드는 인수로 전달한 태그 이름을 가진 모든 요소를 탐색하여 반환함
const $elems = document.getElementsByTagName('li');
[...$elems].forEach(elem => { elem.style.color = 'red'; });
- getElementsByTagName메서드가 반환하는 HTMLCollection객체는 유사배열객체이면서 이터러블임
- HTML문서의 모든 요소 노드를 취득하려면 getElementsByTagName메서드의 인수로 '*'을 전달
const $all = document.getElementsByTagName('*');
- 해당 요소가 존재하지 않을 경우, 빈 HTMLCollection객체 반환
39.2.3 class를 이용한 요소 노드 취득
- Document.prototype/Element.prototype.getElementsByClassName메서드는 인수로 전달한 class값을 갖는 모든 요소들을 탐색하여 HTMLCollection객체를 반환함
- 해당 요소가 존재하지 않을 경우 빈 HTMLCollectin객체 반환
const $bananasFromDocument = document.getElementByClassName('banana');
39.2.4 CSS선택자를 이용한 요소 노드 취득
- Document.prototype/Element.prototype.querySelector메서드는 인수로 전달한 CSS선택자에 맞는 하나의 요소를 반환함
- 해당 요소가 존재하지 않을 경우 null반환
const $elem = document.querySelector('.banana');
$elem.style.color = 'red';
- Document.prototype/Element.prototype.querySelectorAll메서드는 인수로 전달한 CSS선택자에 맞는 모든 요소 노드를 탐색하여 NodeList객체를 반환함
- 해당 요소가 존재하지 않을 경우 빈 NodeList객체를 반환
- HTML문서의 모든 요소 노드를 취득하려면 인수로 '*'을 전달
메서드들 비교
- querySelector, querySelectorAll은 getElementById, getElementBy***메서드보다 다소 느림
- querySelector, querySelectorAll를 사용하면 구체적인 조건으로 요소 노드를 취득할 수 있다는 장점이 있음
- id어트리뷰트가 있는 요소 노드 취득 : getElementById메서드 사용, 그 외엔 querySelector, querySelectorAll메서드 사용 권장
39.2.5 특정 요소 노드를 취득할 수 있는지 확인
- Element.prototype.matches메서드는 인수로 전달한 CSS선택자를 통해 특정 요소 노드를 취득할 수 있는지 확인함
- 이벤트 위임을 사용할 때 유용함
const $apple = document.querySelector('.apple');
console.log($apple.matches('#fruits > li.apple')); // true
39.2.6 HTMLCollection과 NodeList
- HTMLCollection, NodeList 둘 다 유사배열객체이면서 이터러블임
HTMLCollection
- 노드 객체의 상태 변화를 실시간으로 반영하는 살아있는 DOM컬렉션 객체
- HTMLCollection객체를 배열로 전환하면 고차함수를 사용할 수 있음
[...$elems].forEach(elem => elem.className = 'blue');
NodeList
- 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체 (단, childNodes프로퍼티가 반환하는 NodeList객체는 live객체로 동작함)
- querySelectorAll이 반환하는 NodeList객체는 NodeList.prototype.forEach/item/entryes/keys/values메서도를 상속받아 사용할 수 있음
- 안전하게 DOM컬렉션을 사용하려면 배열로 변환하여 사용하는 것을 권장함
39.3 노드 탐색
- 요소 노드를 취득해 부모, 형제, 자식 노드 탐색이 가능함
노드 탐색 프로퍼티
- 노드 탐색 프로퍼티는 모두 접근자 프로퍼티로, 참조만 가능한 읽기 전용임
1. Node.prototype
- parentNode, previousSibling, firstChild, childNodes
2. Element.prototype
- previousElementSibling, nextElementSibling, children
39.3.1 공백 텍스트 노드
- HTML요소 사이의 스페이스, 탭, 줄바꿈 등의 공백 문자가 생성하는 노드
39.3.2 자식 노드 탐색
39.3.3 자식 노드 존재 확인
- Node.prototype.hasChildNodes메서드를 사용하여 존재하면 true/존재하지 않으면 false를 반환
39.3.4 요소 노드의 텍스트 노드 탐색
console.log(document.getElementById('foo').firstChild);
39.3.5 부모 노드 탐색
- Node.prototype.parentNode프로퍼티를 사용
39.3.6 형제 노드 탐색
- Node.prototype.previousSibling/nextSibling, Element.prototype.previousElementSibling.nextElementSibling
39.4 노드 정보 취득
- Node.prototype.nodeType : 노드 타입을 나타내는 상수를 반환
- Node.prototype.nodeName : 노드 이름을 문자열로 반환
39.5 요소 노드의 텍스트 조작
39.5.1 nodeValue
- Node.prototype.nodeValue프로퍼티는 노드 객체의 값을 반환함
- 참조, 할당 둘 다 가능
39.5.2 textContent
- Node.prototype.textContent프로퍼티는 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경함
- 참조, 할당 둘 다 가능
- textContent와 유사하게 동작하는 innerText프로퍼티는 CSS속성에 의해 해당 텍스트를 반환하지 않는 일이 생기거나 느리기 때문에 추천하지 않음
39.6 DOM 조작
- DOM조작은 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것
- DOM조작에 의해 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트 발생
39.6.1 innerHTML
- Element.prototype.innerHTML프로퍼티는 요소 노드의 HTML마크업을 취득하거나 변경함
- 참조, 할당 둘 다 가능
- HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립팅 공격에 취약함
39.6.2 insertAdjacentHTML메서드
- Element.prototype.insertAdjacentHTML메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입함
- HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립팅 공격에 취약함
39.6.3 노드 생성과 추가
1. 요소 노드 생성
- Document.prototype.createElement(tagName)메서드는 요소 노드를 생성하여 반환함
const $li = document.createElement('li');
2. 텍스트 노드 생성
- Document.prototype.createTextNode(text)메서드는 텍스트 노드를 생성하여 반환함
const textNode = document.createTextNode('Banana');
3. 텍스트 노드를 요소 노드의 자식 노드로 추가
- Node.prototype.appendChild(childNode)메서드는 매개변수 childNode에게 인수로 전달한 appendChild메서드를 호출한 노드의 마지막 자식 노드로 추가함
$li.appendChild(textNode);
4. 요소 노드를 DOM에 추가
- Node.prototype.appendChild메서드를 사용하여 텍스트 노드와 부자관계로 연결한 요소 노드를 #fruits요소 노드의 마지막 자식 요소로 추가함
$fruits.appendChild($li);
39.6.4 복수의 노드 생성과 추가
- 여러 개의 노드를 생성하여 DOM에 추가하면 리플로우와 리페인트가 여러번 실행되므로 이러한 문제를 회피하기 위해 DocumentFragment를 사용
...
const $fruits = document.getElementById('fruits');
// DocumentFragment 노드 생성
const $fragment = document.createDocumentFragment();
['Apple', 'Banana', 'Orange'].forEach(text => {
const $li = document.createElement('li');
const textNode = document.createTextNode(text);
$li.appendChild(textNode);
$fragment.appendChild($li);
$fruits.appendChild($fragment);
})
...
39.6.5 노드 삽입
마지막 노드로 추가
- Node.prototype.appendChild메서드는 인수로 전달받은 노드를 자신을 호추한 노드의 마지막 자식 노드로 DOM에 추가, 위치 지정할 수 없음
지정한 위치에 노드 삽입
- Node.prototype.insertBefore(newNode, childNode)메서드는 첫번째 인수로 전달받은 노드를 두번째 인수로 전달받은 노드 앞에 삽입
$fruits.insertBefore($li, $fruits.lastElementChild);
39.6.6 노드 이동
- DOM에 이미 존재하는 노드를 appendChild또는 insertBefore메서드를 사용하여 DOM에 다시 추가하면 노드가 이동함
// 현재 순서 : Apple - Banana - Orange
const $fruits = document.getElementById('fruits');
// 이미 존재하는 요소 노드 취득
const [$apple, $banana, ] = $fruits.children;
// 이미 존재하는 $apple 요소 노드를 #fruits 요소 노드의 마지막 노드로 이동
$fruits.appendChild($apple); // Banana - Orange - Apple
// 이미 존재하는 $banana 요소 노드를 #fruits요소의 마지막 자식 노드 앞으로 이동
$fruits.insertBefore($banana, $fruits.lastElementChild); // Orange - Banana - Apple
39.6.7 노드 복사
- Node.prototype.cloneNode메서드는 노드의 사본을 생성하여 반환함
- 매개변수 deep에 true를 전달하면 깊은복사, false를 전달하면 얕은복사 진행
const $deepClone = $fruits.cloneNode(true);
39.6.8 노드 교체
- Node.prototype.replaceChild메서드는 자신이 호출한 노드의 자식 노드를 다른 노드로 교체함
$fruit.replaceChild($newChild, $fruits.firstElementChild);
39.6.9 노드 삭제
- Node.prototype.removeChild(child)메서드는 child매개변수에 인수로 전달한 노드를 DOM에서 삭제함
$fruits.removeChild($fruits.lastElementChild);
39.7 어트리뷰트
39.7.1 어트리뷰트 노드와 attributes프로퍼티
- 요소 노드의 모든 어트리뷰트 노드는 요소 노드의 Element.prototype.attributes프로퍼티로 취득할 수 있음
- attributes프로퍼티는 읽기전용 프로퍼티며, NamedNodeMap객체를 반환
39.7.2 HTML 어트리뷰트 조작
- Element.prototype.getAttribute/setAttribute메서드를 사용하면 요소 노드에서 직접 HTML어트리뷰트 값을 취득하거나 변경할 수 있음
...
// value 어트리뷰트 값을 취득
const inputValue = $input.getAttribute('value');
// value 어트리뷰트 값을 변경
$input.setAttribute('value', 'foo');
...
- Element.prototype.hasAttribute(attributeName)메서드를 사용하여 특정 HTML어트리뷰트가 존재하는지 확인 가능
- Element.prototype.removeAttribute메서드를 사용하여 특정 HTML어트리뷰트 삭제
...
if ($input.hasAttribute('value')) {
$input.removeAttribute('value');
}
...
39.7.3 HTML어트리뷰트 vs DOM프로퍼티
- HTML어트리뷰트의 역할은 HTML요소의 초기상태를 지정하는 것
- DOM프로퍼티는 HTML어트리뷰트의 값을 초기값으로 가지고 있으며 참조와 변경 가능
1. 어트리뷰트 노드
- HTML어트리뷰트로 지정한 HTML요소의 초기 상태는 어트리뷰트 노드에서 관리함
2. DOM프로퍼티
- 사용자가 입력한 최신상태는 HTML어트리뷰트에 대응하는 요소 노드의 DOM프로퍼티가 관리함
3. HTML어트리뷰트와 DOM프로퍼티의 대응관계
- 대부분의 HTML어트리뷰트는 HTML어트리뷰트와 동일한 DOM프로퍼티와 1:1로 대응하지만, 언제나 1:1대응인 것도 아니고 반드시 이름이 일치하는 것도 아님
4. DOM프로퍼티 값의 타입
- getAttribute메서드로 취득한 어트리뷰트 값은 언제나 문자열임
- DOM프로퍼티로 취득한 최신 상태값은 문자열이 아닐 수도 있음
5. data어트리뷰트와 dataset프로퍼티
- data어트리뷰트와 dataset프로퍼티를 사용하면 HTML요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있음
39.8 스타일
### 39.8.1 인라인 스타일 조작
- HTMLElement.prototype.style프로퍼티는 인라인 스타일을 취득하거나 추가, 변경함
```
$div.style.color = 'blue';
// CSS프로퍼티는 케밥케이스를 따름
$div.style.backgroundColor = 'yellow';
// CSS프로퍼티를 그대로 사용하려면 대괄호 표기법을 사용
$div.style['background-color'] = 'yellow';
```
39.8.2 클래스 조작
className
- Element.prototype.className프로퍼티는 HTML요소의 class어트리뷰트 값을 취득하거나 변경함
- 문자열을 반환하므로 공백으로 구분된 여러 개의 클래스를 반환하는 경우 다루기 불편함
$box.className = $box.className.replace('red', 'blue');
classList
- Element.prototype.classList프로퍼티는 class어트리뷰트 정보를 담은 DOMTokenList객체를 반환함
console.log($classList);
// DOMTokenList(2) [length: 2, value: 'box blue', 0: 'box', 1: 'blue']
- DOMTokenList객체의 메서드
add : 인수로 전달한 1개 이상의 문자열을 class어트리뷰트 값으로 추가함
$box.classList.add('foo');
$box.classList.add('bar', 'baz');
remove : 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 class어트리뷰트에서 삭제함
$box.classList.remove('foo');
$box.classList.remove('bar', 'baz');
item(index) : 인수로 전달한 index에 해당하는 클래스를 class어트리뷰트에서 반환함
$box.classList.item(0); // -> 'box'
contains(className) : 인수로 전달한 문자열과 일치하는 클래스가 class어트리뷰트에 포함되어 있는지 확인
$box.classList.contains('box'); // -> true
replace(oldClassName, newClassName) : class어트리뷰트에서 첫번째 인수로 전달한 문자열을 두번째 인수로 전달한 문자열로 변경
$box.classList.replace('red', 'blue'); // -< class='box blue'
toggle(className[, force]) : class어트리뷰트에 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거, 존재하지 않으면 추가함
$box.classList.toggle('foo'); // -> class='box blue foo'
$box.classList.toggle('foo'); // -> class='box blue'
39.8.3 요소에 적용되어 있는 CSS스타일 참조
- window.getComputedStyle(element[, pseudo])메서드는 첫번째 인수로 전달한 요소 노드에 적용되어 있는 스타일을 CSSStyleDeclaration객체에 담아 반환
const computedStyle = window.getComputedStyle($box);
console.log(computedStyle.width); // 100px