들어가기 전에

"DOM 핸들링, 더 이상 답답하지 않아요! 웹 성능을 극대화하는 꿀팁"웹 개발에서 빠른 속도는 필수죠! 하지만 DOM 조작이 복잡해지면서 웹 페이지가 느려지는 경험, 다들 한번쯤 해보셨을 겁니다.왜 DOM 핸들링이 느릴까요?자바스크립트로 DOM을 조작할 때마다 브라우저는 페이지를 다시 그려야 하는데, 이 과정에서 리플로우리페인트라는 비용이 발생합니다. 특히 많은 양의 DOM을 자주 변경하는 경우 성능 저하가 심해질 수 있습니다.자바스크립트는 오늘날 웹 서비스 개발에서 뗄레야 뗄 수 없는 언어이다. 자바스크립트를 통하여 누구나 손쉽게 문서 객체 모델 *(Document Object Model, 이하 DOM)*을 자유롭게 조작하여 역동적인 웹 페이지를 만들 수 있다. 이렇게 대부분의 웹 사이트에서 사용하고 있는 스크립트를 사용한 DOM의 조작 기능에는 성능상의 문제가 있다. 이 글에서는 스크립트를 사용한 DOM 조작 시 발생하는 성능저하 문제점과 그 최적화에 대해 알아본다.

다양한 최적화 기법 소개

  • Virtual DOM: React, Vue.js 등의 프레임워크에서 사용되는 Virtual DOM 개념을 소개하고, 실제 DOM 조작을 최소화하는 방식을 설명합니다.
  • RequestAnimationFrame: 애니메이션과 같이 자주 발생하는 DOM 변경 시, 브라우저의 다음 리페인트 시점에 변경 작업을 예약하여 성능을 향상시키는 방법을 설명합니다.
  • Event Delegation: 이벤트 버블링을 활용하여 부모 요소에 이벤트 리스너를 한 번만 등록하고, 자식 요소의 이벤트를 처리하는 방식을 설명합니다.
  • CSS Transitions & Animations: 가능한 경우 CSS Transitions와 Animations를 활용하여 부드러운 애니메이션 효과를 구현하고, 자바스크립트를 통한 DOM 조작을 줄입니다.

성능 측정 및 분석 도구 소개

  • 브라우저 개발자 도구: Performance 탭을 활용하여 리플로우, 리페인트, 스크립트 실행 시간 등을 측정하고 분석하는 방법을 상세히 설명합니다.
  • Lighthouse: 웹 페이지의 성능, 접근성, SEO 등을 종합적으로 분석하고 개선점을 제시하는 도구를 소개합니다.
  • Custom Metrics: 필요에 따라 사용자 정의 메트릭을 생성하여 특정 부분의 성능을 집중적으로 측정하고 분석합니다.

고려사항

  • 다양한 시나리오: 테이블 데이터 갱신, 리스트 렌더링, 드래그 앤 드롭 등 다양한 시나리오에서 DocumentFragment를 활용한 최적화 예시를 제공합니다.
  • 코드 비교: 최적화 전후의 코드를 비교 분석하여 성능 향상 효과를 시각적으로 보여줍니다.
  • 코드 품질: 코드 가독성을 높이고, 주석을 충분히 활용하여 코드 이해를 돕습니다.
  • 브라우저 호환성: 각 브라우저별 DOM 조작 성능 차이와 호환성 문제를 고려하여 설명합니다.
  • 최신 트렌드: Web Components, Shadow DOM 등 최신 웹 표준 기술과의 연관성을 설명하고, 미래 지향적인 최적화 방법을 제시합니다.
  • 성능 최적화 전략: 단순히 DOM 조작뿐만 아니라, 이미지 최적화, HTTP 캐싱, 코드 분할 등 전체적인 웹 성능 최적화 전략과의 연관성을 설명합니다.

DOM의 빠른 갱신 시 발생하는 성능 문제

