노드 탐색
요소 노드를 취득한 다음, 취득한 요소를 기점으로 DOM 트리의 노드를 옮겨 다니며 부모, 형제 , 자식 노드등을 탐색해야할때가 있다.
<ul id="fruits">
<li class="apple">Apple</li>
<li class="banana">Banana</li>
<li class="orange">Orange</li>
</ul>
ul#fruits 요소는 3개의 자식 요소를 가진다 따라서 ul#fruits 요소 노드를 취득한 다음 자식노드를 탐색하거나 자식중 하나를 탐색할 수 있다
이와같이 DOM 트리상 노드를 탐색할 수 있도록 Node,Element 인터페이스는 트리 탐색 프로퍼티를 제공해준다.
parentNode,previousSibling,firstChild,childNodes 프로퍼티는 Node.prototype이 제공하고 프로퍼티 키에 Element가 포함된 previousElementSibling,nextElementSibling 과 children 프로퍼티는 Element.prototype이 제공한다.
노드 탐색 프로퍼티는 모두 접근자 프로퍼티로 읽기 전용 접근자 프로퍼티이다.
공백 텍스트 노드
HTML 요소 사이의 스페이스, 탭, 줄바꿈 등의 공백 문자는 텍스트 노드를 생성한다. 이를 공백 텍스트 노드라고 한다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li class="apple">Apple</li>
<li class="banana">Banana</li>
<li class="orange">Orange</li>
</ul>
</body>
</html>
텍스트 에디터에서 HTML문서에 스페이스,탭,엔터 등을 입력하면 공백 문자가 추가된다.
HTML 문서의 공백 문자를 제거하면 공백 텍스트 노드를 생성하지는 않지만 가독성이 좋지 않은 방법이므로 권장하지는 않는다.
<ul id="fruits"><li
class="apple">Apple</li><li
class="banana">Banana</li><li
class="orange">Orange</li></ul>
자식 노드 탐색
자식 노드를 탐색하기 위해서는 아래 프로퍼티들을 사용할 수 있다.
1. Node.prototype.childNodes
자식 노드를 모두 탐색하여 NodeList에 담아 바노한하며 텍스트 노드도 포함될 수 있다.
2. Element.prototype.children
자식 노드 중에서 요소 노드만 모두 탐색하여 HTMLCollection에 담아 반환하며 텍스트 노드가 포함되지 않는다.
3. Node.prototype.firstChild
첫번째 자식 요소를 반환하며 텍스트 노드이거나 요소 노드이다.
4. Node.prototype.lastChild
마지막 자식 노드를 반환하며 텍스트 노드이거나 요소 노드이다.
5. Element.prototype.firstElementChild
첫번째 자식 요소를 반환하며 요소 노드만 반환한다.
6. Element.prototype.lastElementChild
마지막 자식 요소를 반환하며 요소 노드만 반한다.
<!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>
// 노드 탐색의 기점이 되는 #fruits 요소 노드를 취득한다.
const $fruits = document.getElementById('fruits');
// #fruits 요소의 모든 자식 노드를 탐색한다.
// childNodes 프로퍼티가 반환한 NodeList에는 요소 노드뿐만 아니라 텍스트 노드도 포함되어 있다.
console.log($fruits.childNodes);
// NodeList(7) [text, li.apple, text, li.banana, text, li.orange, text]
// #fruits 요소의 모든 자식 노드를 탐색한다.
// children 프로퍼티가 반환한 HTMLCollection에는 요소 노드만 포함되어 있다.
console.log($fruits.children);
// HTMLCollection(3) [li.apple, li.banana, li.orange]
// #fruits 요소의 첫 번째 자식 노드를 탐색한다.
// firstChild 프로퍼티는 텍스트 노드를 반환할 수도 있다.
console.log($fruits.firstChild); // #text
// #fruits 요소의 마지막 자식 노드를 탐색한다.
// lastChild 프로퍼티는 텍스트 노드를 반환할 수도 있다.
console.log($fruits.lastChild); // #text
// #fruits 요소의 첫 번째 자식 노드를 탐색한다.
// firstElementChild 프로퍼티는 요소 노드만 반환한다.
console.log($fruits.firstElementChild); // li.apple
// #fruits 요소의 마지막 자식 노드를 탐색한다.
// lastElementChild 프로퍼티는 요소 노드만 반환한다.
console.log($fruits.lastElementChild); // li.orange
</script>
</html>
자식 노드 존재 확인
Node.protoytpe.hasChildNodes 메서드를 사용하면 자식노드의 존재 유무를 확인할 수 있다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
</ul>
</body>
<script>
// 노드 탐색의 기점이 되는 #fruits 요소 노드를 취득한다.
const $fruits = document.getElementById('fruits');
// #fruits 요소에 자식 노드가 존재하는지 확인한다.
// hasChildNodes 메서드는 텍스트 노드를 포함하여 자식 노드의 존재를 확인한다.
console.log($fruits.hasChildNodes()); // true
</script>
</html>
자식 노드 중에서 텍스트 노드가 아닌 요소 노드가 존재하는지 확인하려면 children.length 혹은 childElementCount 프로퍼티를 사용하면 된다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
</ul>
</body>
<script>
// 노드 탐색의 기점이 되는 #fruits 요소 노드를 취득한다.
const $fruits = document.getElementById('fruits');
// hasChildNodes 메서드는 텍스트 노드를 포함하여 자식 노드의 존재를 확인한다.
console.log($fruits.hasChildNodes()); // true
// 자식 노드 중에 텍스트 노드가 아닌 요소 노드가 존재하는지는 확인한다.
console.log(!!$fruits.children.length); // 0 -> false
// 자식 노드 중에 텍스트 노드가 아닌 요소 노드가 존재하는지는 확인한다.
console.log(!!$fruits.childElementCount); // 0 -> false
</script>
</html>
요소 노드의 텍스트 노드 검색
요소노드의 텍스트 노드는 요소 노드의 자식 노드이므로 firstChild 프로퍼티로 접근할 수 있다.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello</div>
<script>
// 요소 노드의 텍스트 노드는 firstChild 프로퍼티로 접근할 수 있다.
console.log(document.getElementById('foo').firstChild); // #text
</script>
</body>
</html>
부모 노드 탐색
부모 노드를 탐색하려면 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>
</body>
<script>
// 노드 탐색의 기점이 되는 .banana 요소 노드를 취득한다.
const $banana = document.querySelector('.banana');
// .banana 요소 노드의 부모 노드를 탐색한다.
console.log($banana.parentNode); // ul#fruits
</script>
</html>
형제 노드 탐색
부모 노드가 같은 형제 노드를 탐색하려면 아래와 같은 노드 탐색 프로퍼티를 사용할 수 있다.
1. Node.prototype.previousSibloing
형제 노드 중에서 자신의 이전 형제 노드를 반환하며 요소노드와 텍스트노드 둘다 포함된다.
2. Node.prototype.nextSibling
형제 노드 중에서 자신의 다음 형제 노드를 반혼하며 요소노드와 텍스트 노드 둘다 포함된다.
3. Element.prototype.previousElementSibling
형제노드 중에서 자신의 이전 형제 노드를 반환하며 요소 노드만 포함된다.
4. Element.prototype.nextElementSibling
형제 노드 중에서 자신의 다음 형제 노드를 반환하며 요소 노드만 포함된다.
<!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>
// 노드 탐색의 기점이 되는 #fruits 요소 노드를 취득한다.
const $fruits = document.getElementById('fruits');
// #fruits 요소의 첫 번째 자식 노드를 탐색한다.
// firstChild 프로퍼티는 요소 노드뿐만 아니라 텍스트 노드를 반환할 수도 있다.
const { firstChild } = $fruits;
console.log(firstChild); // #text
// #fruits 요소의 첫 번째 자식 노드(텍스트 노드)의 다음 형제 노드를 탐색한다.
// nextSibling 프로퍼티는 요소 노드뿐만 아니라 텍스트 노드를 반환할 수도 있다.
const { nextSibling } = firstChild;
console.log(nextSibling); // li.apple
// li.apple 요소의 이전 형제 노드를 탐색한다.
// previousSibling 프로퍼티는 요소 노드뿐만 아니라 텍스트 노드를 반환할 수도 있다.
const { previousSibling } = nextSibling;
console.log(previousSibling); // #text
// #fruits 요소의 첫 번째 자식 요소 노드를 탐색한다.
// firstElementChild 프로퍼티는 요소 노드만 반환한다.
const { firstElementChild } = $fruits;
console.log(firstElementChild); // li.apple
// #fruits 요소의 첫 번째 자식 요소 노드(li.apple)의 다음 형제 노드를 탐색한다.
// nextElementSibling 프로퍼티는 요소 노드만 반환한다.
const { nextElementSibling } = firstElementChild;
console.log(nextElementSibling); // li.banana
// li.banana 요소의 이전 형제 요소 노드를 탐색한다.
// previousElementSibling 프로퍼티는 요소 노드만 반환한다.
const { previousElementSibling } = nextElementSibling;
console.log(previousElementSibling); // li.apple
</script>
</html>
노드 정보 취득
1. Node.prototype.nodeType
- Node.ELEMENT_NODE : 요소 노드 타입을 나타내는 상수 1을 반환
- Node.TEXT_NODE : 텍스트 요소 타입을 나타내는 상수 3을 반환
- Node.DOCUMENT_NODE : 문서 노드 타입을 나타내는 상수 9를 반환
2. Node.prototype.nodeName
- 요소노드 : 대문자 문자열로 태그이름을 반환
- 텍스트 노드 : 문자열 #text를반환
- 문서 노드 : 문자열 #document를 반환
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello</div>
</body>
<script>
// 문서 노드의 노드 정보를 취득한다.
console.log(document.nodeType); // 9
console.log(document.nodeName); // #document
// 요소 노드의 노드 정보를 취득한다.
const $foo = document.getElementById('foo');
console.log($foo.nodeType); // 1
console.log($foo.nodeName); // DIV
// 텍스트 노드의 노드 정보를 취득한다.
const $textNode = $foo.firstChild;
console.log($textNode.nodeType); // 3
console.log($textNode.nodeName); // #text
</script>
</html>
요소 노드의 텍스트 조작
지금까지 살펴본 노드 탐색,노드정보 프로퍼티는 모두 읽기 전용이었다. 지금부터는 수정할 수 있는 접근자 프로퍼티들을 알아보자.
nodeValue
노드 객체의 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>
이처럼 텍스트 노드의 nodeValue프로퍼티를 참조하는 경우에만 텍스트를 반환한다.
이를 활용해서 텍스트 값을 바꿔보자.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello</div>
</body>
<script>
// 1. #foo 요소 노드의 자식 노드인 텍스트 노드를 취득한다.
const $textNode = document.getElementById('foo').firstChild;
// 2. nodeValue 프로퍼티를 사용하여 텍스트 노드의 값을 변경한다.
$textNode.nodeValue = 'World';
console.log($textNode.nodeValue); // World
</script>
</html>
textContent
요소 노드의 콘텐츠 영역 내의 텍스트를 모두 반환한다. 즉 요소 노드의 childNodes프로퍼티가 반환한 모든 노드들의 텍스트 노드들의 값 즉 텍스트를 반환하며 HTML 마크업은 무시된다.
<!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>
해당 프로퍼티를 활용하면 nodeValue 프로퍼티를 활용하여 텍스트를 변경하는 것보다 코드가 간결하다.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// #foo 요소 노드는 텍스트 노드가 아니다.
console.log(document.getElementById('foo').nodeValue); // null
// #foo 요소 노드의 자식 노드인 텍스트 노드의 값을 취득한다.
console.log(document.getElementById('foo').firstChild.nodeValue); // Hello
// span 요소 노드의 자식 노드인 텍스트 노드의 값을 취득한다.
console.log(document.getElementById('foo').lastChild.firstChild.nodeValue); // world!
</script>
</html>
만약 요소 노드의 콘텐츠 영역에 텍스트만 존재한다면 firstChild.nodeValue와 textContent 프로퍼티는 같은 결과를 반환한다.
<!DOCTYPE html>
<html>
<body>
<!-- 요소 노드의 콘텐츠 영역에 다른 요소 노드가 없고 텍스트만 존재 -->
<div id="foo">Hello</div>
</body>
<script>
const $foo = document.getElementById('foo');
// 요소 노드의 콘텐츠 영역에 자식 요소 노드가 없고 텍스트만 존재한다면
// firstChild.nodeValue와 textContent는 같은 결과를 반환한다.
console.log($foo.textContent === $foo.firstChild.nodeValue); // true
</script>
</html>
textContent프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 이때 HTML 마크어빙 포함되어 있더라도 문자열로 그대로 인식된다.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// #foo 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다.
// 이때 HTML 마크업이 파싱되지 않는다.
document.getElementById('foo').textContent = 'Hi <span>there!</span>';
</script>
</html>
innerText 프로퍼티또한 textContent와 유사한 동작을 하지만 CSS에 순종적인 단점이 있어 사용하지 않는것이 좋다.
DOM 조작
새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 혹은 교체하는 것을 DOM 조작이라고 한다.
innerHTML
Element.prototype.innerHTML 프로퍼티를 활용하면 요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환한다.
<!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>
innerHTML 프로퍼티는 HTML 마크업이 포함된 문자열을 반환한다. 따라서 innerHTML 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 포함되어 있는 HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.
document.getElementById('foo').innerHTML = 'Hi <span>there!</span>';
</script>
</html>
이처럼 innerHTML 프로퍼티를 사용하면 간단하게 DOM 조작이 가능하다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li class="apple">Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 노드 추가
$fruits.innerHTML += '<li class="banana">Banana</li>';
// 노드 교체
$fruits.innerHTML = '<li class="orange">Orange</li>';
// 노드 삭제
$fruits.innerHTML = '';
</script>
</html>
요소노드의 innerHTML프로퍼티에 할당한 HTML 마크업 문자열은 렌더링 엔진에 의해서 파싱되어 DOM에 반영된다. 따라서 사용자로부터 입력받은 데이터를 그대로 innerHTML프로퍼티에 할당하는 것은 크로스 사이트 스크립팅 공격에 매우 취약하므로 위험하다. HTML 마크업 내에 JS 악성코드가 포함되어 있다면 그대로실행될 수 있기 때문이다.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello</div>
</body>
<script>
// innerHTML 프로퍼티로 스크립트 태그를 삽입하여 자바스크립트가 실행되도록 한다.
// HTML5는 innerHTML 프로퍼티로 삽입된 script 요소 내의 자바스크립트 코드를 실행하지 않는다.
document.getElementById('foo').innerHTML
= '<script>alert(document.cookie)</script>';
</script>
</html>
HTML5에서는 innerHTMLl프로퍼티로 삽입된 script 요소내의 JS 파일을 실행하지 않는다. 따라서 위 공격은 통하지 않는다. 하지만 아래의 공격은 모던 브라우저에서도 동작한다.
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello</div>
</body>
<script>
// 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행되도록 한다.
document.getElementById('foo').innerHTML
= `<img src="x" onerror="alert(document.cookie)">`;
</script>
</html>
이처럼 innerHTML 프로퍼티를 사용하면 DOM 조작자체는 간단하고 직관적이지만 크로스 사이트 스크립팅 공격에 취약하다는 단점이 있다.
또한 또다른 단점으로는 innerHTML 프로퍼티에 HTML 마크업 문자열을 할당하는 경우 요소 노드의 모든 자식 노드를 제거하고 할당한 HTML 마크업 문자열을 파싱하여 DOM을 변경한다는 것이다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li class="apple">Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 노드 추가
$fruits.innerHTML += '<li class="banana">Banana</li>';
</script>
</html>
위 예제에서 li.apple은 변경이 없으므로 다시생성할 필요가 없다. 하지만 실제로는 모든 자식 노드를 제거하고 새롭게 요소를 추가하고 있다.
또한 innerHTML 프로퍼티는 새로운 요소를 삽입하는 경우 삽입될 위치를 지정할 수 없다는 단점또한 존재한다.
<ul id="fruits">
<li class="apple">Apple</li>
<li class="orange">Orange</li>
</ul>
따라서 innerHTML 요소는 기존요소를 제거하지 않으면서 위치를 지정하여 새로운 요소를 삽입해야 하는 경우 사용하지 않는것이 좋다.
insertAdjacentHTML
Element.prototype.insertAdjacentHTML 메서드는 기존 요소를 제거하지 안흥면서 위치를지정하여 새로운 요소를 삽입한다.
두번째 인수로 전달한 HTML 마크업 문자열을 파싱하고 그 결과로 생성된 노드를 첫번째 인수로 전달할 위치에 삽입하여 DOM에 반영한다.
<!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>
이 경우 기존의 자식 요소를 제거하지 않기 때문에 innerHTML에 비해서 효율적이다.
다만 크로스 사이트 스크립팅 공격에 취약하다는 점은 동일하다.
노드 생성과 추가
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>
1. 요소 노드 생성
Document.prototype.createElement 메서드는 요소 노드를 생성하여 반환한다.
// 1. 요소 노드 생성
const $li = document.createElement('li');
2. 텍스트 노드 생성
Document.prototype.createTextNode 메서드는 텍스트 노드를 생성하여 반환한다.
// 2. 텍스트 노드 생성
const textNode = document.createTextNode('Banana');
3. 텍스트 요소를 요소 노드의 자식 노드로 추가
Node.prototype.appendChild 메서드는 childNode에게 인수로 전달할 노드를 마지막 자식 노드로 추가한다.
// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
만약 요소가 텍스트 노드만 있다면 textContent 프로퍼티를 사용하는 것이 간편하다.
// 텍스트 노드를 생성하여 요소 노드의 자식 노드로 추가
$li.appendChild(document.createTextNode('Banana'));
// $li 요소 노드에 자식 노드가 하나도 없는 위 코드와 동일하게 동작한다.
$li.textContent = 'Banana';
4. 요소 노드를 DOM에 추가
Node.prototype.appendChild 메서드를 활용하여 DOM에 추가한다.
// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);
복수의 노드 생성과 추가
여러개의 요소 노드를 생성하여 DOM에 추가하는것도 가능하다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits"></ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
['Apple', 'Banana', 'Orange'].forEach(text => {
// 1. 요소 노드 생성
const $li = document.createElement('li');
// 2. 텍스트 노드 생성
const textNode = document.createTextNode(text);
// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);
});
</script>
</html>
위 예제에서는 DOM을 세번 변경하게 된다. DOM을 변경하는 것은 높은 비용이 드므로 줄이는 것이 좋다.
따라서 DOM에 추가할 3개의 요소 노드를 컨테이너 요소에 자식 노드로 추가하고 컨테이너 요소를 #fruits 요소에 자식 으로 추가하는 방식이 있다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits"></ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
// 컨테이너 요소 노드 생성
const $container = document.createElement('div');
['Apple', 'Banana', 'Orange'].forEach(text => {
// 1. 요소 노드 생성
const $li = document.createElement('li');
// 2. 텍스트 노드 생성
const textNode = document.createTextNode(text);
// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
// 4. $li 요소 노드를 컨테이너 요소의 마지막 자식 노드로 추가
$container.appendChild($li);
});
// 5. 컨테이너 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($container);
</script>
</html>
물론 위 경우 DOM은 한번만 변경하지만 불필요한 컨테이너 요소가 추가되는 부작용이 있다.
<ul id="fruits">
<div>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</div>
</ul>
이 경우 DocumentFragment 노드를 통해 해결할 수 있다. DocumentFragment 노드는 문서,요소,어트리뷰트,텍스트 노드와 같은 노드 객체의 일종으로 부모 노드가 없어 기존 DOM과는 별도로 존재한다는 특징이 있다.
해당 노드는 위 예제의 컨테이너 요소처럼 자식 부모들의 부모 노드로써 별도의 서브 DOM을 구성하여 기존 DOM에 추가하기 위한 용도로 사용된다.
DocumentFragment노드는 기존 DOM과 별도이기에 dOM에 어떤 변경도 발생하지 않는다.
또한 이 노드를 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>
따라서 여러개의 요소 노드를 삽입하기 위해서는 DocumentFragment 노드를 사용하는 것이 훨씬 효율적이다.
노드 삽입
마지막 노드로 추가
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);
</script>
</html>
지정한 위치에 노드 삽입
Node.prototype.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>
두번째 인수로 전달받은 노드는 반드시 inserBefore 메서드를 호출한 노드의 자식노드여야 하며 그렇지 않은 경우 에러가 발생한다.
<!DOCTYPE html>
<html>
<body>
<div>test</div>
<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'));
// 두 번째 인수로 전달받은 노드는 반드시 #fruits 요소 노드의 자식 노드이어야 한다.
$fruits.insertBefore($li, document.querySelector('div'));
// DOMException
</script>
</html>
두번째 인수로 전달받은 노드가 null이면 노드의 마지막 자식 노드로 추가된다. 즉 appendChild 메서드와 동일하게 동작한다.
<!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'));
// 두 번째 인수로 전달받은 노드가 null이면 $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.insertBefore($li, null);
</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를 인수로 전달하거나 생략하면 얕은복사하여 노드 자신만의 사본을 생성한다. 얕은 복사로 생성된 요소 노드는 자손 노드를 복사하지 않으므로 텍스트 노드도 없다.
<!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();
// 사본 요소 노드에 텍스트 추가
$shallowClone.textContent = 'Banana';
// 사본 요소 노드를 #fruits 요소 노드의 마지막 노드로 추가
$fruits.appendChild($shallowClone);
// #fruits 요소를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성
const $deepClone = $fruits.cloneNode(true);
// 사본 요소 노드를 #fruits 요소 노드의 마지막 노드로 추가
$fruits.appendChild($deepClone);
</script>
</html>
노드 교체
Node.prototype.replaceChild 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다.
<!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);
</script>
</html>
노드 삭제
Node.prototype.removeChild(child) 메서드는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제한다.
<!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);
</script>
</html>
'FrontEnd > Deep Dive' 카테고리의 다른 글
[JS] DeepDive(40) 이벤트 (1) - event객체 (0) | 2023.09.20 |
---|---|
[JS] DeepDive(39) DOM (3) - 어트리뷰트 (0) | 2023.09.19 |
[JS] DeepDive(39) DOM(1) - DOM,요소취득,요소타입 (0) | 2023.09.19 |
[JS] DeepDive(38) 브라우저의 렌더링 과정 (0) | 2023.09.19 |
[JS] DeepDive(37) Set과 Map (0) | 2023.09.18 |