2025-10-06 21:09

  • 가상돔(Virtual DOM)은 실제 DOM 조작의 성능 문제를 해결하기 위해 메모리에 가상의 DOM 트리를 만들고, 변화가 있을 때 이 가상 트리를 통해 효율적으로 실제 DOM을 업데이트하는 프로그래밍 개념이다.

  • 리액트(React)와 같은 현대적인 자바스크립트 라이브러리 및 프레임워크의 핵심 기술로, 개발자가 UI의 상태에만 집중할 수 있도록 하여 생산성을 크게 향상시킨다.

  • 가상돔은 변경 사항을 일괄적으로 처리하는 ‘배칭(Batching)‘과 이전 가상돔과 현재 가상돔의 차이를 비교하는 ‘비교 알고리즘(Diffing Algorithm)‘을 통해 불필요한 DOM 접근을 최소화하고 렌더링 성능을 최적화한다.


가상돔 완벽 정복 핸드북 DOM을 직접 만지지 않는 개발의 시작

웹 개발의 패러다임을 바꾼 혁신적인 개념, 가상돔(Virtual DOM)에 대해 얼마나 알고 있는가? 많은 개발자가 리액트(React), 뷰(Vue)와 같은 프레임워크를 사용하며 무심코 가상돔의 혜택을 누리고 있지만, 그 내부 동작 원리나 탄생 배경을 깊이 이해하는 경우는 드물다. 이 핸드북은 가상돔이 왜 필요했으며, 어떻게 동작하고, 이를 통해 우리가 무엇을 얻었는지에 대한 포괄적이고 깊이 있는 탐구를 제공한다. 단순히 ‘빠르다’는 표면적인 이해를 넘어, 가상돔의 철학과 구조를 완벽하게 정복해 보자.

1. 가상돔은 왜 태어났을까 절실함이 낳은 혁신

가상돔의 탄생 배경을 이해하려면 먼저 그것이 해결하고자 했던 문제, 즉 DOM(Document Object Model) 조작의 비효율성에 대해 알아야 한다.

1.1 DOM 느림의 역설

DOM은 브라우저가 웹 페이지의 콘텐츠와 구조를 표현하는 방식이다. HTML 문서를 파싱하여 노드(Node) 트리 형태로 메모리에 저장한 것이 바로 DOM이다. 자바스크립트는 이 DOM API를 통해 웹 페이지의 요소를 동적으로 변경하고, 사용자와의 상호작용을 구현할 수 있다.

문제는 이 과정이 생각보다 훨씬 비용이 많이 드는 작업이라는 점이다. DOM 노드에 변화가 생길 때마다 브라우저는 다음과 같은 복잡한 과정을 거친다.

  1. 스타일 계산 (Recalculate Style): 변경된 요소뿐만 아니라, 관련된 다른 요소들의 CSS 스타일까지 다시 계산한다. 예를 들어, 자식 요소의 크기가 바뀌면 부모 요소도 영향을 받을 수 있다.

  2. 레이아웃 (Layout/Reflow): 각 요소가 화면의 어디에 위치하고 어떤 크기를 가질지 다시 계산한다. 이 과정은 문서 전체에 영향을 미칠 수 있어 특히 비용이 크다.

  3. 페인트 (Paint): 계산된 레이아웃에 따라 실제 픽셀을 화면에 그린다.

  4. 합성 (Composite): 여러 레이어를 순서대로 합쳐 최종적인 화면을 완성한다.

단순히 div 요소 하나의 배경색을 바꾸는 작업조차도 이러한 과정을 유발할 수 있다. 만약 수십, 수백 개의 요소가 짧은 시간 안에 계속해서 변경된다면 어떻게 될까? 브라우저는 쉴 새 없이 레이아웃 계산과 페인팅을 반복하게 되고, 이는 곧 성능 저하와 사용자 경험 악화로 이어진다.

1.2 시대의 요구 동적인 웹 애플리케이션의 등장

과거의 웹 페이지는 대부분 정적이었다. 페이지가 한번 로드되면 내용이 거의 바뀌지 않았다. 하지만 페이스북, 트위터와 같은 소셜 미디어와 Gmail 같은 단일 페이지 애플리케이션(SPA, Single Page Application)이 등장하면서 웹의 패러다임이 바뀌었다.

