본문 바로가기

2주차

성능 좋게 DOM을 조작해보자!

안녕하세요! 웹파트 OB 김채현입니다.

 

자바스크립트의 기능 중 하나로 DOM을 조작하는 것이 있습니다.

이번 2주차 과제에서도 반복적으로 DOM에서 노드를 추가해서 보여주는 기능을 구현해야 했습니다.

그렇다면 효율적으로 DOM을 조작할 수 있는 방법으로

무엇이 있는지 한번 알아보고 싶어서 아티클을 작성하게 되었습니다.

강조되고 반복되는 DOM 조작은 개발자를 불안하게 해요!

 

DOM이란?

DOM은 The Document Object Model로, HTML, XML 문서의 프로그래밍 인터페이스입니다.

DOM은 nodes와 objects로 문서를 표현하는데,

웹 페이지를 스크립트 또는 프로그래밍 언어들에서 사용될 수 있게 연결시켜주는 역할을 합니다.

 

웹 페이지는 일종의 문서로, 웹 브라우저를 통해 그 내용이 해석되어

웹 브라우저 화면에 나타나거나 HTML 소스 자체로 나타나기도 합니다.

우리는 JS와 같은 스크립팅 언어를 이용해서 DOM을 수정할 수 있습니다.

 

자주 쓰이는 노드 추가 방법

우리는 보통 DOM 트리에 노드를 추가하고 싶으면

`createElement` 와 `appendChild`를 사용합니다.

const p = document.createElement("p"); // 노드 생성
document.body.appendChild(p); // 생성한 요소를 DOM 트리에 추가

 

그런데 만약 여러개의 노드를 추가하고 싶을 때는?

const parent = document.querySelector('.parent');

for (let i = 1; i <= 20; i++) {
    const p = document.createElement('p');
    p.textContent = '!';

    parent.appendChild(p);
}

이런식으로 반복문을 사용한다면 계속해서 추가할 수 있습니다.

20개의 노드를 추가하기 위해서, 20번의 DOM 접근이 필요합니다.

그런데 만약 개수가 늘어나서 100개, 200개를 넘어가면 어떻게 될까요?

100번, 200번의 접근을 해야하는 것일까요?

 

"이렇게 직접 DOM에 접근해서 노드를 추가하는것이 문제가 없을까??"라는 의문이 들었습니다.

 

노드를 직접 접근하면 생기면 발생하는 문제

노드를 접근하게 되면, DOM에서는 reflow가 발생합니다.

DOM repaint
: 특정 요소의 visibility를 수정하거나 배경색이나 글자색 등을 바꿀 때 발생하는 것으로, 화면에 각 HTML 요소들의 위치는 변경되지 않고, 화면에 표시되는 것이 바뀔 때 일어납니다.
특히 DOM 요소의 visibility를 수정하면 해당 DOM의 자식 DOM까지 하위 트리 구조 전체를 탐색해야 하므로 CPU 자원이 소모됩니다.
DOM reflow
: repaint와는 다르게 DOM이 화면에 표시되는 구조가 바뀔 때, 또는 CSS 클래스가 바뀔 때 일어납니다.
DOM 트리가 배치되는 위치를 전체적으로 다시 계산해서 화면에 출력하는 것을 의미하며,
DOM repain보다 자원소모가 큽니다.

➡️ repaint는 스타일만 변경, reflow는 화면 구조가 변경

 

reflow는 렌더트리가 재생성되는 것으로 비용이 매우 크기 때문에

앞에 방식을 사용한다면, reflow가 발생하여 성능에 영향을 주게 됩니다.

 

요소를 생성할 때 reflow가 되나요?
➡️ 아니요! 요소를 추가하는 것은 그냥 메모리에 올라가는 것으로,
부모 요소에 자식 요소를 추가할 때만, DOM 트리를 다시 계산하기 때문에 reflow가 발생합니다.

 

그렇다면 어떻게 성능에 영향을 덜 주면서 요소를 추가할 수 있을까요?

DocumentFragment에 대해 알아보겠습니다.

 

DocumentFragment란?

DocumentFragment는 메인 HTML DOM 트리에 포함되지 않는, 가상 메모리에 존재하는 DOM 노드 객체로,

DOM 요소들을 메모리에 일시적으로 저장할 수 있는 객체입니다.

일반 HTML DOM처럼 요소를 추가하거나 조작할 수 있는데,

이 과정에서는 웹 페이지에는 아무런 시각적 변화가 없습니다.

 

그 대신에 여러개의 요소들을 모아두었다가 준비가 되면,

Document의 요소를 한번에 HTML DOM에 추가됩니다.

 

즉, 접근하는 횟수를 합번으로 줄여 영향을 최소한으로 주는 방식으로 성능을 최적화할 수 있습니다!

 

사용 방법

`createDocumentFragment()` 메서드를 사용해서 메인 DOM 객체와는 별개의 새로운 DOM 객체를 생성해 사용합니다.

평소 DOM에 접근하던 것처럼 접근해서 사용하면 됩니다.

// DocumentFragment 생성
const fragment = document.createDocumentFragment();

// DocumentFragment에 추가
fragment.appendChild(clone);

 

