자바스크립트로 DOM을 조작하다보면 가장 번잡한 일이 새로운 HTML Element를 만들어 추가하는 것입니다. DOM 조작에 의해 DOM에 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되므로 성능에 영향을 줍니다. 따라서 복잡한 콘텐츠를 다루는 DOM 조작은 성능 최적화를 위해 주의해서 다뤄야 합니다. 이번 게시글을 통해 HTML Element를 추가하는 방법들과 각 방법의 장단점과 활용 방안에 대해 정리해보겠습니다.
Element.prototype.innerHTML
프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당된 문자열에 포함되어 있는 HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영됩니다.
const $app = document.getElementById('app');
// HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 파싱
$app.innerHTML = `<li>할 일</li>`;
장점
단점
모든 자식 노드를 제거하고 할당(중간에 삽입 불가능)
크로스 사이트 스크립팅 공격에 취약
크로스 사이트 스크립팅 공격
크로스 사이트 스크립팅 공격은 공격자가 상대방의 브라우저에 스크립트가 실행되도록 해 사용자의 세션을 가로채거나, 웹 사이트를 변조하거나, 악의적 콘텐츠를 삽입하거나, 피싱 공격을 진행하는 것을 말합니다.
const $app = document.getElementById('app');
// 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행
$app.innerHTML = `<img src="x" >`;
링크: https://codesandbox.io/embed/1-innerhtml-5y8m2x?fontsize=14&hidenavigation=1&theme=dark
활용 방안
Element.prototype.insertAdjacentHTML(position, DOMString)
메소드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입합니다. insertAdjacentHTML()
는 두 번째 인자로 전달한 HTML 마크업 문자열을 파싱하고 그 결과로 생성된 노드를 첫 번째 인수로 전달한 위치에 삽입하여 DOM에 반영합니다. 첫 번째 인수로 전달받은 위치는 아래 그림과 같습니다.
const $app = document.getElementById('app');
const childHtmlString = `<li>할 일</li>`
// HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 추가
$app.insertAdjacentHTML('beforeend', childHtmlString)
innerHTML
과 마찬가지로 HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립팅 공격에 취약Node.prototype.appendChild(childNode)
메소드는 매개변수 childNode에게 인수로 전달한 노드를 appendChild 메소드를 호출한 노드의 마지막 자식 노드로 추가합니다. appendChild()
는 노드 객체만 추가할 수 있고, 문자열을 추가하면 에러가 발생합니다. 그리고 return 값을 반환합니다. 또, 한번에 2개 이상의 요소를 추가할 수 없습니다.
const $app = document.getElementById('app');
// 자식 노드를 추가하고 기존 DOM에 추가하려는 경우
const $li1 = document.createElement('li');
const $childNode = document.createElement('input');
$childNode.setAttribute('type', 'hidden');
$childNode.setAttribute('value', 100);
// 새롭게 생선한 요소에 자식 노드 추가 (DOM 변경 X)
$li1.appendChild($childNode);
// 기존 DOM에 한번 추가하므로 DOM은 한 번 변경된다.
$app.appendChild($li1);
// 텍스트 추가 불가능
$app.appendChild('<li>할 일2</li>');
// 2개 이상의 요소 추가 불가능 (인자를 2개 넣어도 첫 번째 인자만 부모 노드에 추가)
const $li2 = document.createElement('li');
li2.textContent = '할 일2';
const $li3 = document.createElement('li');
li3.textContent = '할 일3';
$app.appendChild($li2, $li3);
Element.append()
메소드는 appendChild()
와 같은 비슷한 기능을 수행합니다. 하지만 문자열을 입력받아 텍스트 노드를 생성해 추가할 수 있습니다. 또, 한번에 2개 이상의 자식 노드를 추가할 수 있습니다. appendChild()
에 비해 기능이나 확장성 면에서 더 뛰어나다고 생각합니다.
const $app = document.getElementById('app');
// 기본 형태
const $li1 = document.createElement('li');
$li1.textContent = '할 일 1';
const $li2 = document.createElement('li');
$li2.textContent = '할 일 2';
$app.append($li1, $li2);
// 구조분해할당 사용
const todoNodeList = ['할 일 3', '할 일 4'].map((todo) => {
const $li = document.createElement('li');
$li.textContent = todo;
return $li;
})
$app.append(...todoNodeList)
Documnet.prototype.DocumentFragment
노드는 웹 문서의 메인 DOM 트리에 포함되지 않는, 가상의 메모리에 존재하는 DOM 노드 객체입니다. 이를 통해 메인 DOM 트리 외부에 경량화된 DOM을 만들 수 있어 리플로우와 리페인트의 영향 없이 메모리에서 DOM 조작이 가능합니다. 여러 개의 요소 노드를 생성하여 DOM에 추가하려고 할 때 유용하게 사용될 수 있습니다.
const $app = document.getElementById('app');
// DocumentFragment 노드 생성
const $fragment = document.createDocumentFragment();
const todoNodeList = ['할 일 1', '할 일 2'].forEach((todo) => {
const $li = document.createElement('li');
$li.textContent = todo;
// $li 요소 노드를 DocumentFragment 노드의 마지막 자식 노드로 추가
$fragment.appendChild($li);
})
// DocumentFragment 노드를 $app 요소 노드의 마지막 자식 노드로 추가
$app.appendChild($fragment);
DocumentFragment
노드를 사용하는 것이 더 효율적입니다.