낮은 버전의 브라우저 특히 IE 에서는 스크립트를 사용하여 DOM을 빠르게 갱신하는 경우 심각한 성능저하를 발생시킨다. 아직까지 각 사이트에서 많이 사용되는 IE7을 기준으로 예제를 작성하였다.
var targetTb = document.getElementById(tbl_id);
var rowLength = targetTb.rows.length - 1;
for ( var i = 1; i < rowLength; i += 2) {
preCell = targetTb.rows[i].cells[0];
currCell = targetTb.rows[i + 1].cells[0];
preCell.rowSpan = preCell.rowSpan + currCell.rowSpan;
currCell.parentNode.deleteCell(0);
}
<예제1> 테이블 병합이 스크립트는 테이블을 읽어 첫번째 열을 2개씩 묶어 병합하는 스크립트 코드이다. 위 예제를 통해 IE7에서의 성능저하를 체감하기 위해서는 CSS 적용이 필요하다. 눈에 띄는 결과를 보여줄 수 있는 테스트를 위해 2개의 열과 1000개의 행을 가진 테이블을 준비하였으며 아래와 같은 CSS를 설정하였다.
td, th{font-size: 13px;border: 1px solid black;}
th{background-color: #000000; height:40px;color: white;}
td{background-color: #EEEEEE; height:20px;color: black;}
table{border: 1px solid black;border-collapse:collapse;}
<예제1-1> 예제 CSS
브라우저 병합 소요시간(초)
IE 7 8.852s
IE 8 0.224s
IE 9 0.217s
Fire Fox (25) 0.010s
Chrome (30) 0.014s
<표1> 테이블 병합 스크립트[예제1] 브라우저 별 소요시간위 예제의 결과로 IE7에서의 큰 성능저하를 볼 수 있다. 먼저 원인을 짚고 넘어가보자. 성능 저하의 원인은 스크립트가 DOM을 수정함으로써 생기는 리플로우(reflow)에 있다. 리플로우란 브라우저가 DOM요소들을 다시 렌더링하는 과정을 말하는데 최신 브라우저는 DOM이 빠르게 갱신될 때 과도한 렌더링으로 인한 소요시간 증가를 막아주지만 IE7은 그렇지 않다. 예제에서는 500번의 열 크기 변환과 500번의 열 삭제 총 1000번의 리플로우가 있었다. 이러한 많은양의 리플로우가 발생하는 경우 브라우저는 큰 성능저하를 보여주며 웹 페이지의 DOM이 복잡하게 구성되어 있고 CSS가 많이 적용된 사이트일수록 더욱 심해진다.이 문제의 해결책은 바로 DocumentFragment를 사용하는 것이다. DocumentFragment는 화면상에 표시되지 않는 document-like한 컨테이너로 DOM이 갱신될 때 마다 과도한 리플로우를 하지 않도록 한다
var targetTb = document.getElementById(tbl_id);
var frag = document.createDocumentFragment();
frag.appendChild(targetTb);
var rowLength = targetTb.rows.length - 1;
for ( var i = 1; i < rowLength; i += 2) {
	preCell = targetTb.rows[i].cells[0];
	currCell = targetTb.rows[i + 1].cells[0];
	preCell.rowSpan = preCell.rowSpan + currCell.rowSpan;
	currCell.parentNode.deleteCell(0);
} // 조작된 DOM을 원래 위치에 넣는다.
document.getElementById('test').appendChild(frag.cloneNode(true));
<예제2>DocumentFragment를 사용한 리플로우를 최소화 한 DOM 갱신
브라우저 병합 소요시간(초)
IE 7 3.647s
IE 8 0.166s
IE 9 0.044s
Fire Fox (25) 0.008s
Chrome (30) 0.006s

<표2> – 브라우저 별 병합 스크립트 소요시간 2

1000회의 리플로우 횟수를 2회로 줄인 결과 예제 스크립트는 IE7에서 기존 코드 대비 약 3배의 성능향상이 있었다. 다른 브라우저도 컴퓨터 성능에 따라 약간씩 다르지만 평균적으로 2배의 성능향상을 보여준다. IE에서 가장 성능저하가 매우 심한 테이블로 예제를 작성하였으나 리플로우를 줄임으로써 얻는 최적화 방식은 IE만이 아닌 모든 브라우저에서 스크립트로 DOM을 조작하는 모든 영역에서 활용할 수 있는 방식임을 예제를 통해 알 수 있다.

DocumentFragment는 본래 용도인 생성에도 빠른 성능을 보여준다.

var div = document.getElementsByTagName("div");
for ( var i = 0; i < div.length; i++) {
	for ( var e = 0; e < elems.length; e++) {
		div[i].appendChild(elems[e].cloneNode(true));
	}
}

<예제3-1> appendChild를 사용한 DOM 그리기

var div = document.getElementsByTagName("div");
var frag = document.createDocumentFragment();
for ( var e = 0; e < elems.length; e++) {
	frag.appendChild(elems[e]);
}
for ( var i = 0; i < div.length; i++) {
	div[i].appendChild(frag.cloneNode(true));
}

<예제3-2> documentFragment를 사용한 DOM 그리기

브라우저 예제3-1 소요시간(초) 예제3-2 소요시간(초)
IE 7 0.201s 0.080s
IE 8 0.104s 0.030s
IE 9 0.044s 0.020s
Fire Fox (25) 0.005s 0.002s
Chrome (30) 0.003s 0.000s

<표2> 브라우저별 DOM 그리기 소요시간

단순한 DOM 변경에도 평균적으로 2배의 성능향상이 있었다.

마치며

DocumentFragment을 사용한 DOM 조작 스크립트 최적화 방법은 웹 개발에서 흔하게 무시되곤 하는 영역이다. 최신 브라우저의 경우 리플로우가 발생하지 않도록 최적화되고 성능이 향상되어 그 차이가 체감되지 않기 때문이다. 그러나 ActiveX 기반의 국내 웹 환경상 낮은 버전의 IE를 요구하는 경우가 분명히 존재하며 그런 환경이 아니더라도 많은 DOM을 조작해야만 하는 역동적인 화면에서의 브라우저의 부담을 덜어주고 빠른 화면갱신을 위해서 DocumentFragment를 활용하면 충분한 최적화를 얻을 수 있을 것이다.