이러한 애플리케이션들은 실시간으로 데이터가 업데이트되고, 사용자의 인터랙션에 따라 UI가 수시로 변해야 했다. 개발자들은 이제 거대한 데이터 목록을 렌더링하고, 특정 데이터가 변경될 때마다 UI의 일부를 정확하고 빠르게 업데이트해야 하는 과제에 직면했다.

전통적인 방식, 즉 jQuery와 같은 라이브러리를 사용하여 직접 DOM을 조작하는 방식은 프로젝트의 복잡도가 증가할수록 한계를 드러냈다. 개발자는 애플리케이션의 ‘상태(State)‘를 관리하는 동시에, 이 상태가 변경될 때 어떤 DOM 요소를 어떻게 바꿔야 할지 일일이 명령해야 했다. 이는 코드를 복잡하게 만들고, 버그를 유발하며, 최적의 성능을 보장하기 어려웠다.

1.3 가상돔의 탄생 선언적 UI와 성능의 조화

이러한 문제의식 속에서 “DOM 조작을 최소화하면서 어떻게 UI를 효율적으로 업데이트할 수 있을까?” 라는 질문에 대한 답으로 가상돔이 등장했다. 페이스북의 리액트 팀은 이 문제를 해결하기 위해 발상의 전환을 했다.

“실제 DOM을 직접 건드리는 대신, 메모리에 가상의 DOM을 만들고, 변화가 생기면 이 가상의 DOM을 먼저 수정한 다음, 실제 DOM과의 차이점만 찾아내서 딱 한 번만 실제 DOM을 업데이트하면 어떨까?”

이것이 바로 가상돔의 핵심 아이디어다.

가상돔은 실제 DOM의 구조를 흉내 내는 가벼운 자바스크립트 객체다. 이 객체는 실제 DOM처럼 복잡한 API나 렌더링 엔진과의 연결 없이 순수하게 메모리상에만 존재하기 때문에 생성하고 수정하는 비용이 거의 들지 않는다.

개발자는 더 이상 “어떻게” 바꿀지를 고민할 필요 없이, 그저 데이터(상태)가 변했을 때 UI가 “어떤 모습이어야 하는지”만 선언적으로 정의하면 된다. 그러면 가상돔이 나머지 복잡한 과정을 알아서 처리해 준다.

  • 변경 전 가상돔 트리변경 후 가상돔 트리를 비교한다.

  • 두 트리의 **차이점(Diff)**을 찾아낸다.

  • 이 차이점들을 모아서 실제 DOM에 단 한 번의 작업(Batch Update)으로 적용한다.

이 방식을 통해 불필요한 레이아웃 계산과 페인팅을 최소화하고, DOM 접근 횟수를 획기적으로 줄여 성능을 극대화할 수 있게 되었다. 가상돔은 복잡한 동적 웹 애플리케이션을 구축하는 데 필요한 성능개발 생산성이라는 두 마리 토끼를 모두 잡은 혁신적인 해결책이었다.

2. 가상돔의 구조와 작동 원리 깊게 들여다보기

가상돔의 ‘왜’를 이해했다면, 이제 ‘어떻게’ 동작하는지 그 내부를 살펴볼 차례다. 가상돔의 핵심은 **비교(Diffing)**와 **조정(Reconciliation)**이라는 두 가지 메커니즘에 있다.

2.1 가상돔의 실체 자바스크립트 객체

가상돔은 이름 때문에 무언가 거창하고 복잡한 기술처럼 보이지만, 그 본질은 단순하다. 아래는 가상돔 노드를 표현하는 가장 기본적인 자바스크립트 객체의 예시다.

JavaScript

const vNode = {
  tagName: 'div',
  props: {
    id: 'container',
    class: 'main'
  },
  children: [
    {
      tagName: 'h1',
      props: { class: 'title' },
      children: ['Hello, Virtual DOM!']
    },
    {
      tagName: 'p',
      props: {},
      children: ['This is a paragraph.']
    }
  ]
};

이 객체는 실제 DOM의 <div>, <h1>, <p> 태그 구조와 속성, 자식 요소들을 그대로 묘사하고 있다. 실제 DOM 요소가 가진 수많은 프로퍼티와 메서드 없이, 오직 렌더링에 필요한 최소한의 정보만을 담고 있어 매우 가볍다. 리액트에서는 이를 **리액트 엘리먼트(React Element)**라고 부른다.

