* 아래 내용은 이웅모 저자님의 모던 자바스크립트 Deep Dive 책(위키북스)을 정리한 내용입니다.
저작권에 문제가 된다면 삭제하도록 하겠습니다.
6. DOM 조작
DOM 조작은 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것을 말한다.
6.1 innerHTML
Element.prototype.innerHTML 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 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>
textContent 프로퍼티와 다르게 HTML 마크업이 포함된 문자열을 그대로 반환한다.
요소 노드의 innerHTML 프로퍼티에 문자열을 할당하면 원래 있던 모든 자식노드가 제거되고 문자열에 포함되어 있는 HTML 마크업이 파싱되어 반영된다.
// HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.
document.getElementById('foo').innerHTML = 'Hi <span>there!</span>';
이렇게 innerHTML 프로퍼티를 사용하면 간단하게 DOM 조작이 가능하지만, 크로스 사이트 스크립팅(Cross-Site Scripting Attacks) 공격에 취약하므로 위험하다.
// 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행되도록 한다.
document.getElementById('foo').innerHTML
= `<img src="x" onerror="alert(document.cookie)">`;
innerHTML 프로퍼티의 또다른 단점은 요소 노드의 모든 자식노드를 제거하고 할당한 문자열을 파싱한다.
<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>
위 코드는 정상적으로 기존 코드에 li태그를 추가할 것 처럼 보이지만 사실은 $fruits의 모든 자식요소를 제거하고 새롭게 요소 노드 li.apple과 li.banana를 생성하여 $fruit 요소의 자식 요소로 추가한다.
기존 코드를 모두 삭제하고 다시 생성하여 할당하기 때문에 효율적이지 않다.
6.2 insertAdjacentHTML 메서드
Element.prototype.insertAdjacentHTML 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.
첫번째 인수로 위치를 지정하고 두번째 인수에 HTML 마크업 문자열을 전달한다.
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>');
insertAdjacentHTML 메서드 역시 HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립팅 공격에 취약하다.
6.3 노드 생성과 추가
DOM은 노드를 직접 생성/삽입/삭제/치환하는 메서드도 제공한다.
<body>
<ul id="fruits">
<li>Apple</li>
</ul>
</body>
<script>
const $fruits = document.getElementById('fruits');
const $li = document.createElement('li');
const textNode = document.createTextNode('Banana');
$li.appendChild(textNode);
$fruits.appendChild($li);
</script>
Document.prototype.createElement(tagName) 메서드는 요소 노드를 생성하여 반환한다.
// 1. 요소 노드 생성
const $li = document.createElement('li');
Document.prototype.createTextNode(text) 메서드는 텍스트 노드를 생성하여 반환한다.
// 2. 텍스트 노드 생성
const textNode = document.createTextNode('Banana');
Node.prototype.appendChild(childNode) 메서드는 매개변수 childNode에게 인수로 전달한 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가한다.
// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
Node.prototype.appendChild 메서드를 사용하여 요소 노드를 #fruit 요소 노드의 마지막 자식 요소로 추가한다.
// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);
이 과정에서 새롭게 생성한 요소 노드가 DOM에 추가된다. DOM이 한 번 변경되므로 리플로우와 리페인트가 실행된다.
6.4 복수의 노드 생성과 추가
복수의 노드를 추가할 때 위의 방법으로 반복해서 추가하면 DOM이 여러번 변경되고, 리플로우와 리페인트가 여러 번 실행된다. DOM을 변경하는 것은 높은 비용이 드는 처리이므로 가급적 횟수를 줄이는 편이 성능에 유리하다.
div 컨테이너를 추가하여 한 번만 변경하는 방법도 있지만, 불필요한 div 태그가 추가되는 부작용이 있다.
<ul id="fruits">
<div>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</div>
</ul>
이러한 문제는 DocumentFragment 노드를 통해 해결할 수 있다.
DocumentFragment 노드는 노드 객체의 일종으로 부모 노드가 없어서 DOM과는 별도로 존재한다는 특징이 있다.
DocumentFragment 노드에 자식 노드를 추가하여도 기존 DOM에는 변경사항이 없고, DocumentFragment 노드를 DOM에 추가하면 자신은 제거되고 자신의 자식 노드만 DOM에 추가된다.
<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>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
</ul>
6.5 노드 삽입
앞에서 보았던 Node.prototype.appendChild 메서드는 인수로 전달받은 노드를 자신을 호출한 노드의 마지막 자식 노드로 DOM에 추가한다.
지정한 위치에 노드를 삽입하고 싶으면 Node.prototype.insertBefore(newNode,childNode) 메서드를 사용한다.
첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입한다.
<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>
<ul id="fruits">
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</ul>
두 번째 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 자식 노드이어야 한다. 그렇지 않으면 DOMException에러가 발생한다.
두 번째 인수로 전달받은 노드가 null이면 appendChild 메서드처럼 마지막 자식노드로 추가한다.
6.6 노드 이동
DOM에 이미 존재하는 노드를 appendChild 또는 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서 노드를 제거하고 새로운 위치에 노드를 추가한다. 즉, 노드가 이동한다.
<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>
<ul id="fruits">
<li>Orange</li>
<li>Banana</li>
<li>Apple</li>
</ul>
6.7 노드 복사
Node.prototype.cloneNode([deep:true | false]) 메서드는 노드의 사본을 생성하여 반환한다.
매개변수 deep에 true를 인수로 전달하면 노드를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성하고,
false를 인수로 전달하거나 생략하면 얕은 복사를 하여 노드 자신만의 사본을 생성한다.
<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>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
<ul id="fruits">
<li>Apple</li>
<li>Banana</li>
</ul>
</ul>
6.8 노드 교체
Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다.
첫 번째 인수로 교체할 새로운 노드를 전달하고 두 번째 인수로 이미 존재하는 교체될 노드를 전달한다.
두 번째 인수로 전달한 노드는 replaceChild 메서드를 호출한 노드의 자식 노드이어야 한다.
<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>
<ul id="fruits">
<li>Banana</li>
</ul>
6.9 노드 삭제
Node.prototype.removeChild(child) 메서드는 인수로 전달한 노드를 DOM에서 삭제한다.
인수로 전달한 노드는 removeChild 메서드를 호출한 노드의 자식 노드이어야 한다.
<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>
<ul id="fruits">
<li>Apple</li>
</ul>
Reference
- 이웅모 , 모던 자바스크립트 Deep Dive , 위키북스 , 2020
'개인공부 > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글
40장 이벤트(1) (0) | 2023.01.31 |
---|---|
39장 DOM(4) (0) | 2023.01.29 |
39장 DOM(2) (0) | 2023.01.28 |
39장 DOM(1) (0) | 2023.01.28 |
38장 브라우저의 렌더링 과정 (0) | 2023.01.27 |
댓글