React 의 "JSX 를 쓰면 DOM 이 알아서 갱신" 의 진짜 — virtual DOM + reconciliation. 그러나 modern (Svelte / Solid) 는 VDOM 없이도 같은 마법. 이 가이드는 VDOM 의 동작, diff 알고리즘이 O(n³) 가 아닌 O(n) 인 휴리스틱, key prop 의 중요성, no-VDOM 의 다른 길을 정리한다.
VDOM — 무엇인가
기본 아이디어: 직접 DOM 조작 X, JS object 로 "원하는 모양" 표현.
JSX:
<div className="card">
<h1>{title}</h1>
<p>{body}</p>
</div>
→ 컴파일 후:
React.createElement("div", {className: "card"}, [
React.createElement("h1", null, title),
React.createElement("p", null, body)
])
→ runtime 의 JS object (VDOM tree):
{type: "div", props: {className: "card"}, children: [
{type: "h1", props: {}, children: [title]},
{type: "p", props: {}, children: [body]}
]}
state 변경 → 새 VDOM tree 생성 (이전과 다른 object)
→ React 가 새 vs 이전 비교 (reconciliation) → 실제 DOM 의 차이만 업데이트.왜 VDOM 인가 — 비교
직접 DOM 조작 (jQuery 시대):
$("#list").empty();
for (let item of items) {
$("#list").append("<li>" + item.name + "</li>");
}
문제:
- 100 item 의 list → 100 DOM mutation
- 단 1 item 변경에도 모두 재생성 (또는 수동으로 diff)
- jQuery 가 그래도 가볍지만 — application 복잡도 ↑
VDOM 의 답:
function List({items}) {
return <ul>{items.map(i => <li>{i.name}</li>)}</ul>;
}
- React 가 자동 diff
- 100 item 중 1 개 변경 → 1 DOM mutation
- application 코드는 "전체 render" — declarativeReconciliation Diff — O(n) 휴리스틱
tree diff 의 일반 알고리즘 = O(n³) — 두 큰 tree 의 정확한 minimum
edit distance. 1000-node tree = 10^9 연산 → 매우 느림.
React 의 trick — 두 휴리스틱으로 O(n) 으로 단순화:
1. 다른 type 의 element 는 다른 subtree
<div>...</div> → <span>...</span>
→ div 전체 제거 + span 새로 생성 (안에 변경 비교 X)
2. key prop 으로 list 의 reorder 알림
key 없으면: <li>A</li><li>B</li><li>C</li> → <li>B</li><li>C</li>
→ React 가 A→B, B→C, C→remove 로 판단 (3 mutation)
key 있으면: <li key="a">A</li><li key="b">B</li><li key="c">C</li>
→ A 제거, B C 그대로 (1 mutation)
→ "정확한" 알고리즘 포기 + 일반적 사용 패턴에 맞춤 = 빠른 dev experience.Key Prop 의 중요성
흔한 안티 패턴:
{items.map((item, i) => <Card key={i} data={item} />)}
↑
index 를 key 로
문제: list 의 reorder 시 — key=0 이 무조건 첫 item.
items 가 [A, B, C] → [B, C] 로 변경 (A 삭제) 면:
key=0: 이전 A → 이제 B (re-render 후 B 데이터로)
key=1: 이전 B → 이제 C
key=2: 이전 C → unmount
→ A B C 의 모든 Card 가 props 변경 → re-render
(실제로 unmount 한 건 C 의 위치 뿐)
올바른 패턴:
{items.map(item => <Card key={item.id} data={item} />)}
↑
unique stable id
→ React 가 정확히 인식: A 사라짐, B C 그대로 → B C 의 Card 는 동일 instance 유지.
특히 form input 같은 stateful component 에서 큰 차이.React Fiber — interruptible scheduler (2017+)
React 16 의 새 reconciliation engine.
이전 (Stack Reconciler):
render 가 시작되면 끝까지 동기 실행
→ 큰 tree 의 render 가 main thread 점유 → user input 무시 (jank)
Fiber:
render 가 "interruptible" unit 으로 쪼개짐
→ main thread 가 다른 일 (animation, input) 처리 시 잠시 yield
→ 우선순위 시스템 (high: user input / low: 데이터 update)
→ "concurrent rendering" (React 18) 의 기반
특징:
- 같은 render 가 여러 frame 에 걸침 가능
- urgent update (input) 가 lower-priority update (data) 를 가로채기 가능
- Suspense / startTransition / useDeferredValue 의 기반
→ "큰 page 도 60fps 유지" 의 modern React 약속.Vue — Tracked Dependency Patch
Vue 의 reactivity 는 React 와 다른 접근:
const state = reactive({count: 0});
console.log(state.count); // get → React 가 의존 추적
function Counter() {
return <div>{state.count}</div>; // 의존 등록됨
}
state.count++; // set → 의존하는 component 만 정확히 re-render
// (다른 component 다 그대로)
→ React 가 "tree 전체 diff" 인 반면 Vue 는 "변경된 곳만 정확히 patch".
런타임 cost 다름 — Vue 가 fine-grained 갱신, 추적 cost 추가.
Vue 3 의 VDOM 도 React 보다 작음 + Proxy 기반 reactivity 가 더 효율적.Svelte / Solid — No VDOM (Signal)
다른 path: VDOM 자체 제거.
Svelte:
<script>
let count = 0;
</script>
<button on:click={() => count++}>{count}</button>
→ compile time 에 "count 가 변경되면 그 button 의 textContent 갱신"
같은 정확한 DOM 명령으로 변환.
→ runtime 에 VDOM diff X, 직접 DOM 변경.
→ bundle 매우 작음 (framework runtime 0 에 가까움).
Solid:
function Counter() {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}
→ JSX 사용하지만 internally signal — Vue 와 비슷한 fine-grained reactivity.
→ React API 익숙한 사람이 받기 쉽고, 성능은 Svelte 급.
trade-off:
- React/Vue: framework runtime 큼, mental model 안정적
- Svelte/Solid: framework runtime 작음, 새 패러다임성능 — VDOM 이 항상 더 느린가?
흔한 오해: "VDOM 비싸다 → 직접 DOM 이 빠르다"
실제:
- 작은 update: 직접 DOM ≈ VDOM (둘 다 1 mutation)
- 큰 update: VDOM 이 batched diff 후 한 번에 → 직접보다 빠를 수도
- 복잡한 reorder: 직접은 거의 불가능 (수동 diff 필요)
→ "VDOM 의 cost" 자체는 작음 (수십 μs). 진짜 비용은 component 의
render 함수 안 작업.
VDOM 의 가치:
1. Developer experience (declarative)
2. 자동 batching + scheduling
3. cross-platform (React Native — VDOM 을 native widget 으로 mapping)
4. testing / DevTools (tree snapshot)흔한 함정
- index 를 key 로 — list reorder / 추가 / 삭제 시 잘못된 instance 재사용 → state 누수, perf 손해.
- inline object / function 을 prop 으로 — 매 render 마다 새 reference → memoized child 가 무용. useCallback / useMemo.
- 큰 component 의 render 가 자주 — Fiber 도 한 component 의 render 는 동기. 잘게 쪼개기.
- useMemo / useCallback 남용 — memoization 자체도 cost. 측정 후 적용.
- VDOM 의 cost 만 걱정 — 실제 bottleneck 은 보통 큰 state / 큰 list / 잘못된 component split.
마무리
VDOM 의 본질 — "원하는 모양 (JS object) 와 실제 DOM 의 diff". React 의 휴리스틱 (다른 type = 다른 subtree, key 로 reorder) 이 O(n³) → O(n) 로. Fiber 가 interruptible 로 60fps 유지.
실용 — React/Vue 의 VDOM 은 modern hardware 에서 충분 빠름. bundle / runtime 더 줄이려면 Svelte / Solid 의 no-VDOM signal 접근. 어느 쪽이든 key prop 정확한 사용 + 작은 component 가 성능의 80%.