가상 DOM의 오해와 진실
2025년 3월 16일
목차
- 들어가면서: 가상 DOM에 대한 흔한 오해
- 리액트와 가상 DOM
- 가상 DOM에 대한 말말말
- 가상 DOM의 대안: 피하거나, 개선하거나
- 그렇다면 가상 DOM을 기피해야 하는가?
- 나오면서
- 참고 자료
들어가면서: 가상 DOM에 대한 흔한 오해
"리액트의 가상 DOM은 실제 DOM보다 빠르다."
리액트를 학습하는 과정에서 자주 접하게 되는 주장입니다. 이 문장은 많은 개발자들에게 사실로 받아들여지고 있습니다. 그러나 이는 절반만 맞는 말입니다. 가상 DOM이 특정 상황에서 성능상 이점을 제공하는 것은 사실이지만, 모든 상황에서 항상 빠른 것은 아닙니다.
리액트와 가상 DOM
가상 DOM의 등장 배경
먼저 가상 DOM의 등장 배경에 대해서 알아보기 전에, 기존의 웹 페이지 렌더링 과정에 대해서 살펴봅시다.
단계 | 설명 |
---|---|
HTML 파싱 | HTML 문서를 파싱하여 DOM 트리를 생성합니다. |
CSS 파싱 | CSS를 파싱하여 CSSOM 트리를 생성합니다. |
렌더 트리 구성 | DOM 트리와 CSSOM 트리를 결합해 렌더 트리를 생성합니다. |
레이아웃 | 각 요소의 정확한 위치와 크기를 계산합니다 |
페인트 | 계산된 위치와 스타일에 따라 각 요소를 화면에 픽셀로 그립니다. |
이것이 브라우저가 웹페이지를 렌더링하는 일반적인 과정이며, 레이아웃이 발생하면 페인팅 역시 필연적으로 발생하게 됩니다. 브라우저는 이러한 과정을 거쳐 사용자에게 완성된 웹페이지를 제공해 왔습니다.
만약 특정 요소의 위치와 크기가 변경될 경우, 앞서 언급한 것처럼 레이아웃-페인팅 작업이 발생하게 됩니다. 또한 DOM 변경이 일어나는 요소가 많은 자식 요소를 가지고 있다면, 변경이 발생할 때 하위 자식 요소도 같이 렌더링됩니다. 이러한 추가 작업은 SPA가 본격적으로 부상하면서 점차 늘어나기 시작했습니다. 하나의 페이지에서 변경되는 요소를 계속해서 계산하는 경우가 증가하기 시작한 것입니다.
가상 DOM
은 이러한 문제를 해결하기 위해서 등장했습니다. 가상 DOM은 실제 브라우저 DOM의 가벼운 사본으로, 메모리상에서 변경 사항을 먼저 계산한 후 최종 결과만 실제 DOM에 반영합니다. 이 접근법은 여러 변경 사항을 일괄적으로 처리하여 불필요한 렌더링을 최소화하는 데 도움을 줍니다.
리액트 파이버: 가상 DOM을 지탱하는 힘
리액트는 가상 DOM을 리액트 파이버(React fiber)
라는 아키텍처-이자 객체-를 통해 관리합니다. 리액트 파이버는 작업 단위로 구성되어 있으며 파이버 재조정자(fiber reconciler)에 의해 관리됩니다.
리액트 파이버의 핵심은 렌더링 작업을 작은 단위로 쪼개고 우선순위를 부여하는 능력입니다. 이를 통해 작업을 임시 중단하고 나중에 재개할 수 있으며, 자바스크립트의 싱글 스레드 환경에서도 비동기적인 렌더링을 가능하게 합니다.

