ReactMedium#14
React의 Virtual DOM이란 무엇이며 성능에 어떤 영향을 미치나요?
#React#VirtualDOM#성능#렌더링
힌트
실제 DOM 조작 비용과 Diffing 알고리즘을 생각해보세요.
정답 및 해설
React의 Virtual DOM이란 무엇이며 성능에 어떤 영향을 미치나요?
Virtual DOM(가상 DOM)은 실제 브라우저 DOM의 가벼운 JavaScript 객체 복사본입니다. React는 상태가 변경될 때 실제 DOM을 바로 수정하는 대신, 메모리 상의 Virtual DOM을 먼저 업데이트한 뒤 이전 Virtual DOM과 비교(Diffing)하여 실제로 변경된 부분만 실제 DOM에 반영(Reconciliation)합니다. 이 과정을 통해 비용이 큰 DOM 조작을 최소화하여 렌더링 성능을 향상시킵니다.
실제 DOM의 문제점
브라우저 DOM은 강력하지만 조작 비용이 높습니다. DOM 요소 하나를 변경해도 브라우저는 다음 과정을 거칩니다.
DOM 변경 → Style 계산 → Layout(Reflow) → Paint → Composite
- Reflow(리플로우): 요소의 크기/위치가 바뀌면 영향받는 모든 요소를 재계산
- Repaint(리페인트): 색상, 배경 등 시각적 속성 변경 시 다시 그리기
- 직접 DOM 조작의 비용: 1,000개 목록 항목을 개별 업데이트 시 1,000번의 리플로우 발생
// 비효율적인 직접 DOM 조작
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li');
item.textContent = `항목 ${i}`;
list.appendChild(item); // 매번 reflow 발생
}
// 개선: DocumentFragment 사용
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li');
item.textContent = `항목 ${i}`;
fragment.appendChild(item);
}
list.appendChild(fragment); // 1번의 reflow
Virtual DOM의 동작 원리
1단계: Virtual DOM 트리 생성
React 컴포넌트의 render 또는 함수 반환값은 실제 DOM 대신 JavaScript 객체로 표현됩니다.
// JSX 코드
const element = (
<div className="container">
<h1>안녕하세요</h1>
<p>반갑습니다</p>
</div>
);
// Babel이 변환한 React.createElement 호출
const element = React.createElement(
'div',
{ className: 'container' },
React.createElement('h1', null, '안녕하세요'),
React.createElement('p', null, '반갑습니다')
);
// 실제 Virtual DOM 객체 구조
{
type: 'div',
props: {
className: 'container',
children: [
{ type: 'h1', props: { children: '안녕하세요' } },
{ type: 'p', props: { children: '반갑습니다' } }
]
}
}
2단계: Diffing 알고리즘
상태가 변경되면 React는 새 Virtual DOM 트리를 생성하고 이전 트리와 비교합니다.
이전 Virtual DOM 새 Virtual DOM
div div
/ \ / \
h1 p h1 p
"안녕" "반갑" "안녕" "변경됨" ← 변경 감지
React의 Diffing 알고리즘은 두 가지 가정을 기반으로 O(n) 복잡도를 달성합니다.
- 다른 타입의 요소는 다른 트리를 생성한다: 타입이 바뀌면 이전 트리 전체를 버리고 새로 구성
- key prop으로 자식 요소를 안정적으로 식별한다: 목록에서 변경된 항목만 업데이트
// 타입이 같을 때: 속성만 업데이트
// 이전: <div className="old" />
// 새로: <div className="new" />
// → className 속성만 업데이트
// 타입이 다를 때: 전체 교체
// 이전: <div>...</div>
// 새로: <span>...</span>
// → 이전 트리 언마운트 후 새 트리 마운트
3단계: Reconciliation (재조정)
Diffing 결과로 얻은 최소한의 변경사항만 실제 DOM에 일괄 적용합니다.
Virtual DOM 비교 결과:
- p 요소의 textContent 변경 필요
실제 DOM 조작 (1회):
document.querySelector('p').textContent = '변경됨';
성능 최적화 효과
일괄 업데이트 (Batching)
React는 여러 상태 변경을 하나의 렌더링으로 묶어 처리합니다.
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const handleClick = () => {
// React 18: 이벤트 핸들러 내의 모든 setState를 1번의 리렌더링으로 처리
setCount(prev => prev + 1);
setName('업데이트됨');
// → 렌더링 1회 발생 (2회 X)
};
}
// React 18의 자동 배칭 (Automatic Batching)
// 이전: setTimeout, Promise 내부는 각각 리렌더링 발생
// React 18: 어디서나 자동 배칭
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18: 1번의 리렌더링 (이전에는 2번)
}, 1000);
최소 DOM 업데이트
function ProductList({ products }) {
return (
<ul>
{products.map(product => (
<li key={product.id}>
<span>{product.name}</span>
<span>{product.price}원</span>
</li>
))}
</ul>
);
}
// 상태 변경: products[2].price만 변경됨
// Virtual DOM 비교 결과: 3번째 li의 price span만 변경
// 실제 DOM 조작: 1개 요소의 textContent만 업데이트
React Fiber 아키텍처 (React 16+)
React 18의 Fiber는 기존 재귀 방식의 한계를 극복한 새로운 렌더링 엔진입니다.
기존 Stack Reconciler의 문제
기존 방식 (동기적, 중단 불가):
렌더링 시작 → ... 긴 작업 ... → 렌더링 완료
↑ 이 동안 UI 블로킹, 애니메이션 끊김
Fiber의 해결책: 작업 분할과 우선순위
Fiber 방식 (비동기적, 중단 가능):
렌더링 시작 → 청크 처리 → 중단 → 사용자 입력 처리 → 재개 → 렌더링 완료
↑ 고우선순위 작업이 끼어들 수 있음
// React 18의 Concurrent Features 활용
import { useTransition, startTransition } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value); // 고우선순위: 즉시 업데이트
startTransition(() => {
// 저우선순위: 무거운 작업, 다른 업데이트에 양보
setResults(expensiveFilter(value));
});
};
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending ? <span>검색 중...</span> : <ResultList results={results} />}
</div>
);
}
Fiber의 주요 특징
// Fiber 노드 구조 (단순화)
const fiberNode = {
type: 'div', // 요소 타입
key: null, // 식별 키
stateNode: domNode, // 실제 DOM 노드 참조
return: parentFiber, // 부모 Fiber
child: childFiber, // 첫 번째 자식 Fiber
sibling: siblingFiber, // 형제 Fiber
pendingProps: {}, // 처리할 새 props
memoizedProps: {}, // 이전 props
memoizedState: {}, // 현재 상태
effectTag: 'UPDATE', // 수행할 작업 (INSERT, UPDATE, DELETE)
lanes: 1, // 작업 우선순위
};
Virtual DOM vs 직접 DOM 조작 성능 비교
// 시나리오: 10,000개 목록 중 500개 업데이트
// 직접 DOM 조작 (최적화 없음)
function updateListDirect(items) {
const container = document.getElementById('list');
container.innerHTML = ''; // 전체 삭제 후 재생성
items.forEach(item => {
const el = document.createElement('li');
el.textContent = item.text;
container.appendChild(el);
});
// 10,000번의 createElement + appendChild → 무거운 reflow
}
// React Virtual DOM
function ItemList({ items }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
// 변경된 500개 항목의 textContent만 업데이트 → 최소 DOM 조작
Virtual DOM의 한계
// 1. 소규모 앱에서는 오버헤드 발생
// Virtual DOM 비교 자체도 연산 비용이 있음
// 단순한 카운터 같은 경우 직접 DOM 조작이 더 빠를 수 있음
// 2. 메모리 사용량 증가
// Virtual DOM 트리를 메모리에 유지해야 함
// 3. 초기 렌더링 속도
// 서버 사이드 렌더링(SSR) 없이는 초기 로딩이 느릴 수 있음
// Svelte의 접근법: 컴파일 타임에 최적화된 직접 DOM 업데이트 코드 생성
// → Virtual DOM 없이도 유사한 개발자 경험 제공
성능 측정 및 최적화 도구
// React DevTools Profiler로 성능 측정
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log(`${id} [${phase}]: ${actualDuration.toFixed(2)}ms`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
}
// React.memo로 불필요한 리렌더링 방지
const ExpensiveChild = React.memo(({ data }) => {
return <div>{data}</div>;
});
// props가 변경되지 않으면 리렌더링 스킵
Virtual DOM 핵심 개념 요약
| 개념 | 설명 | 이점 |
|---|---|---|
| Virtual DOM | 실제 DOM의 JS 객체 복사본 | 메모리에서 빠른 비교 가능 |
| Diffing | 이전/새 Virtual DOM 트리 비교 | 변경 사항만 식별 |
| Reconciliation | 변경된 부분만 실제 DOM에 반영 | DOM 조작 최소화 |
| Batching | 여러 상태 변경을 1회 렌더링으로 처리 | 불필요한 리렌더링 방지 |
| Fiber | 작업 분할과 우선순위 기반 렌더링 | UI 응답성 유지, Concurrent Mode |
| Automatic Batching | React 18에서 모든 컨텍스트에서 자동 배칭 | 추가 최적화 없이 성능 향상 |
핵심 정리
- Virtual DOM은 실제 DOM 조작을 최소화하기 위한 추상화 레이어로, DOM의 느린 조작 비용을 JavaScript 연산으로 대체합니다.
- Diffing 알고리즘은 O(n³) 문제를 두 가지 휴리스틱 가정으로 O(n)으로 해결합니다.
- React Fiber(React 16+)는 렌더링 작업을 청크로 분할하고 우선순위를 부여해 UI 응답성을 개선했습니다.
- Virtual DOM이 항상 직접 DOM보다 빠른 것은 아니며, 복잡한 UI에서 개발 생산성과 성능의 균형을 잡아주는 도구입니다.