2.2 조정 과정 (Reconciliation)

애플리케이션의 상태가 변경되면(예: 사용자가 버튼을 클릭하거나, 서버에서 새로운 데이터를 받아왔을 때), 라이브러리(리액트 등)는 새로운 상태를 기반으로 새로운 가상돔 트리를 생성한다. 그리고 이전에 메모리에 저장해 두었던 이전 가상돔 트리와 비교하여 변경 사항을 찾아내는 조정 과정을 시작한다.

이 과정의 핵심은 **비교 알고리즘(Diffing Algorithm)**이다.

2.3 비교 알고리즘 (Diffing Algorithm) 어떻게 차이를 찾아낼까?

두 개의 일반적인 트리를 비교하여 최소한의 차이점을 찾는 알고리즘은 복잡도가 에 달할 정도로 매우 비효율적이다. 하지만 리액트와 같은 라이브러리들은 웹 애플리케이션의 특성을 고려한 두 가지 현실적인 가정 위에서 의 복잡도로 이 문제를 해결하는 휴리스틱(Heuristic) 알고리즘을 사용한다.

가정 1: 서로 다른 타입의 엘리먼트는 서로 다른 트리를 생성한다.

만약 루트 엘리먼트의 타입이 <div>에서 <span>으로 바뀌었다면, 리액트는 굳이 두 트리를 비교하려 하지 않는다. 그냥 기존 트리를 통째로 버리고(Unmount), 완전히 새로운 트리를 처음부터 구축(Mount)한다. 이는 합리적인 가정이다. 전혀 다른 컴포넌트가 렌더링되는 상황에서 내부 구조를 재사용하려는 시도 자체가 비효율적일 가능성이 높기 때문이다.

변경 전변경 후결과
<div>...</div><span>...</span>이전 div와 그 자식들 모두 파괴, 새로운 span 트리 생성
<Counter /><Header />Counter 컴포넌트 인스턴스 파괴, Header 컴포넌트 인스턴스 생성

가정 2: 개발자가 key prop을 통해 여러 렌더링 사이에서 어떤 자식 엘리먼트가 그대로 유지되는지 표시해 줄 수 있다.

배열(리스트)을 렌더링하는 경우는 비교 알고리즘에서 가장 흥미로운 부분이다. 예를 들어, 아래와 같이 3명의 사용자를 렌더링한다고 가정해 보자.

변경 전:

HTML

<ul>
  <li>Alice</li>
  <li>Bob</li>
  <li>Charlie</li>
</ul>

이제 이 리스트의 맨 앞에 ‘David’를 추가하면 어떻게 될까?

변경 후:

HTML

<ul>
  <li>David</li>
  <li>Alice</li>
  <li>Bob</li>
  <li>Charlie</li>
</ul>

만약 key가 없다면, 리액트는 단순히 순서대로 비교한다.

  1. <li>Alice</li><li>David</li>로 바뀌었네? (DOM 내용 수정)

  2. <li>Bob</li><li>Alice</li>로 바뀌었네? (DOM 내용 수정)

  3. <li>Charlie</li><li>Bob</li>으로 바뀌었네? (DOM 내용 수정)

  4. 맨 뒤에 <li>Charlie</li>를 새로 추가해야겠군. (DOM 노드 추가)

총 3번의 수정과 1번의 추가 작업이 발생한다. 매우 비효율적이다.

하지만 각 엘리먼트에 **고유하고 안정적인 key**를 제공하면 리액트는 훨씬 똑똑하게 작동한다.

변경 전 (key 사용):

JavaScript

<ul>
  <li key="alice">Alice</li>
  <li key="bob">Bob</li>
  <li key="charlie">Charlie</li>
</ul>

변경 후 (key 사용):

JavaScript

<ul>
  <li key="david">David</li>
  <li key="alice">Alice</li>
  <li key="bob">Bob</li>
  <li key="charlie">Charlie</li>
</ul>