작동 방식은 이중 트리 구조를 기반으로 합니다. Current
트리는 현재 화면에 표시된 UI를, WorkInProgress
트리는 변경 작업이 진행 중인 상태를 나타냅니다. 모든 변경 작업이 완료되면 리액트는 단순히 포인터를 변경하여 WorkInProgress
트리를 새로운 Current
트리로 만듭니다.
효율성 측면에서 리액트 파이버는 최초 렌더링 시에만 새 파이버를 생성하고, 이후 업데이트에서는 변경점을 기반으로 가급적 기존 파이버를 재사용합니다. 이러한 구조를 통해 리액트는 복잡한 UI 업데이트를 예측 가능하고 효율적인 방식으로 처리할 수 있습니다.
리액트의 렌더링 프로세스
리액트 파이버를 통해, 리액트는 최초 렌더링 시 다음과 같은 과정을 진행합니다.
단계 | 설명 |
---|---|
1. 초기화 | React 엘리먼트 생성 |
2. 가상 DOM 생성 | 컴포넌트 트리에 대한 최초 가상 DOM 구조 생성 |
3. 파이버 트리 구축 | 각 컴포넌트에 대한 파이버 노드 생성 및 트리 구조화 |
4. 렌더 단계 | 파이버 트리를 순회하며 모든 컴포넌트의 렌더 함수 실행 |
5. 커밋 단계 | 파이버 트리의 변경사항을 실제 DOM에 반영 |
6. 레이아웃 계산 | 브라우저가 요소의 크기와 위치를 계산 |
7. 페인팅 | 브라우저가 모든 시각적 요소를 화면에 그림 |
이후 업데이트가 발생해 리렌더링이 진행될 경우 렌더 단계에서 재조정 작업을 실행합니다. 이 재조정 단계에서 Diffing 알고리즘을 적용해 변경점을 식별하는데, 이 알고리즘이 바로 오늘 하고자 하는 이야기의 주인공입니다.
Diffing 알고리즘

