📌 DOM
- DOM(Document Object Model): HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조
- 노드 객체들로 구성된 트리 자료구조
📌 노드
HTML 요소와 노드 객체
- HTML 요소: HTML 문서를 구성하는 개별적인 요소
- HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환됨
- HTML 문서는 HTML 요소들의 집합으로 이뤄지며, HTML 요소는 중첩 관계를 가짐
- HTML 요소 간에는 중첩 관계에 의해 계층적인 부자(parent-child) 관계가 형성됨
- HTML 요소 간의 부자 관계를 반영하여 HTML 문서의 구성 요소인 HTML 요소를 객체화한 모든 노드 객체들을 트리 자료 구조로 구성함
- 트리 자료구조: 부모 노드와 자식 노드로 구성되어 노드 간의 계층적 구조(부자, 형제 관계)를 표현하는 비선형 자료구조
노드 객체의 타입
노드 객체에는 총 12개의 타입이 있음
- 문서 노드
- DOM 트리의 최상위에 존재하는 루트 노드 (document 객체)
- 브라우저가 렌더링한 HTML 문서 전체를 가리키는 객체
- 전역 객체 window의 document 프로퍼티에 바인딩되어 있음
- 문서 노드는 DOM 트리의 루트 노드이므로 DOM 트리의 노드들에 접근하기 위한 진입점 역할을 담당함
- 요소 노드
- HTML 요소를 가리키는 객체
- HTML 요소 간의 중첩에 의해 부자 관계를 가지며, 부자 관계를 통해 정보를 구조화함
- 요소 노드는 문서의 구조를 표현함
- 어트리뷰트 노드
- HTML 요소의 어트리뷰트를 가리키는 객체
- 어트리뷰트가 지정된 HTML 요소의 요소 노드와 연결되어 있음
- 어트리뷰트 노드는 부모 노드와 연결되어 있지 않고 요소 노드에만 연결되어 있기 때문에 어트리뷰트를 참조하거나 변경하려면 요소 노드에 먼저 접근해야 함
- 텍스트 노드
- HTML 요소의 텍스트를 가리키는 객체
- 텍스트 노드는 문서 정보를 표현함
- 요소 노드의 자식 노드이며, 자식 노드를 가질 수 없는 리프 노드
- 텍스트 노드는 DOM 트리의 최종단이기 때문에 접근하려면 부모 노드인 요소 노드에 먼저 접근해야 함
노드 객체의 상속 구조
- 노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 가짐
- 노드 객체에는 노드 객체의 타입에 상관없이 모든 노드 객체가 공통으로 갖는 기능과 노드 타입에 따라 고유한 기능이 있음
- 노드 객체는 공통된 기능일수록 프로토타입 체인의 상위에, 개별적인 고유 기능일수록 프로토타입 체인의 하위에 프로토타입 체인을 구축하여 노드 객체에 필요한 프로퍼티와 메서드를 제공하는 상속 구조를 가짐
- DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공함
- DOM API를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있음
- 프론트엔드 개발자는 HTML을 DOM과 연관 지어 바라보아야 함
📌 요소 노드 취득
id를 이용한 요소 노드 취득 (document.getElementById)
- Document.prototype.getElementById: 인수로 전달한 id 어트리뷰트 값을 갖는 하나의 요소 노드를 탐색하여 반환하는 메서드
- id 값은 HTML 문서 내에서 유일한 값이어야 하며, 공백 문자로 구분하여 여러 개의 값을 가질 수 없음
- HTML 문서 내에 중복된 id 값을 갖는 요소가 있을 경우 인수로 전달된 id 값을 갖는 첫 번째 요소 노드만 반환함
- 인수로 전달된 id 값을 갖는 HTML 요소가 존재하지 않는 경우 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>
</ul>
<script>
// id값이 'banana'인 요소 노드를 탐색하여 반환
// 두 번째 li 요소가 파싱되어 생성된 요소 노드가 반환됨
const $elem = document.getElementById('banana');
// 취득한 요소 노드의 style.color 프로퍼티 값 변경
$elem.style.color = 'red';
</script>
</body>
</html>
태그 이름을 이용한 요소 노드 취득 (document.getElementsByTagName)
- Document.prototype/Element.prototype.getElementsByTagName: 인수로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환하는 메서드
- 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환함
- HTML 문서의 모든 요소 노드를 취득하려면 메서드의 인수로 '*'를 전달함
- Document.prototype.getElementsByTagName 메서드는 DOM 루트 노드인 문서 노드, 즉 document를 통해 호출하며 DOM 전체에서 요소 노드를 탐색하여 반환함
- Element.prototype.getElementsByTagName 메서드는 특정 요소 노드를 통해 호출하며, 특정 요소 노드의 자손 노드 중에서 요소 노드를 탐색하여 반환함
- 인수로 전달된 태그 이름을 갖는 HTML 요소가 존재하지 않는 경우 빈 HTMLCollection 객체를 반환함
<!DOCTYPE html>
<html>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
// 태그 이름이 li인 요소 노드를 모두 탐색하여 반환함
// 탐색된 요소 노드들은 HTMLCollection 객체에 담겨 반환됨
// HTMLCollection 객체는 유사 배열 객체이면서 이터러블
const $elems = document.getElementByTagName('li');
// 취득한 모든 요소 노드의 style.color 프로퍼티 값 변경
// HTMLCollection 객체를 배열로 변환하여 순회하며 color 프로퍼티 값을 변경함
[...$elems].forEach(el => { el.style.color = 'red'; });
</script>
</body>
</html>
class를 이용한 요소 노드 취득 (document.getElementsByClassName)
- Document.prototype/Element.prototype.getElementByClassName: 인수로 전달한 class 어트리뷰트 값을 갖는 모든 요소 노드들을 탐색하여 반환하는 메서드
- 인수로 전달할 class 값은 공백으로 구분하여 여러 개의 class을 지정할 수 있음
- 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환함
- Document.prototype.getElementsByClassName 메서드는 DOM의 루트 노드인 문서 노드, 즉 document를 통해 호출하며 DOM 전체에서 요소 노드를 탐색하여 반환함
- Element.prototype.getElementsByClassName 메서드는 특정 요소 노드를 통해 호출하며, 특정 요소 노드의 자손 노드 중에서 요소 노드를 탐색하여 반환함
- 인수로 전달된 class 값을 갖는 HTML 요소가 존재하지 않는 경우 빈 HTMLCollection 객체를 반환함
<!DOCTYPE html>
<html>
<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.getElementByClassName('fruit');
// 취득한 모든 요소의 CSS color 프로퍼티 값 변경
[...$elems].forEach(el => { el.style.color = 'red'; });
// class 값이 'fruit apple'인 요소 노드들 모두 탐색하여 HTMLCollection 객체에 담아 반환
const $apples = document.getElementByClassName('fruit apple');
// 취득한 모든 요소의 CSS color 프로퍼티 값 변경
[...$apples].forEach(el => { el.style.color = 'blue'; });
</script>
</body>
</html>
CSS 선택자를 이용한 요소 노드 취득 (document.querySelector/querySelectorAll)
- Document.prototype/Element.prototype.querySelector
- 인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환하는 메서드
- 인수로 전달한 CSS 선택자를 만족시키는 요소 노드가 여러 개인 경우 첫 번째 요소 노드만 반환함
- 인수로 전달한 CSS 선택자를 만족시키는 요소 노드가 존재하지 않는 경우 null을 반환함
- 인수로 전달한 CSS 선택자가 문법에 맞지 않는 경우 DOMException 에러 발생
- Document.prototype/Element.prototype.querySelectorAll
- 인수로 전달한 CSS 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환하는 메서드
- 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 NodeList 객체를 반환함
- 인수로 전달한 CSS 선택자를 만족시키는 요소가 존재하지 않는 경우 빈 NodeList 객체를 반환함
- 인수로 전달한 CSS 선택자가 문법에 맞지 않는 경우 DOMException 에러 발생
- HTML 문서의 모든 요소 노드를 취득하려면 메서드의 인수로 '*'를 전달함
- Document.prototype에 정의된 메서드는 DOM 루트 노드인 문서 노드, 즉 document를 통해 호출하며 DOM 전체에서 요소 노드를 탐색하여 반환함
- Element.prototype에 정의된 메서드는 특정 요소 노드를 통해 호출하며, 특정 요소 노드의 자손 노드 중에서 요소 노드를 탐색하여 반환함
- 장점: CSS 선택자 문법을 사용하여 좀 더 구체적인 조건으로 요소 노드를 취득할 수 있고 일관된 방식으로 요소 노드를 취득할 수 있음
- 단점: getElementById, getElementSBy*** 메서드보다 다소 느림
- id 어트리뷰트가 있는 요소 노드를 취득하는 경우에는 getElementById 메서드를 사용하고, 그 외의 경우에는 querySelector, querySelectorAll 메서드를 사용하는 것이 좋음
<!DOCTYPE html>
<html>
<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');
// 취득한 요소 노드의 style.color 프로퍼티 값 변경
$elem.style.color = 'red';
// ul 요소의 자식 요소인 li 요소를 모두 탐색하여 반환
const $elems = document.querySelectorAll('ul > li');
// 취득한 요소 노드들은 NodeList 객체에 담겨 반환됨
console.log($elems); // NodeList(3) [li.apple, li.banana, li.orange]
// 취득한 모든 요소 노드의 style.color 프로퍼티 값 변경
// NodeList는 forEach 메서드를 제공함
$elems.forEach(el => { el.style.color = 'blue'; });
</script>
</body>
</html>
툭종 요소 노드를 취득할 수 있는지 확인
- Element.prototype.matches: 인수로 전달한 CSS 선택자를 통해 특정 요소 노드를 취득할 수 있는지 확인하는 메서드
- Element.prototype.matches 메서드는 이벤트 위임을 사용할 때 유용함
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li class="apple">Apple</li>
<li class="banana">Banana</li>
<li class="orange">Orange</li>
</ul>
</body>
<script>
const $apple = document.querySelector('.apple');
// $apple 노드는 '#fruits > li.apple'로 취득할 수 있음
console.log($apple.matches('#fruits > li.apple')); // true
// $apple 노드는 '#fruits > li.banana'로 취득할 수 없음
console.log($apple.matches('#fruits > li.banana')); // false
</script>
</html>
HTMLCollection과 NodeList
- HTMLCollection
- 노드 객체의 상태 변화를 실시간으로 반영하는 살아있는 DOM 컬렉션 객체
- 유사 배열 객체이면서 이터러블
- HTMLCollection 객체는 실시간으로 노드 객체의 상태 변경을 반영하여 요소를 제거할 수 있가 때문에 HTMLCollection 객체를 for문으로 순회하면서 노드 객체의 상태를 변경해야 할 때 주의해야 함
- HTMLCollection 객체를 배열로 변환하여 사용하면 부작용을 줄일 수 있고 유용한 배열의 고차 함수를 사용할 수 있음
- NodeList
- 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체
- 유사 배열 객체이면서 이터러블
- 대부분의 경우 노드 객체의 상태 변경을 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 같이 실시간으로 노드 객체의 상태 변경을 반영하는 live 객체로 동작하므로 주의가 필요함
- 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장함
📌 노드 탐색
공백 텍스트 노드
- HTML 요소 사이의 스페이스, 탭, 줄바꿈(개행) 등의 공백 문자는 텍스트 노드를 생성함
- 노드를 탐색할 때는 공백 문자가 생성한 공백 텍스트 노드에 주의해야 함
자식 노드 탐색
프로퍼티 | 설명 |
Node.prototype.childNodes | 자식 노드를 모두 탐색하여 DOM 컬렉션 객체인 NodeList에 담아 반환함 childeNodes 프로퍼티가 반환한 NodeList에는 요소 노드뿐만 아니라 텍스트 노드도 포함되어 있을 수 있음 |
Element.prototype.children | 자식 노드 중에서 요소 노드만 모두 탐색하여 DOM 컬렉션 객체인 HTMLCollection에 담아 반환함 children 프로퍼티가 반환한 HTMLCollection에는 텍스트 노드가 포함되지 않음 |
Node.prototype.firstChild | 첫번째 자식 노드를 반환함 |
Node.prototype.nextChild | 마지막 자식 노드를 반환함 |
Element.prototype.firstElementChild | 첫번째 자식 요소 노드를 반환함 |
Element.prototype.nextElementChild | 마지막 자식 요소 노드를 반환함 |
자식 노드 존재 확인
- Node.prototype.hasChildNodes: 자식 노드가 존재하는지 확인하는 메서드
- 자식 노드의 존재 여부를 불리언 값으로 반환함
- 텍스트 노드를 포함하여 자식 노드의 존재를 확인함
- 자식 노드 중에 텍스트 노드가 아닌 요소 노드가 존재하는지 확인하려면 children.length 또는 Element 인터페이스의 childElementCount 프로퍼티를 사용함
요소 노드의 텍스트 노드 탐색
- 요소 노드의 텍스트 노드는 firstChild 프로퍼티로 접근할 수 있음
- firstChild 프로퍼티는 첫 번째 자식 노드를 반환하며 반환한 노드는 텍스트 노드이거나 요소 노드임
부모 노드 탐색
- Node.prototype.parentNode: 부모 노드를 탐색할 때 사용하는 프로퍼티
- 텍스트 노드는 DOM 트리의 최종단 노드인 리프 노드이므로 부모 노드가 텍스트 노드인 경우는 없음
형제 노드 탐색
프로퍼티 | 설명 |
Node.prototype.previousSibling | 부모 노드가 같은 형제 노드 중에서 자신의 이전 형제 노드를 탐색하여 반환함 |
Node.prototype.nextSibling | 부모 노드가 같은 형제 노드 중에서 자신의 다음 형제 노드를 탐색하여 반환함 |
Element.prototype.previousElementSibling | 부모 노드가 같은 형제 노드 중에서 자신의 이전 형제 요소 노드를 탐색하여 반환함 |
Element.prototype.nextElementSibling | 부모 노드가 같은 형제 노드 중에서 자신의 다음 형제 요소 노드를 탐색하여 반환함 |
📌 노드 정보 취득
프로퍼티 | 설명 |
Node.prototype.nodeType | 노드 객체의 타입을 나타내는 상수를 반환함 - Node.ELEMENT_NODE: 요소 노드 타입을 나타내는 상수 1 반환 - Node.TEXT_NODE: 텍스트 노드 타입을 나타내는 상수 3 반환 - Node.DOCUMENT_NODE: 문서 노드 타입을 나타내는 상수 9 반환 |
Node.prototype.nodeName | 노드의 이름을 문자열로 반환함 - 요소 노드: 대문자 문자열로 태그 이름 반환 - 텍스트 노드: 문자열 "#text" 반환 - 문서 노드: 문자열 "#document" 반환 |
📌 요소 노드의 텍스트 조작
nodeValue
- Node.prototype.nodeValue는 setter와 getter 모두 존재하는 접근자 프로퍼티 (참조와 할당 모두 가능)
- 노드 객체의 nodeValue 프로퍼티를 참조하면 노드 객체의 값(텍스트 노드의 텍스트)을 반환함
- 텍스트 노드가 아닌 노드의 nodeValue 프로퍼티를 참조하면 null을 반환함
- 요소 노드의 텍스트를 변경하려면 따로 처리가 필요함
- 텍스트를 변경할 요소 노드를 취득한 다음, 취득한 요소 노드의 텍스트 노드 탐색(firstChild 프로퍼티 사용)
- 탐색한 텍스트 노드의 nodeValue 프로퍼티를 사용해 텍스트 노드의 값 변경
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello</div>
</body>
<script>
// 문서 노드의 nodeValue 프로퍼티 참조
console.log(document.nodeValue); // null
// 요소 노드의 nodeValue 프로퍼티 참조
const $foo = document.getElementById('foo');
console.log($foo.nodeValue); // null
// 텍스트 노드의 nodeValue 프로퍼티 참조
const $textNode = $foo.firstChild;
console.log($textNode.nodeValue); // Hello
</script>
</html>
textContent
- Node.prototype.textContent는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경함
- 요소 노드의 textContent 프로퍼티를 참조하면 요소 노드의 콘텐츠 영역 내의 텍스트를 모두 반환함
이때, HTML 마크업은 파싱되지 않음 - 요소 노드의 textContent 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가됨
- textContent 프로퍼티와 유사한 동작을 하는 innerText 프로퍼티가 있지만, innerText 프로퍼티는 CSS를 고려해야 해 textContent 프로퍼티보다 느려 사용하지 않는 것이 좋음
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// #foo 요소 노드의 텍스트를 모두 취득함 (이때 HTML 마크업은 무시됨)
console.log(document.getElementById('foo').textContent); // Hello world!
</script>
</html>
📌 DOM 조작
- DOM 조작: 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것
- DOM 조작에 의해 DOM에 변화가 생기면 리플로우와 리페인트가 발생하기 때문에 성능에 영향을 줌
innerHTML
- Element.prototype.innerHTML: 요소 노드의 HTML 마크업을 취득하거나 변경하는 프로퍼티
- setter와 getter 모두 존재하는 접근자 프로퍼티임
- 요소 노드의 innerHTML 프로퍼티를 참조하면 요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환함
(HTML 마크업이 포함된 문자열을 그대로 반환) - 요소 노드의 innerHTML 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열에 포함되어 있는 HTML 마크업이 렌더링 엔진에 의해 파싱되어 요소 노드의 자식 노드로 DOM에 반영됨
- 장점:
- 구현이 간단하고 직관적임 - 단점:
- 크로스 사이트 스크립팅 공격에 취약함
- 요소 노드의 innerHTML 프로퍼티에 HTML 마크업 문자열을 할당하는 경우 유지되어도 좋은 기존의 자식 노드까지 모두 제거하고 다시 처음부터 새롭게 자식 노드를 생성하여 DOM에 반영함
- 새로운 요소를 삽입할 때 삽입될 위치를 지정할 수 없음
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// #foo 요소의 콘텐츠 영역 내의 HTML 마크업을 문자열로 취득함
console.log(document.getElementById('foo').innerHTML);
// "Hello <span>world!</span>"
</script>
</html>
insertAdjacentHTML 메서드
- Element.prototype.insertAdjacentHTML(position, DOMString): 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입하는 메서드
- insertAdjacentHTML 메서드는 두 번째 인수로 전달한 HTML 마크업 문자열(DOMString)을 차싱하고 그 결과로 생성된 노드를 첫 번째 인수로 전달한 위치(position)에 삽입하여 DOM에 반영함
- position: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
- 장점: 기존 요소에는 영향을 주지 않고 새롭게 삽입될 요소만을 파싱하여 자식 요소로 추가하기 때문에 innerHTML 프로퍼티보다 효율적이고 빠름
- 단점: HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립팅 공격에 취약함
<!DOCTYPE html>
<html>
<body>
<!-- beforebegin -->
<div id="foo">
<!-- afterbegin -->
text
<!-- beforeend -->
</div>
<!-- afterend -->
</body>
<script>
const $foo = document.getElementById('foo');
$foo.insertAdjacentHTML('beforebegin', '<p>beforebegin</p>');
$foo.insertAdjacentHTML('afterbegin', '<p>afterbegin</p>');
$foo.insertAdjacentHTML('beforeend', '<p>beforeend</p>');
$foo.insertAdjacentHTML('afterend', '<p>afterend</p>');
</script>
</html>
노드 생성과 추가
- 요소 노드 생성
- Document.prototype.createElement(tagName): 요소 노드를 생성하여 반환하는 메서드
- 매개변수 tagName에는 태그 이름을 나타내는 문자열을 인수로 전달함
- createElement 메서드는 요소 노드를 생성할 뿐 DOM에 추가하지는 않음
- createElement 메서드로 생성한 요소 노드는 아무런 자식 노드를 갖고 있지 않음
- 텍스트 노드 생성
- Document.prototype.createTextNode(text): 텍스트 노드를 생성하여 반환하는 메서드
- 매개변수 text에는 텍스트 노드의 값으로 사용할 문자열을 인수로 전달함
- createTextNode 메서드는 텍스트 노드를 생성할 뿐 요소 노드에 추가하지는 않음
- 텍스트 노드를 요소 노드의 자식 노드로 추가
- Node.prototype.appendChild(childNode): 매개변수 childNode에게 인수로 전달할 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가하는 메서드
- appendChild 메서드의 인수로 createTextNode 메서드로 생성한 텍스트 노드를 전달하면 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 텍스트 노드가 추가됨
- 요소 노드를 DOM에 추가
- appendChild 메서드를 사용해 텍스트 노드와 부자 관계로 연결한 요소 노드를 DOM에 있는 요소 노드의 마지막 자식 요소로 추가함
- DOM에 요소가 추가되면서 DOM이 변경되고, 이때 리플로우와 리페인트가 실행됨
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
</body>
<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>
</html>
복수의 노드 생성과 추가
- 기존 DOM에 요소 노드를 반복하여 추가하는 것은 리플로우와 리페인트를 계속 유발하기 때문에 비효율적임
- 여러 개의 요소 노드를 DOM에 추가하는 경우 DOM 변경을 한 번으로 줄일 수 있는 DocumentFragment 노드를 사용하는 것이 더 효율적임
- DocumentFragment: 문서, 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종, 부모 노드가 없어 기존 DOM과는 별도로 존재함
- Document.prototype.createDocumentFragment: 비어있는 DocumentFragment 노드를 생성하여 반환하는 메서드
- DocumentFragment 노드는 자식 노드들의 부모 노드로서 별도의 서브 DOM을 구성하여 기존 DOM에 추가하기 위한 용도로 사용함
- DocumentFragment 노드는 기존 DOM과 별도로 존재하므로 DocumentFragment 노드에 자식 노드를 추가해도 기존 DOM에는 어떠한 변경도 발생하지 않으며, DocumentFragment 노드를 DOM에 추가하면 자신은 제거되고 자신의 자식 노드만 DOM에 추가됨
<!DOCTYPE html>
<html>
<body>
<ul id="fruits"></ul>
</body>
<script>
const $fruits = document.getElementById('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);
});
// 5. DocumentFragment 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($fragment);
</script>
</html>
노드 삽입
- 마지막 노드로 추가
- Node.prototype.appendChild: 인수로 전달받은 노드를 자신을 호출한 노드의 마지막 자식 노드로 DOM에 추가하는 메서드
- 노드를 추가할 위치를 지정할 수 없고 언제나 마지막 자식 노드로 추가함
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
</ul>
</body>
<script>
// 요소 노드 생성
const $li = document.createElement('li');
// 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
$li.appendChild(document.createTextNode('Orange'));
// $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
document.getElementById('fruits').appendChild($li);
// 결과
// Apple - Banana - Orange
</script>
</html>
- 지정한 위치에 노드 삽입
- Node.prototype.insertBefore(newNode, childNode): 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입하는 메서드
- 두 번째 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드여야 함 (아닐 시 DOMException 에러 발생)
- 두 번째 인수로 전달받은 노드가 null이면 첫 번째 인수로 전달받은 노드를 insertBefore 메서드를 호출한 노드의 마지막 자식 노드로 추가함
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 요소 노드 생성
const $li = document.createElement('li');
// 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
$li.appendChild(document.createTextNode('Orange'));
// $li 요소 노드를 #fruits 요소 노드의 마지막 자식 요소 앞에 삽입
$fruits.insertBefore($li, $fruits.lastElementChild);
// 결과
// Apple - Orange - Banana
</script>
</html>
노드 이동
- DOM에 이미 존재하는 노드를 appendChild 또는 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서 노드를 제거하고 새로운 위치에 노드를 추가함 -> 노드 이동
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
</ul>
</body>
<script>
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
</script>
</html>
노드 복사
- Node.prototype.cloneNode([deep: true | false]): 노드의 사본을 생성하여 반환하는 메서드
- 매개변수 deep에 true를 인수로 전달하면 노드를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성하고, false를 인수로 전달하거나 생략하면 노드를 얕은 복사하여 노드 자신만의 사본을 생성함
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
const $apple = $fruits.firstElementChild;
// $apple 요소를 얕은 복사하여 사본 생성. 텍스트 노드가 없는 사본이 생성됨
const $shallowClone = $apple.cloneNode();
// 사본 요소 노드를 #fruits 요소 노드의 마지막 노드로 추가
$fruits.appendChild($shallowClone);
// #fruits 요소를 깊은 복사하여 모든 자손 노드가 포함된 사본 생성
const $deepClone = $fruits.cloneNode(true);
// 사본 요소 노드를 #fruits 요소 노드의 마지막 노드로 추가
$fruits.appendChild($deepClone);
// 겱과
// Apple - Banana
// Apple - Banana
</script>
</html>
노드 교체
- Node.prototype.replaceChild(newChild, oldChild): 자신을 호출한 노드의 자식 노드를 다른 노드로 교체하는 메서드
- 첫 번째 매개변수 newChild에는 교체할 새로운 노드를 인수로 전달하고, 두 번째 매개변수 oldChild에는 이미 존재하는 교체될 노드를 인수로 전달함
- replaceChild 메서드는 자신을 호출한 노드의 자식 노드인 oldChild 노드를 newChild 노드로 교체함
(이때 oldChild 노드는 DOM에서 제거됨)
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 기존 노드와 교체할 요소 노드 생성
const $newChild = document.createElement('li');
$newChild.textContent = 'Banana';
// #fruits 요소 노드의 첫 번째 자식 요소 노드를 $newChild 요소 노드로 교체
$fruits.replaceChild($newChild, $fruits.firstElementChild);
// 결과
// Apple
</script>
</html>
노드 삭제
- Node.prototype.removeChild(child): child 매개변수에 인수로 전달한 노드를 DOM에서 삭제하는 메서드
- 인수로 전달한 노드는 removeChild 메서드를 호출한 노드의 자식 노드여야 함
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// #fruits 요소 노드의 마지막 요소를 DOM에서 삭제
$fruits.removeChild($fruits.lastElementChild);
// 결과
// Apple
</script>
</html>
📌 어트리뷰트
어트리뷰트 노드와 attributes 프로퍼티
- HTML 어트리뷰트는 HTML 요소의 시작 태그에 어트리뷰트 이름="어트리뷰트 값" 형식으로 정의함
- 글로벌 어트리뷰트와 이벤트 핸들러 어트리뷰트는 모든 HTML 요소에서 공통적으로 사용할 수 있지만 특정 HTML 요소에만 한정적으로 사용 가능한 어트리뷰트도 있음
- HTML 문서가 파싱될 때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결되고, 이때 HTML 어트리뷰트당 하나의 어트리뷰트 노드가 생성됨
- 요소 노드의 모든 노드 어트리뷰트 노드는 요소 노드의 Element.prototype.attributes 프로퍼티로 취득할 수 있음
- Element.prototype.attributes: 읽기 전용 접근자 프로퍼티로, 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체를 반환함
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="tester1">
<script>
// 요소 노드의 attribute 프로퍼티는 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴
// NamedNodeMap 객체를 반환함
const { attributes } = document.getElementById('user');
console.log(attributes);
// NamedNodeMap {0: id, 1: type, 2: value, id: id, type: type, value: value, length: 3}
// 어트리튜브 값 취득
console.log(attributes.id.value); // user
console.log(attributes.type.value); // text
console.log(attributes.value.value); // tester1
</script>
</body>
</html>
HTML 어트리뷰트 조작
- Element.prototype.getAttribute/setAttribute: attributes 프로퍼티를 통하지 않고 요소 노드에서 직접 HTML 어트리뷰트 값을 취득하거나 변경할 수 있는 메서드
- HTML 어트리뷰트 값을 참조하려면 getAttribute(attributeName) 메서드를, HTML 어트리뷰트 값을 변경하려면 setAttribute(attributeName, attributeValue) 메서드를 사용함
- 특정 HTML 어트리뷰트가 존재하는지 확인하려면 Element.prototype.hasAttribute(attributeName) 메서드를, 특정 HTML 어트리뷰트를 삭제하려면 Element.prototype.removeAttribute(attributeName) 메서드를 사용함
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="tester1">
<script>
const $input = document.getElementById('user');
// value 어트리뷰트 값 취득
const inputValue = $input.getAttribute('value');
console.log(inputValue); // tester1
// value 어트리뷰트 값 변경
$input.setAttribute('value', 'foo');
console.log($input.getAttribute('value')); // foo
// value 어트리뷰트의 존재 확인
if ($input.hasAttribute('value')) {
// value 어트리뷰트 삭제
$input.removeAttribute('value'));
}
// value 어트리뷰트 삭제됨 (false 출력)
console.log($input.hasAttribute('value')); // false
</script>
</body>
</html>
data 어트리뷰트와 dataset 프로퍼티
- data 어트리뷰트와 dataset 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있음
- data 어트리뷰트:
- data- 접두사 다음에 임의의 이름을 붙여 사용함
- data 어트리뷰트의 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있음
- dataset 프로퍼티:
- HTML 요소의 모든 data 어트리뷰트의 정보를 제공하는 DOMStringMap 객체를 반환함
- DOMStringMap 객체는 data 어트리뷰트의 data- 접두사 다음에 붙인 임의의 이름을 카멜 케이스로 변환한 프로퍼티를 가짐
- data 어트리뷰트의 data- 접두사 다음에 존재하지 않는 이름을 키로 사용하여 dataset 프로퍼티에 값을 할당하면 HTML 요소에 data 어트리뷰트가 추가됨
<!DOCTYPE html>
<html>
<body>
<ul class="users">
<li id="1" data-user-id="7621">Lee</li>
<li id="2" data-user-id="9524">Kim</li>
</ul>
<script>
const users = [...document.querySelector('.users').children;
// user-id가 '7621'인 요소 노드 취득
const user = users.find(el => el.dataset.userId === '7621');
// user-id가 '7621'인 요소 노드에 새로운 data 어트리뷰트 추가
user.dataset.role = 'admin';
console.log(user.dataset);
/*
DOMStringMap {userId: "7621", role: "admin"}
-> <li id="1" data-user-id="7621" data-role="admin">Lee</li>
*/
</script>
</body>
</html>
📌 HTML 어트리뷰트 vs. DOM 프로퍼티
- 요소 노드 객체에는 HTML 어트리뷰트에 대응하는 프로퍼티(DOM 프로퍼티)가 존재함
- DOM 프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 갖고 있음
- HTML 어트리뷰트의 역할: HTML 요소의 초기 상태를 지정하는 것
즉, HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며, 이는 변하지 않음 - 요소 노드는 2개의 상태(state), 즉 초기 상태와 최신 상태를 관리해야 함
초기 상태: HTML 어트리뷰트로 지정한 어트리뷰트 값
최신 상태: 사용자의 입력에 의해 변경된 어트리뷰트 값 - 요소 노드의 초기 상태는 어트리뷰트 노드가, 최신 상태는 DOM 프로퍼티가 관리함
- getAttribute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이지만 DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수도 있음
어트리뷰트 노드
- HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태는 어트리뷰트 노드에서 관리함
- 사용자의 입력에 의해 상태가 변경되어도 변하지 않고 HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태를 그대로 유지함
- 어트리뷰트 노드가 관리하는 초기 상태 값을 취득하거나 변경하려면 getAttribute/setAttribute 메서드를 사용해야 함
DOM 프로퍼티
- 사용자가 입력한 최신 상태는 HTML 어트리뷰트에 대응하는 요소 노드의 DOM 프로퍼티가 관리함
- DOM 프로퍼티는 사용자의 입력에 의한 상태 변화에 반응하여 언제나 최신 상태를 유지함
- DOM 프로퍼티에 값을 할당하는 것은 HTML 요소의 최신 상태 값을 변경하는 것을 의미함 (사용자가 상태를 변경하는 행위와 같음)
- 사용자 입력에 의한 상태 변화와 관계있는 DOM 프로퍼티만 최신 상태 값을 관리하며, 그 외의 사용자 입력에 의한 상태 변화와 관계없는 어트리뷰트와 DOM 프로퍼티는 항상 동일한 값으로 연동함
HTML 어트리뷰트와 DOM 프로퍼티의 대응 관계
- 대부분의 HTML 어트리뷰트는 HTML 어트리뷰트 이름과 동일한 DOM 프로퍼티와 1:1로 대응함
- 그러나 HTML 어트리뷰트와 DOM 프로퍼티가 언제나 1:1로 대응하는 것은 아님
- id 어트리뷰트와 id 프로퍼티는 1:1 대응하며, 동일한 값으로 연동함
- input 요소의 value 어트리뷰트는 value 프로퍼티와 1:1 대응하지만 value 어트리뷰트는 초기 상태를, value 프로퍼티는 최신 상태를 가짐
- class 어트리뷰트는 className, classList 프로퍼티와 대응함
- for 어트리뷰트는 htmlFor 프로퍼티와 1:1 대응함
- td 요소의 colspan 어트리뷰트는 대응하는 프로퍼티가 존재하지 않음
- textContent 프로퍼티는 대응하는 어트리뷰트가 존재하지 않음
- 어트리뷰트 이름은 대소문자를 구별하지 않지만 대응하는 프로퍼티 키는 카멜 케이스를 따름 (maxlength -> maxLength)
📌 스타일
인라인 스타일 조작
- HTMLElement.prototype.style: 요소 노드의 인라인 스타일을 취득하거나 추가 또는 변경하는 프로퍼티. 읽기와 쓰기가 가능한 접근자 프로퍼티임
- style 프로퍼티를 참조하면 CSSStyleDeclaration 타입의 객체를 반환함
- CSSStyleDeclaration 객체는 다양한 CSS 프로퍼티에 대응하는 프로퍼티를 가지고 있으며, 이 프로퍼티에 값을 할당하면 해당 CSS 프로퍼티가 인라인 스타일로 HTML 요소에 추가되거나 변경됨
- CSS 프로퍼티는 케밥 케이스를 따르며, 이에 대응하는 CSSStyleDeclaration 객체의 프로퍼티는 카멜 케이스를 따름
- 단위 지정이 필요한 CSS 프로퍼티의 값은 반드시 단위를 지정해야 함
<!DOCTYPE html>
<html>
<body>
<div style="color: red">Hello World</div>
<script>
const $div = document.querySelector('div');
// 인라인 스타일 취득
console.log($div.style); // CSSStyleDeclaration { 0: "color", ... }
// 인라인 스타일 변경
$div.style.color = 'blue';
// 인라인 스타일 추가
$div.style.width = '200px';
$div.style.height = '100px';
$div.style.backgroundColor = 'yellow';
</script>
</body>
</html>
클래스 조작
- className
- Element.prototype.className: HTML 요소의 class 어트리뷰트 값을 취득하거나 변경하는 프로퍼티. 읽기와 쓰기가 가능한 접근자 프로퍼티임
- 요소 노드의 className 프로퍼티를 참조하면 class 어트리뷰트 값을 문자열로 반환함
- 요소 노드의 className 프로퍼티에 문자열을 할당하면 class 어트리뷰트 값을 할당한 문자열로 변경함
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px; height: 200px;
background-color: green;
}
.red { color: red; }
.blue { color: blue; }
</style>
</head>
<body>
<div style="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 객체를 반환하는 프로퍼티
- DOMTokenList 객체는 class 어트리뷰트의 정보를 나타내는 컬렉션 객체로서, 유사 배열 객체이면서 이터러블임
DOMTokenList 객체 제공 메서드 | 설명 |
add(...className) | 인수로 전달한 1개 이상의 문자열을 class 어트리뷰트 값으로 추가함 |
remove(...className) | 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 class 어트리뷰트에서 삭제함 일치하는 클래스가 없으면 에러 없이 무시됨 |
item(index) | 인수로 전달한 index에 해당하는 클래스를 class 어트리뷰트에서 반환함 |
constains(className) | 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 포함되어 있는지 확인함 |
replace(oldClassName, newClassName) | class 어트리뷰트에서 첫 번째 인수로 전달한 문자열을 두 번째 인수로 전달한 문자열로 변경함 |
toggle(className[, force]) | class 어트리뷰트에 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거하고, 존재하지 않으면 추가함 두 번째 인수로 불리언 값으로 평가되는 조건식을 전달할 수 있음 조건식의 평가 결과가 true이면 class 어트리뷰트에 첫 번째 인수로 전달받은 문자열을 강제로 추가하고, false이면 강제로 제거함 |
요소에 적용되어 있는 CSS 스타일 참조
- HTML 요소에 적용되어 있는 모든 CSS 스타일을 참조해야 할 경우 getComputedStyle 메서드를 사용함
- window.getComputedStyle(element[, pseudo]): 첫 번째 인수(element)로 전달한 요소 노드에 적용되어 있는 평가된 스타일을 CSSStyleDeclaration 객체에 담아 반환하는 메서드
- 평가된 스타일(computed style): 요소 노드에 적용되어 있는 모든 스타일이 조합되어 최종적으로 적용된 스타일
- getComputedStyle 메서드의 두 번째 인수(pseudo)로 :before, :after와 같은 의사 요소를 지정하는 문자열을 전달할 수 있음
📌 DOM 표준w
- HTML과 DOM 표준은 W3C(World Wide Web Consortium)과 WHATWG(Web Hypertext Application Technology Working Group)이라는 두 단체가 협력하며 공통된 표준을 만들어옴
- 2018년 4월부터 구글, 애플, 마이크로소프트, 모질라로 구성된 4개의 주류 브라우저 벤더사가 주도하는 WHATWG이 단일 표준을 내놓기로 두 단체가 합의함
참고문헌 및 출처 : 모던 자바스크립트 Deep Dive (이웅모)
'JavaScript > 모던 자바스크립트 딥다이브' 카테고리의 다른 글
[JS] 타이머 (0) | 2024.09.20 |
---|---|
[JS] 이벤트 (5) | 2024.09.07 |
[JS] 브라우저의 렌더링 과정 (0) | 2024.08.19 |
[JS] Map (0) | 2024.08.18 |
[JS] Set (0) | 2024.08.18 |
댓글