이제 리액트는 key를 보고 어떤 엘리먼트가 그대로 유지되었는지, 어떤 것이 새로 추가되었는지, 또는 순서가 바뀌었는지를 명확하게 알 수 있다.

  1. 이전 트리에는 david 키가 없었군. 새로운 노드구나. (<li>David</li> 추가)

  2. alice, bob, charlie 키는 이전 트리에도 있었네. 순서만 바뀌었을 뿐, 노드를 재사용할 수 있겠다. (기존 노드들 이동)

이 경우, 단 한 번의 노드 추가 작업만으로 업데이트가 끝난다. 이처럼 key는 리스트 렌더링 시 성능 최적화에 매우 중요한 역할을 한다. key는 형제 엘리먼트 사이에서만 고유하면 되며, 배열의 인덱스를 key로 사용하는 것은 리스트의 순서가 바뀌거나 항목이 추가/삭제될 때 버그를 유발할 수 있으므로 권장되지 않는다.

2.4 일괄 처리 (Batching)

가상돔의 또 다른 중요한 최적화 기법은 **일괄 처리(Batching)**다. 짧은 시간 동안 발생하는 여러 번의 상태 변경을 한 번에 모아서 처리하는 것이다.

예를 들어, 사용자가 버튼을 클릭했을 때 counttext라는 두 개의 상태를 동시에 변경한다고 가정해 보자.

JavaScript

function handleClick() {
  setCount(c => c + 1); // 상태 변경 1
  setText('Updated');     // 상태 변경 2
}

만약 Batching이 없다면, setCount가 호출될 때마다 리렌더링이 일어나고, setText가 호출될 때 또 리렌더링이 일어날 것이다. 이는 불필요한 렌더링을 두 번이나 유발한다.

하지만 리액트는 이벤트 핸들러 내에서 발생하는 상태 변경들을 하나의 큐(Queue)에 모아두었다가, 이벤트 핸들러가 끝나는 시점에 단 한 번만 리렌더링을 실행한다. 이를 통해 여러 상태 변경이 단일 업데이트로 처리되어 성능을 크게 향상시킨다.

3. 가상돔 사용법과 장점 개발자가 얻는 것들

가상돔의 내부 동작을 이해했다면, 이제 이것이 실제 개발 경험에 어떤 영향을 미치는지 알아보자.

3.1 선언적 UI와 개발 생산성 향상

가장 큰 장점은 선언적(Declarative) 프로그래밍이 가능해진다는 것이다.

  • 명령형(Imperative) 프로그래밍: “어떻게(How)” 할지를 지시한다. (예: “ID가 ‘title’인 엘리먼트를 찾아서, 텍스트를 ‘New Title’로 바꿔라.“)

  • 선언형(Declarative) 프로그래밍: “무엇(What)“을 원하는지를 선언한다. (예: “타이틀은 ‘New Title’이어야 한다.“)

가상돔이 없다면 우리는 DOM을 직접 조작하는 명령형 코드를 작성해야 한다. 애플리케이션이 복잡해질수록 상태와 UI의 동기화를 맞추는 코드는 기하급수적으로 늘어나고 관리하기 어려워진다.

하지만 가상돔을 사용하는 리액트에서는 그저 현재 상태에 따라 UI가 어떻게 보여야 하는지만 JSX로 선언하면 된다. 상태가 바뀌면 리액트가 알아서 가상돔을 통해 이전 UI와 비교하고, 최소한의 변경사항을 실제 DOM에 반영해 준다. 개발자는 복잡한 DOM 조작 과정에서 해방되어 오직 애플리케이션의 로직과 상태 관리에만 집중할 수 있다.

3.2 플랫폼 독립성

가상돔은 특정 렌더링 환경에 종속되지 않는 추상적인 개념이다. 가상돔 트리를 실제 DOM에 렌더링하면 **리액트돔(react-dom)**이 되고, 네이티브 모바일 UI로 렌더링하면 **리액트 네이티브(React Native)**가 된다. 이처럼 가상돔은 브라우저 너머의 세상으로 리액트의 생태계를 확장하는 중요한 발판이 되었다.

3.3 성능 최적화

앞서 설명했듯이, 가상돔은 DOM 조작을 최소화하여 대부분의 경우에 뛰어난 성능을 보장한다. 물론, 모든 상황에서 가상돔이 직접 DOM을 조작하는 것보다 빠른 것은 아니다. 아주 간단하고 정적인 페이지나, 고도로 최적화된 DOM 조작 코드를 작성할 수 있는 경우라면 직접 조작이 더 빠를 수도 있다.