Diffing 알고리즘
은 가상 DOM의 이전 버전과 새 버전을 비교해 실제 DOM에서 변경해야 할 최소한의 요소를 식별하는 과정입니다. 리액트는 다른 타입의 엘리먼트는 다른 트리를 생성한다는 가정 하에, 업데이트가 발생했을 때 두 루트 엘리먼트부터 시작해 트리를 동시에 순회하며 비교 작업을 진행합니다.
이때 엘리먼트 타입을 비교하여 타입이 같으면 속성만 업데이트하고 자식 요소로 재귀적으로 진행하지만, 타입이 다르면 이전 트리를 완전히 해체하고 새 트리를 생성합니다. DOM 노드의 경우 같은 타입이면 속성만 업데이트하고 내부 변경을 진행하지만, 다른 타입이면 전체 서브트리를 교체합니다. 컴포넌트는 같은 타입이면 인스턴스를 유지하며 props만 업데이트하지만, 다른 타입이면 이전 컴포넌트를 언마운트하고 새 컴포넌트를 마운트합니다. 마지막으로 자식 리스트는 key가 없으면 순서대로 비교하고, key가 있으면 이를 기준으로 일치항목을 찾아 효율적으로 DOM을 조작합니다.
이를 기반으로 세울 수 있는 리액트의 최적화 케이스는 다음과 같습니다.
상황 | 결과 | 효율성 |
---|---|---|
같은 위치, 같은 타입 | 속성만 변경 | 가장 효율적 |
같은 타입, 다른 위치, key 있음 | 실제 DOM 이동으로 해결 | 효율적 |
다른 타입 | 전체 서브트리 재생성 | 비효율적 |
가상 DOM에 대한 말말말
얼핏 보면 이러한 Diffing 알고리즘이 굉장히 효율적인 것처럼 보입니다. 그러나 최근 몇 년간 가상 DOM의 성능에 대한 의문은 끊임없이 제기되어 왔습니다. 특히 Svelte(스벨트)의 창시자인 리치 해리스가 작성한 Virtual DOM is pure overhead라는 도발적인 제목의 글이 이 논쟁에 불을 지폈습니다.
리치 해리스는 그의 글에서 "가상 DOM이 빠르다"는 문구가 일종의 밈이 되었다고 지적하며, 가상 DOM 자체가 기능이 아닌 수단이라고 주장했습니다.
"가상 DOM은 기능이 아닙니다. 그것은 목적을 달성하기 위한 수단이며, 그 목적은 선언적이고 상태 주도적인 UI 개발입니다. 가상 DOM은 상태 전환에 대해 생각하지 않고도 일반적으로 충분히 좋은 성능으로 앱을 구축할 수 있게 해주기 때문에 가치가 있습니다." (원문 번역)
그의 주장에 따르면, Diffing 작업 자체가 불필요한 오버헤드처럼 느껴집니다. 앞서 이야기한 것처럼, 가상 DOM을 사용할 때 실제 DOM 업데이트에 앞서 새로운 가상 DOM과 이전 가상 DOM의 스냅샷을 비교하는 Diffing 작업이 항상 수반됩니다. 그러나 대부분의 업데이트에서는 앱의 기본 구조가 크게 변경되지 않기 때문에, 이 작업이 불필요한 경우가 많다는 것입니다.
가상 DOM의 대안: 피하거나, 개선하거나
이러한 단점을 가진 Diffing 알고리즘을 사용하는 가상 DOM에 대한 여러가지 시도가 있었습니다. 그 중 두 가지 사례만 소개하고자 합니다.
Svelte
"런타임에서 조정하지 말고, 컴파일 타임에 최적화하라."
Svelte는 가상 DOM을 사용하지 않는 대신, 컴파일 타임에 DOM을 업데이트하는 코드를 생성합니다. 간단한 카운터 예시를 하나 살펴봅시다.
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
클릭 수: {count}
</button>
리액트에서는 버튼을 클릭할 때마다 컴포넌트 함수가 재실행되고, 새로운 가상 DOM 트리가 생성된 후 이전 트리와 비교(diffing)하는 과정을 거칩니다. 반면 스벨트는 컴파일 시점에 count 변수가 변경될 때 어떤 DOM 요소를 어떻게 업데이트해야 하는지 미리 파악하고, 런타임에는 해당 요소만 직접 업데이트합니다.
Million.js
"DOM을 비교하지 말고, 데이터를 비교하라."
Million.js는 기존의 가상 DOM을 개선한 블록 가상 DOM(Block Virtual DOM)
이라는 새로운 접근법을 제시합니다. 이 방식은 정적 분석과 더티 체킹이라는 특징을 가지고 있습니다.
정적 분석(Static Analysis)은 가상 DOM을 분석하여 동적 부분을 편집 맵(Edit Map)으로 추출합니다. 이는 가상 DOM의 동적 부분과 상태 간의 매핑 목록입니다. 더티 체킹(Dirty Checking)은 가상 DOM 트리가 아닌 상태만 비교하여 변경 사항을 결정합니다. 상태가 변경되면 편집 맵을 통해 DOM을 직접 업데이트합니다.
import { useState } from 'react';
import { block } from 'million/react';
function Count() {
const [count, setCount] = useState(0);
const node1 = count + 1;
const node2 = count + 2;
return (
<div>
<ul>
<li>{node1}</li>
<li>{node2}</li>
</ul>
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment Count
</button>
</div>
);
}
const CountBlock = block(Count);
위 예시에서 Million.js는 정적 분석 단계에서 동적 부분(node1과 node2)을 추출하고, 상태(count)가 변경될 때 이 부분만 업데이트합니다.
그렇다면 가상 DOM을 기피해야 하는가?
그렇다면 가상 DOM은 기피해야 할 대상일까요? 그렇지는 않습니다. Million.js에 대한 글에서 언급한 것처럼, 동적 콘텐츠가 많은 경우에는 여전히 전통적인 가상 DOM 방식이 효율적입니다.
이론적인 논의를 넘어, 실제 성능 비교 결과에서도 가상 DOM을 기피하지는 않아야 한다는 것을 확인할 수 있습니다.
2020년에 발표된 논문 DOM benchmark comparison of the front-end JavaScript frameworks React, Angular, Vue, and Svelte에서는 다양한 자바스크립트 프레임워크에서의 DOM 조작 성능 벤치마크 지표를 확인할 수 있습니다.
실험 유형 | React v16.12.0 | Vue v2.6.11 | Angular v8.2.14 | Svelte v3.20.0 |
---|---|---|---|---|
DOM 요소 10,000개 추가 (ms) | 30.96 | 25.36 | 52.75 | 31.26 |
단일 요소 수정 (ms) | 16.58 | 22.23 | 6.08 | 0.11 |
10,000개 요소 수정 (ms) | 17.86 | 20.64 | 896.76 | 885.03 |
단일 요소 제거 (ms) | 16.54 | 24.51 | 0.09 | 0.53 |
10,000개 요소 제거 (ms) | 7.39 | 33.33 | 23.83 | 22.97 |
컴파일 속도 (s) | 3.96 | 3.07 | 8.70 | 1.61 |
벤치마크 결과에서 확인할 수 있듯, 단일 요소 조작은 직접 DOM을 조작하는 스벨트가 압도적으로 빠른 반면, 대량 요소를 조작하는 경우에는 가상 DOM을 사용하는 리액트가 압도적인 성능을 보입니다. 가상 DOM이 모든 상황에서 빠른 것은 아니지만, 대규모 DOM 업데이트가 필요한 복잡한 애플리케이션에서는 여전히 매우 효율적임을 알 수 있습니다.
나오면서
"리액트는 가상 DOM이 대부분의 상황에서 웬만한 애플리케이션을 개발할 수 있을 만큼 합리적으로 빠르기 때문에 이를 채용했다."
가상 DOM은 단순히 성능 최적화를 위한 도구가 아닌, 리액트의 근간을 이루는 핵심 개념입니다. 리액트가 가상 DOM을 도입한 주된 목적은 개발자들이 선언적이고 상태 중심적인 UI 개발 방식을 채택할 수 있도록 하는 데 있습니다. 이는 복잡한 상태 전환과 DOM 조작 로직을 직접 관리하지 않고도 예측 가능하고 일관된 방식으로 애플리케이션을 구축할 수 있게 해줍니다.
앞선 벤치마크 결과에서 확인했듯이, 가상 DOM은 모든 상황에서 최고의 성능을 보장하지는 않습니다. 단일 요소의 조작에서는 Svelte와 같이 컴파일 타임 최적화를 사용하는 접근법이 더 효율적일 수 있습니다. 그러나 대규모 DOM 업데이트나 복잡한 UI 구조에서는 가상 DOM의 일괄 처리 방식이 여전히 강력한 이점을 제공합니다.
따라서 "가상 DOM이 항상 실제 DOM보다 빠르다"는 주장은 정확하지 않지만, 동시에 가상 DOM을 무조건 기피해야 한다는 주장 역시 지나친 단순화입니다. 리액트의 창시자들이 언급했듯이, 가상 DOM은 대부분의 웹 애플리케이션에서 충분히 빠른 성능을 제공하면서도 개발자 경험을 크게 향상시키는 균형 잡힌 접근법입니다.
오늘날 웹 개발의 다양한 요구사항을 고려할 때, 프로젝트의 특성과 요구사항에 맞는 기술을 선택하는 것이 중요합니다. 작은 규모의 인터랙티브 컴포넌트에는 스벨트와 같은 컴파일 타임 최적화 방식이 유리할 수 있으며, 대규모 엔터프라이즈 애플리케이션에서는 리액트와 같은 가상 DOM 기반 프레임워크가 더 적합할 수 있습니다. Million.js와 같은 하이브리드 접근법도 특정 상황에서 좋은 대안이 될 수 있습니다.
결론적으로, 가상 DOM에 대한 미신에서 벗어나 각 기술의 장단점을 정확히 이해하고, 상황에 맞게 적절한 도구를 선택하는 것이 현명한 개발자의 자세일 것입니다. 웹과 그 기반 기술은 계속해서 진화하고 있으며, 우리는 이러한 발전을 열린 마음으로 받아들이고 배워나가야 합니다.