🎨 Virtual DOM이란?
📖 정의
Virtual DOM은 실제 DOM의 가벼운 복사본으로, 메모리에만 존재하는 JavaScript 객체입니다. React 같은 라이브러리는 Virtual DOM을 사용하여 UI 변경사항을 먼저 가상으로 처리한 후, 실제 DOM에 최소한의 변경만 적용합니다. 이를 통해 웹 애플리케이션의 성능을 크게 향상시킵니다.
🎯 비유로 이해하기
건축 설계도 비유
Virtual DOM을 건축 설계도에 비유하면:
실제 건물 (Real DOM)
├─ 건축 비용 비쌈
├─ 수정 시간 오래 걸림
└─ 철거하고 다시 짓기 힘듦
설계도면 (Virtual DOM)
├─ 종이 위에서 자유롭게 수정
├─ 빠르고 저렴
├─ 여러 안을 비교 가능
└─ 최종안만 실제 건물에 반영
과정:
1. 설계도에서 여러 번 수정 (Virtual DOM)
2. 변경된 부분만 파악
3. 실제 건물에 최소한만 적용 (Real DOM)
이렇게 하면 건축 비용과 시간을 절약!
워드 프로세서 비유
메모장 (바닐라 JavaScript)
- 타이핑할 때마다 즉시 저장
- 느리고 비효율적
- 파일 입출력 반복
워드 프로세서 (React + Virtual DOM)
- 메모리에서 편집
- 변경사항 추적
- 저장 버튼 누를 때만 파일에 쓰기
- 빠르고 효율적
Virtual DOM = 메모리 버퍼
Real DOM = 디스크에 저장
게임 렌더링 비유
게임 개발에서:
직접 화면에 그리기 (Real DOM 직접 조작)
- 깜빡임 발생
- 성능 저하
- 부분 업데이트 어려움
더블 버퍼링 (Virtual DOM 방식)
1. 뒷 화면(버퍼)에 그림 그리기
2. 완성되면 앞 화면과 교체
3. 부드러운 렌더링
4. 변경된 픽셀만 업데이트
Virtual DOM이 더블 버퍼링과 같은 역할!
⚙️ 작동 원리
1. DOM이란?
// DOM (Document Object Model)
// HTML을 JavaScript로 조작할 수 있는 인터페이스
<!DOCTYPE html>
<html>
<body>
<div id="root">
<h1>Hello</h1>
<p>World</p>
</div>
</body>
</html>
// DOM 트리
document
└─ html
└─ body
└─ div#root
├─ h1 ("Hello")
└─ p ("World")
// JavaScript로 DOM 조작
document.getElementById('root').innerHTML = '<h1>Changed!</h1>';
2. Real DOM의 문제점
// ❌ 비효율적인 Real DOM 조작
// 문제 1: 매번 전체 리렌더링
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // 1000번 DOM 조작!
}
// 문제 2: 레이아웃 재계산 (Reflow)
element.style.width = '100px'; // Reflow 발생
element.style.height = '100px'; // Reflow 발생
element.style.margin = '10px'; // Reflow 발생
// 문제 3: 복잡한 UI 업데이트
// 한 글자 바뀌어도 전체 컴포넌트 재생성
<div>
<Header />
<Content /> ← 이것만 바뀌었는데
<Footer />
</div>
// 전체를 다시 만들어야 함!
// Real DOM 조작이 느린 이유:
// 1. 브라우저 렌더링 엔진과 JavaScript 엔진 사이 통신
// 2. Reflow (레이아웃 재계산)
// 3. Repaint (화면 다시 그리기)
3. Virtual DOM 동작 과정
// ✅ Virtual DOM의 효율적인 처리
// 1단계: 초기 렌더링
const initialVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 0'] },
{ type: 'button', props: {}, children: ['Click'] }
]
};
// Real DOM에 반영
<div id="app">
<h1>Count: 0</h1>
<button>Click</button>
</div>
// 2단계: 상태 변경 (count = 1)
const newVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 1'] }, // ← 변경됨
{ type: 'button', props: {}, children: ['Click'] }
]
};
// 3단계: Diffing (차이 계산)
const diff = {
path: ['div', 'h1', 'text'],
oldValue: 'Count: 0',
newValue: 'Count: 1'
};
// 4단계: Reconciliation (재조정)
// 최소한의 변경만 Real DOM에 적용
document.querySelector('h1').textContent = 'Count: 1';
// div와 button은 그대로 유지!
4. Diffing 알고리즘
// React의 Diffing 알고리즘 원리
// 규칙 1: 다른 타입의 엘리먼트는 다른 트리 생성
// 이전
<div>
<Counter />
</div>
// 이후
<span>
<Counter />
</span>
// 결과: div를 span으로 완전 교체
// Counter도 언마운트 후 재마운트
// 규칙 2: key를 사용하여 리스트 아이템 추적
// ❌ key 없이
<ul>
<li>A</li>
<li>B</li>
</ul>
// 맨 앞에 C 추가
<ul>
<li>C</li> // 전체를 다시 만듦
<li>A</li>
<li>B</li>
</ul>
// ✅ key 사용
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// 맨 앞에 C 추가
<ul>
<li key="c">C</li> // C만 추가
<li key="a">A</li> // 그대로
<li key="b">B</li> // 그대로
</ul>
// 규칙 3: 재귀적으로 자식 비교
function diff(oldNode, newNode) {
// 1. 타입이 다르면 교체
if (oldNode.type !== newNode.type) {
return { action: 'REPLACE', newNode };
}
// 2. 속성 비교
const propsDiff = diffProps(oldNode.props, newNode.props);
// 3. 자식 비교 (재귀)
const childrenDiff = diffChildren(
oldNode.children,
newNode.children
);
return { propsDiff, childrenDiff };
}
5. Reconciliation (재조정)
// Reconciliation: Virtual DOM을 Real DOM에 반영
// Phase 1: Render Phase (비동기)
// - Virtual DOM 비교
// - 변경사항 계산
// - 중단 가능 (React 18 Fiber)
// Phase 2: Commit Phase (동기)
// - Real DOM 업데이트
// - 생명주기 메서드 실행
// - 중단 불가능
// 예시: 리스트 업데이트
const oldList = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
];
const newList = [
{ id: 1, name: 'A' },
{ id: 3, name: 'C-updated' }, // 수정
{ id: 4, name: 'D' } // 추가
// id: 2 삭제
];
// Reconciliation 결과:
// - id:1 유지
// - id:2 제거
// - id:3 텍스트만 업데이트
// - id:4 추가
// 실제 DOM 조작 최소화:
// remove(id:2)
// updateText(id:3, 'C-updated')
// append(id:4)
💡 실제 예시
기본 Virtual DOM 구현
// 간단한 Virtual DOM 구현 (교육용)
// 1. Virtual DOM 노드 생성
function createElement(type, props = {}, ...children) {
return {
type,
props,
children: children.flat()
};
}
// JSX 없이 사용
const vdom = createElement(
'div',
{ id: 'app', className: 'container' },
createElement('h1', {}, 'Hello Virtual DOM'),
createElement('p', {}, 'This is a paragraph')
);
console.log(vdom);
// {
// type: 'div',
// props: { id: 'app', className: 'container' },
// children: [
// { type: 'h1', props: {}, children: ['Hello Virtual DOM'] },
// { type: 'p', props: {}, children: ['This is a paragraph'] }
// ]
// }
// 2. Virtual DOM을 Real DOM으로 변환
function render(vnode) {
// 텍스트 노드
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(vnode);
}
// 엘리먼트 노드
const element = document.createElement(vnode.type);
// 속성 적용
Object.entries(vnode.props || {}).forEach(([key, value]) => {
if (key === 'className') {
element.setAttribute('class', value);
} else if (key.startsWith('on')) {
// 이벤트 리스너
const event = key.substring(2).toLowerCase();
element.addEventListener(event, value);
} else {
element.setAttribute(key, value);
}
});
// 자식 렌더링 (재귀)
(vnode.children || []).forEach(child => {
element.appendChild(render(child));
});
return element;
}
// 사용
const domElement = render(vdom);
document.body.appendChild(domElement);
Diffing 알고리즘 구현
// 3. Diffing 알고리즘 (간단 버전)
function diff(oldVNode, newVNode) {
// 1. 새 노드가 없으면 제거
if (!newVNode) {
return { type: 'REMOVE' };
}
// 2. 이전 노드가 없으면 추가
if (!oldVNode) {
return { type: 'CREATE', newVNode };
}
// 3. 타입이 다르면 교체
if (
typeof oldVNode !== typeof newVNode ||
(typeof oldVNode === 'string' && oldVNode !== newVNode) ||
oldVNode.type !== newVNode.type
) {
return { type: 'REPLACE', newVNode };
}
// 4. 같은 타입이면 업데이트
if (newVNode.type) {
return {
type: 'UPDATE',
props: diffProps(oldVNode.props, newVNode.props),
children: diffChildren(oldVNode.children, newVNode.children)
};
}
// 5. 변경 없음
return { type: 'NONE' };
}
// 속성 비교
function diffProps(oldProps = {}, newProps = {}) {
const patches = [];
// 변경되거나 추가된 속성
Object.keys(newProps).forEach(key => {
if (oldProps[key] !== newProps[key]) {
patches.push({ type: 'SET_PROP', key, value: newProps[key] });
}
});
// 제거된 속성
Object.keys(oldProps).forEach(key => {
if (!(key in newProps)) {
patches.push({ type: 'REMOVE_PROP', key });
}
});
return patches;
}
// 자식 비교
function diffChildren(oldChildren = [], newChildren = []) {
const patches = [];
const maxLength = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLength; i++) {
patches.push(diff(oldChildren[i], newChildren[i]));
}
return patches;
}
Patch 적용
// 4. Patch를 Real DOM에 적용
function patch(parent, patches, index = 0) {
if (!patches) return;
const element = parent.childNodes[index];
switch (patches.type) {
case 'CREATE':
parent.appendChild(render(patches.newVNode));
break;
case 'REMOVE':
parent.removeChild(element);
break;
case 'REPLACE':
parent.replaceChild(render(patches.newVNode), element);
break;
case 'UPDATE':
// 속성 업데이트
patches.props.forEach(propPatch => {
if (propPatch.type === 'SET_PROP') {
element.setAttribute(propPatch.key, propPatch.value);
} else if (propPatch.type === 'REMOVE_PROP') {
element.removeAttribute(propPatch.key);
}
});
// 자식 업데이트 (재귀)
patches.children.forEach((childPatch, i) => {
patch(element, childPatch, i);
});
break;
}
}
React에서 Virtual DOM 사용
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// React는 자동으로 Virtual DOM 처리
return (
<div className="counter">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}
// 내부 동작:
// 1. count 변경 → setState 호출
// 2. React가 새로운 Virtual DOM 생성
// 3. 이전 Virtual DOM과 비교 (Diffing)
// 4. <h1> 텍스트만 변경됨을 감지
// 5. Real DOM의 텍스트 노드만 업데이트
// 6. div, button은 그대로 유지
// 성능 비교:
// ❌ 바닐라 JS: 전체 div.innerHTML 교체
// ✅ React: <h1>의 텍스트만 업데이트