하지만 복잡하고 동적인 대규모 애플리케이션에서는 가상돔의 비교 알고리즘과 일괄 처리 기능이 주는 성능상의 이점이 훨씬 크다. 개발자가 일일이 최적화를 신경 쓰지 않아도 “충분히 빠른” 성능을 기본적으로 제공한다는 것이 가상돔의 강력함이다.

4. 심화 내용 가상돔을 넘어서

가상돔은 웹 개발에 큰 영향을 미쳤지만, 완벽한 만병통치약은 아니다. 가상돔의 한계와 이를 극복하려는 새로운 시도들도 계속해서 등장하고 있다.

4.1 가상돔의 비용

가상돔에도 비용은 존재한다.

  1. 메모리 사용: 실제 DOM 외에도 가상돔 트리를 메모리에 유지해야 하므로 추가적인 메모리 사용량이 발생한다. 모바일 환경이나 저사양 기기에서는 부담이 될 수 있다.

  2. 비교 알고리즘 실행 시간: 상태가 변경될 때마다 가상돔 트리를 생성하고 비교하는 과정 자체에도 계산 비용이 든다. 컴포넌트 트리가 매우 거대하고 복잡해지면 이 비교 과정이 병목 지점이 될 수도 있다.

리액트는 이러한 비용을 최소화하기 위해 shouldComponentUpdate 생명주기 메서드나 React.memo, useMemo, useCallback과 같은 도구를 제공하여 불필요한 리렌더링과 비교 연산을 건너뛸 수 있도록 지원한다.

4.2 가상돔의 대안들

최근 프레임워크 생태계에서는 가상돔의 비용을 없애고 더 나은 성능을 추구하려는 움직임이 활발하다.

  • Svelte (스벨트): 스벨트는 가상돔을 사용하지 않는다. 대신, 코드를 작성하는 빌드(Build) 시점에 코드를 분석하여, 상태가 변경될 때 어떤 DOM을 어떻게 업데이트해야 하는지에 대한 정밀한 코드를 미리 생성해 낸다. 런타임에 가상돔을 비교하는 과정이 아예 없기 때문에 더 가볍고 빠르다.

  • Solid.js (솔리드): 솔리드는 리액트와 유사한 JSX 문법을 사용하지만, 가상돔 대신 세밀한 반응성(Fine-grained Reactivity) 시스템을 기반으로 동작한다. 컴포넌트 단위로 리렌더링하는 대신, 상태가 변경되면 그 상태를 사용하는 DOM 부분만 직접 업데이트한다. 이를 통해 가상돔 비교 과정 없이도 매우 효율적인 업데이트를 구현한다.

이러한 프레임워크들은 가상돔이 유일한 해답이 아니며, 웹 개발의 성능 최적화는 계속해서 진화하고 있음을 보여준다.

결론 미래를 향한 디딤돌

가상돔은 웹 개발의 역사에서 하나의 이정표다. 이는 단순히 성능 문제를 해결한 기술을 넘어, 개발자가 UI를 바라보는 관점을 명령형에서 선언형으로 바꾸는 패러다임의 전환을 이끌었다. 복잡한 UI 업데이트 로직을 추상화 계층 뒤로 숨겨줌으로써, 개발자들이 더 복잡하고 동적인 애플리케이션을 더 쉽게 만들 수 있는 길을 열어주었다.

비록 스벨트나 솔리드와 같은 새로운 경쟁자들이 가상돔의 대안을 제시하고 있지만, 가상돔이 지난 몇 년간 웹 생태계에 미친 영향력과 그 견고함은 여전히 무시할 수 없다. 리액트와 뷰 생태계는 여전히 건재하며, 가상돔의 원리를 이해하는 것은 현대 웹 프레임워크의 근간을 이해하는 것과 같다.

이 핸드북을 통해 가상돔의 탄생 배경부터 내부 동작, 그리고 그 한계까지 깊이 있게 이해했기를 바란다. 이제 당신은 리액트 코드를 작성할 때, 그 이면에서 벌어지는 보이지 않는 춤, 즉 가상돔의 정교한 조정 과정을 상상하며 더 나은 코드를 만들어나갈 수 있을 것이다.

레퍼런스(References)

가상돔