DOM은 HTML문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조다.
HTML 요소는 HTML 문서를 구성하는 개별적인 요소를 의미한다.
HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소로 노드 객체로 변환된다. 이때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로, HTML 요소의 텍스트 콘텐츠는 텍스트 노드로 변환된다.
트리 자료구조는 노드들의 계층 구조로 이뤄짐.
노드 객체들로 구성된 트리 자료구조를 DOM이라고 함
문서 노드(document node)
DOM 트리의 최상위에 존재하는 루트 노드로서 document 객체를 가리킴.
요소 노드(element node)
HTML 요소를 가리키는 객체. 문서의 구조를 표현한다. HTML 요소 간의 중첩에 의해 부자 관계를 가지며, 이 부자 관계를 통해 정보를 구조화함.
어트리뷰트 노드(attribute node)
HTML 요소의 attribute를 가리키는 객체. attribute가 지정된 HTML 요소의 요소 노드와 연결되어 있음. 단, 요소 노드는 부모 노드와 연결되어 있지만 어트리뷰트 노드는 부모 노드 와 연결되어 있지 않고 요소 노드에만 연결되어있음!
텍스트 노드(text node)
HTML 요소의 텍스트를 가리키는 객체. 요소 노드의 자식노드이며 자식 노드를 가질 수 없는 리프노드(leaf node). 즉 텍스트 노드는 DOM 트리의 최종단. 텍스트 노드에 접근하려면 먼저 부모 노드인 요소노드에 접근해야함.
DOM을 구성하는 노드 객체는 ECMAScript 사양에 정의된 표준 빌트인 객체가 아니라 브라우저 환경에서 추가적으로 제공하는 호스트 객체(host objects).
하지만 노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 갖는다!
Object - EventTarget - Node - (Document, Element, Attr, CharacterData) - ... - ...
ex) Input 노드 객체의 상속 구조
Object - EventTarget - Node - Element - HTMLElement - HTMLInputElement
DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API(Application Programming Interface)로 제공함. 이 DOM API를 통해 HTML 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.
HTML의 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야함!
const something = document.getElementById('banana');
something.style.color = 'red';
인수로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환함. DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다!
🔖 HTMLCollection 객체는 유사 배열 객체이면서 이터러블.
const something = document.getElementsByTagName('li');
HTML 문서의 모든 요소 노드를 취득하려면 메서드의 인수로 '*'를 전달한다.
// 모든 요소 노드를 탐색하여 반환
const all = document.getElementByTagName('*');
여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollectioin 객체를 반환
const elems = document.getElementsByClassName('fruit');
CSS 선택자는 스타일을 적용하고자 하는 HTML 요소를 특정할 때 사용하는 문법.
/* 전체 선택자 : 모든 요소 선택*/
*{...}
/* 태그 선택자 : 모든 p 태그 요소 선택*/
p{...}
/* id 선택자 : id 값이 'foo'인 요소 선택*/
#foo{...}
/* 자식 선택자 : div 요소의 자식 요소 중 p 요소를 모두 선택*/
div > p {...}
/* 일반 형재 선택자 : p 요소의 형제 요소 중에 p 요소 뒤에 위치하는 ul 요소를 모두 선택*/
p ~ ul {...}
/* 가상 클래스 선택자 : hover 상태인 a 요소를 모두 선택*/
a:hover {...}
/* 가상 요소 선택자 : p 요소의 콘텐츠 앞에 위치하는 공간을 선택. 일반적으로 content 프로퍼티와 함께 사용된다.*/
p::before{...}
인수로 전달한 CSS 선택자를 만족시키는 요소 노드가 여러 개인 경우 첫번째 요소 노드만 반환함.
document.querySelector('banana');
모든 요소 노드 반환. DOM 컬렉션 객체인 NodeList 객체를 반환한다. 유사 배열 객체이면서 이터러블.
Element.prototype.matches 메서드
<body>
<ul id ="fruits">
<li class = "apple">Apple</li>
</ul>
</body>
<script>
const apple = document.querySelector('.apple');
// apple 노드는 #fruits > li.apple로 취득할 수 있다.
console.log(apple.matches('#fruits > li.apple')); //true
</script>
HTMLCollection과 NodeList의 중요한 특징은 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체라는 것.
HTMLCollection은 언제나 live 객체로 동작한다. 단 NodeList는 대부분의 경우 노드 객체의 상태 변화를 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하지만 경우에 따라 live 객체로 동작할 때가 있음.
getElementsByTagName, getElementsByClassName 메서드가 반환한다. 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 DOM 컬렉션 객체.
QuerySelectorAll이 반환.
NodeList.prototype.forEach 메서드를 상속받아 사용할 수 있음.
노드 객체의 상태 변경과 상과없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장함.
ex) 스프레드 문법, Array.from 메서드르 사용하여 변환 가능
요소 노드를 취득한 다음, 취득한 요소 노드를 기점으로 DOM 트리의 노드를 옮겨 다니며 부모, 형제, 자식 노드 등을 탐색해야할 때가 있음.
👉 DOM 트리 상의 노드를 탐색할 수 있도록 Node, Element 인터페이스는 트리 탐색 프로퍼티를 제공한다.
Node.prototype 가 제공: parentNode, previoustSibling, firstChild, childNodes 프로퍼티
Element.prototype 가 제공 : previousElementSibling, nextElementSibling, children 프로퍼티
HTML요소 사이의 스페이스, 탭, 줄바꿈 등의 공백 문자는 공백 테스트 노드를 생성 (DOM트리 내에 생성된다.)
자식 노드를 탐색하기 위해 사용하는 노드 탐색 프로퍼티들
Node.prototype.childNodes
childNodes 프로퍼티가 반환한 NodeList에는 요소 노드뿐만 아니라 텍스트 노드도 포함되어 있을 수 있다.
Element.prototype.children
children 프로퍼티가 반환한 HTMLCollection에는 텍스트 노드가 포함되지 않는다.
Node.prototype.firstChild
텍스트 노드이거나 요소노드
Node.prototype.lastChild
텍스트 노드이거나 요소노드
Element.prototype.firstElementChild
요소노드만 반환
Element.prototype.lastElementChild
요소노드만 반환
ex)
<ul id ="fruits">
<li class ="apple">Apple</li>
<li class ="banana">Banana</li>
<li class ="orange">Orange</li>
</ul>
<script>
const fruites = document.getElementById('fruits');
console.log(fruits.childNodes);
// fruits요소의 모든 자식 노드 탐색 (텍스트 노드 포함)
// NodeList(7) [text, li.apple, text, li.banana, text, li.orange, text]]
console.log(fruits.children);
// fruits 요소의 모든 자식 노드를 탐색함 (요소 노드만 포함)
// HTMLCollection [li.apple, li.banana, li.orange]
console.log(fruits.firstChild); // #text
console.log(fruits.lastChild); // #text
console.log(fruits.firstElementChild); // li.apple
console.log(fruits.lastElementChild); // li.orange
</script>
👉 Node.prototype.hasChildNodes 메서드 사용
텍스트 노드를 포함하여 자식 노드의 존재를 확인
const fruits = document.getElementById('fruits');
console.lg(fruits.hasChildNodes); // true
👉 children, childElementCount
텍스트 노드가 아닌 요소 노드가 존재하는지 확인
console.log(fruits.children.length); // 0 => false
console.log(fruits.childElementCout); // 0 => false
요소노드의 텍스트 노드는 요소 노드의 자식 노드다. 따라서 요소 노드의 텍스트 노드는 firstChild 프로퍼티로 접근할 수 있다. firstChild 프로퍼티는 첫 번째 자식 노드를 반환. firstChild 프로퍼티가 반환한 노드는 텍스트 노드이거나 요소 노드.
console.log(document.getElementsById('foo').firstChild); // #text
Node.prototype.paraneNode 사용
텍스트 노드는 DOM 트리의 최종단 노드인 리프 노드이므로 부모 노드가 텍스트 노드인 경우는 없음.
Node.prototype.previousSibling, Node.prototype.NextSibling, Element.prototype.previousElementSibling, Element.prototype.nextElementSibling 사용
어트리뷰트 노드는 요소 노드와 연결되어 있지만, 부모 노드가 같은 형제 노드가 아니기 때문에 반환되지 않음. previousElementSibling, nextElementSibling의 경우 요소 노드만 반환
Node.prototype.nodeType : 상수 반환
Node.ElEMENT_NODE : 요소 노드 타입을 나타내는 상수 1을 반환
Node.TEXT_NODE : 텍스트 노드 타입을 나타내는 상수 3반환
Node.DOCUMENT_NODE :문서 노드 타입을 나타내는 상수 9 반환
Node.prototype.nodeName
요소 노드 : 대문자 문자열로 태그 이름 ("UL" 등) 반환
텍스트 노드 : "#text" 반환
문서 노드 : "#document" 반환
Node.prototype.nodeValue 프로퍼티 : setter와 getter 모두 존재하는 접근자 프로퍼티. 즉 참조와 할당 모두 가능.
<div id = "foo">Hello</div>
<script>
const textNode = foo.firstNode;
console.log(textNode.nodeValue); // Hello
</script>
const textNode = document.getElementsById('foo').firstChild;
// nodeValue 프로퍼티를 사용하여 텍스트 노드의 값을 변경
textNode.nodeValue = 'World';
console.log(textNode.nodeValue); // World
Node.prototype.textContext 프로퍼티 : setter와 getter 모두 존재.
요소 노드의 콘텐츠 영역 내의 텍스트를 모두 반환. HTML 마크업은 무시된다.
<div id="foo">Hello <span> world!</span> </div>
<script>
// #foo 요소노드는 텍스트 노드가 아님.
console.log(document.getElementsById('foo').nodeValue); // null
// #foo 요소노드의 자식 노드인 텍스트 노드의 값을 취득함
console.log(document.getElementsById('foo').firstChild.nodeValue); // Hello
// span 요소 노드의 자식 노드인 텍스트 노드의 값을 취득함
console.log(document.getElementsById('foo').nodeValue); // world!
</script>
// 요소 노드의 콘텐츠 영역에 다른 요소 노드가 없고 텍스트만 존재
<div id ="foo">Hello </div>
const foo = document.getElementById('foo');
console.log(foo.textContext === foo.firstChild.nodeValue); // true
textContext 는 HTML 마크업이 파싱되지 않는다는 특징이 있다.
<div id ="foo">Hello <span>world!</span></div>
<script>
// foo 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다.
document.getElementById('foo').textContext = 'Hi <span>there!</span>'.
</script>
🚨 textContext와 유사한 동작을 하는 innerText 프로퍼티가 있음. 사용하지 않는 것이 좋음.
Element.prototype.innerHTML 프로퍼티 : getter와 setter 모두 존재하는 접근자 프로퍼티.
textContent 프로퍼티를 참조하면 HTML마크업을 무시하고 텍스트만 반환하지만 innerHTML 프로퍼티는 HTML 마크업이 포함된 문자열을 그대로 반환함.
console.log(document.getElementById('foo').innerHTML);
// Hello <span>world</span>
✔️ 사용자로부터 입력받은 데이터(untrusted input data)를 그대로 innerHTML 프로퍼티에 할당하는 것은 크로스 사이트 스크립팅 공격(XSS : Cross-Site Scripting Attacks)에 취약하므로 위험함.
HTML 마크업 내에 자바스크립트 악성 코드가 포함되어 있다면 파싱 과정에서 그대로 실행될 가능성이 있기 때문!
HTML5는 innerHTML 프로퍼티로 삽입된 script 요소 내의 자바스크립트 코드를 실행하지 않지만, script 없이도 사이트 스크립트 공격은 가능.
ex)
// 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행되도록 함.
document.getElementsById('foo').innerHTML = `<img src ="x">`;
🔖 HTML 새니티제이션 (HTML sanitization)
HTML 새니티제이션은 사용자로부터 입력받은 데이터에 의해 발생할 수 있는 크로스 사이트 스크립팅 공격 예방하기 위해 잠재적 위험 제거하는 기능. DOMPurify 라이브러리를 사용하는 것을 권장
DOMPurify.sanitize('<img src ="x">');
// => <img src="x" />
✔️ innerHTML 또다른 단점 : 요소 노드의 innerHTML 프로퍼티에 HTML 마크업 문자열을 할당하는 경우 요소 노드의 모든 자식 노드를 제거하고 할당한 HTML 마크업 문자열을 파싱하여 DOM을 변경.
fruits.innerHTML += '<li class="banana">banana</li>'
위의 코드는 기존에 존재한 코드를 제거하고 다시 처음부터 새롭게 자식 노드를 생성하여 DOM에 반영함. 효율적.
👉 innerHTML은 복잡하지 않은 요소를 새롭게 추가할 때 유용하지만 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입해야 할 때는 사용하지 않는 것이 좋음
Elemnt.prototype.insertAdjacentHTML(position, DOMString) 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.
첫번째 인수로 전달할 수 있는 문자열 : 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
// beforebegin
<div id ="foo">
// afterbegin
text
// beforeend
</div>
// afterend
<script>
foo.insertAdjacentHTML('beforebegin', '<p>hi</p>');
</script>
👉 기존 요소에 영향 주지 않고 새롭게 삽입될 요소만을 파싱하여 자식 요소로 추가하므로 innerHTML 프로퍼티보다 효율적이고 빠름. 단, innerHTML 프로퍼티와 마찬가지로 HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립칭 공격에 취약하다는 점은 동일
// 텍스트 노드를 생성하여 요소 노드의 자식 노드로 추가
li.appendChild(document.createTextNode('Banana'));
// li 요소 노드에 자식 노드가 하나도 없는 위 코드와 동일하게 동작
li.textContext = 'Banana';
DocumentFragment 노드를 사용하는 것이 효율적.
👉 문서, 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종으로 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있음. (자식 노드를 추가한다고 하여 기존 DOM에 어떠한 변경도 발생하지 않음)
const fruits = document.getElementsById('fruits');
// DocumentFragment 노드 생성
const fragment = document.createDocumentFragment();
['Apple', 'Banana', 'Orange'].forEach(text => {
// 1. 요소 노드 생성
const li = document.createElement('li');
// 2. 텍스트 노드 생성
const textNode = document.createTextNode(text);
// 3. 텍스트 노드를 li 요소 노드의 자식 노드로 추가
li.appendChild(textNode);
// 4. li 요소 노드를 DocumentFragment 노드의 마지막 자식 노드로 추가
fragment.appendChild(li);
});;
// DocumentGfament 노드를 fruits 요소 노드의 마지막 자식 노드로 추가
fruits.appendChild(fragment);
위를 통해 실제로 DOM 변경이 발생하는 것은 한 번뿐이며 리플로우와 리페인트도 한 번만 실행된다. 따라서 여러 개의 요소 노드를 DOM에 추가하는 경우 DocumentFragment노드 사용하는 것이 더 효율적!
const fruits = document.getElementsById('fruits');
const li = document.createElement('li');
// 텍스트 노드를 li 요소 노드의 마지막 자식 노드로 추가
li.appendChild(document.createTextNode('Orange'));
// li 요소 노드를 fruits 요소 노드의 마지막 자식 요소 앞에 삽입
fruits.insertBefore(li, fruits.lastElementChild);
두 번째 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드이어야 함!
appendChild, insertBefore 메서드 사용
Node.prototype.cloneNode([deep : true | false]) 메서드는 노드의 사본을 생성하여 반환.
// 텍스트 노드가 없는 사본이 생성됨
const shallowClone = apple.cloneNode();
shallowClone.textContent = 'Banana';
fruits.appendChild(shallowClone);
// 모든 자손 노드가 포함된 사본을 생성
const deepClone = fruits.cloneNode(true);
fruits.appendChild(deepClone);
Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체.
const fruits = document.getElementById('fruits');
const newChild = document.createElement('li');
newChild.textContext = 'Banana';
// fruits 요소 노드의 첫번째 자식 요소 노드를 newChild 요소로 교체
fruits.relaceChild(newChild, fruits.firstElementChild);
Node.prototype.removeChild(child) 메서드는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제.
인수로 전달한 노드는 removeChild 메서드를 호출한 노드의 자식 노드이어야함.
// fruits 요소 노드의 마지막 요소를 DOM에서 삭제
fruits.removeChild(fruits.lastElementChild);