모든 작업이 완료되면, 반영이 될 수 있도록 실제 document에 넣어주면 끝입니다.

const tableBody = document.querySelector("tbody");
// 테이블에 DocumentFragment 추가
tableBody.appendChild(fragment);

 

 

사용 방법은 간단한데 얼마나 성능이 좋아질지 궁금하지 않나요?

 

createElement vs DocumentFragemet 성능 비교

소요 시간 비교

DOM에 노드를 추가했을 때 추가되는 시간을 비교해보겠습니다.

소요시간은 아래 코드를 통해서 쉽게 실제 시간을 확인할 수 있습니다.

const startTime = performance.now();
const endTime = performance.now();
console.log(`createElement 방식 소요 시간: ${endTime - startTime} ms`);

 

웹 파트의 인원 수는 29이기 때문에 데이터가 크기 않아 많은 차이는 나지 않지만

확실히 DocumentFragment 방식이 시간이 적게 소요된 것을 볼 수 있습니다.

 

DOM 접근 횟수 비교

createElement 방식에서는 각 멤버당 9번의 DOM 접근이 필요합니다.

 

  • 체크박스 셀: tr.appendChild(checkboxCell) - 1회 DOM 접근
  • 이름 셀: tr.appendChild(nameCell) - 1회 DOM 접근
  • 영어 이름 셀: tr.appendChild(englishNameCell) - 1회 DOM 접근
  • GitHub ID 셀: tr.appendChild(githubCell) - 1회 DOM 접근
  • 성별 셀: tr.appendChild(genderCell) - 1회 DOM 접근
  • 역할 셀: tr.appendChild(roleCell) - 1회 DOM 접근
  • 첫 번째 금잔디조 셀: tr.appendChild(firstWeekGroupCell) - 1회 DOM 접근
  • 두 번째 금잔디조 셀: tr.appendChild(secondWeekGroupCell) - 1회 DOM 접근
  • tbody에 tr 추가: tbody.appendChild(tr) - 1회 DOM 접근

이 구조에서 DocumentFragment를 사용하면

tbody.appendChild(fragment)에서 한번만 DOM 접근을 하기 때문에 성능 차이가 크게 날 수 있습니다.

 

<template> 태그

`<template>`는 페이지를 불러온 순간 즉시 그려지지는 않지만,

이후 JS를 사용해 인스턴스를 생성할 수 있는 HTML 코드를 담을 방법을 제공합니다.

 

즉, 브라우저가 렌더링하지 않는 HTML 조각을 정의할 수 있습니다.

초기에는 렌더링되지 않기 때문에 브라우저가 불필요한 레이아웃, 스타일 조정을 하지 않습니다.

이로 인해 초기 로딩 시 레이아웃과 스타일 조정이 줄어들어 reflow와 repaint가 최소화될 수 있습니다.

 

아래처럼 <template> 태그를 작성하면

<!-- tr 템플릿 -->
<template id="row-template">
<tr>
  <td><input class="checkbox" type="checkbox" /></td>
  <td class="name"></td>
  <td class="english-name"></td>
  <td><a class="github"></a></td>
  <td class="gender"></td>
  <td class="role"></td>
  <td class="first-week-group"></td>
  <td class="second-week-group"></td>
</tr>
</template>

 

템플릿을 아래처럼 한번만 가져오면 굳이 여러번 요소들을 만들고 추가할 필요가 사라집니다.

// 템플릿 가져오기
const template = document.querySelector("#row-template");

// template에서 복제된 내용 가져오기
const clone = template.content.cloneNode(true);

 

 

성능 외의 장점은 없나요?

`<template>` 태그는 HTML 문서에서 코드를 작성하기 때문에 구조가 직관적으로 보여서

코드의 가독성이 높아지고 유지보수하기 좋다는 장점이 있습니다.

 

계속해서 요소를 생성하는 JS 코드를 작성하지 않아도 되어서 코드도 짧아지기까지 하다니 

코드를 효율적으로 작성할 수 있다는 장점도 있습니다.

 

따라서 계속 반복적으로 동일한 구조의 요소를 추가해야 한다면

<template>을 사용하는 것이 훨씬 효율적이라고 생각합니다.


아티클을 작성하면서 공부해보고, 직접 실습해보니까

그냥 단순히 요소를 추가하며 DOM을 조작하는 것보다

성능과 코드 개선을 위해 DocumentFragment와 template 태그를 사용하는 것이 더 좋은 것 같다고 느꼈습니다.

 

혹시 지금까지 그냥 직접 DOM에 하나씩 접근해서 조작하고 있었다면

이렇게 개선해보는 것은 어떨까요?

읽어주셔서 감사합니다 :)

 

참고한 글

https://developer.mozilla.org/ko/docs/Web/API/DocumentFragment

https://developer.mozilla.org/ko/docs/Web/HTML/Element/template

https://friedegg556.tistory.com/151

https://simuing.tistory.com/entry/javascript-DOM-reflow-%EC%8B%9C-%EC%9E%90%EC%9B%90-%EC%86%8C%EB%AA%A8-%EC%B5%9C%EC%86%8C%ED%99%94-%EB%B0%A9%EB%B2%95