서론
React 코드를 작성하면서 항상 궁금했음 "React가 뭐가 그렇게 빠르지? 매번 다시 렌더링하는데?" 찾아보니 Virtual DOM이라는 게 있고, Reconciliation이라는 과정이 있더라 "실제 DOM을 직접 건드리지 말고, 가상의 DOM에서 비교한 다음에 필요한 것만 업데이트한다"는 거 같았음 그래서 Virtual DOM이 뭐고, Reconciliation이 뭐고, 어떻게 빠르게 하는지 정리해봄
본론
DOM이 뭐냐?
먼저 DOM(Document Object Model)을 이해해야 함
<html>
<body>
<div id="root">
<h1>안녕하세요</h1>
<p>첫 번째 문단</p>
</div>
</body>
</html>이 HTML을 브라우저가 해석하면 이런 구조의 DOM 트리가 생김:
Document
└── html
└── body
└── div#root
├── h1 (텍스트: "안녕하세요")
└── p (텍스트: "첫 번째 문단")DOM 조작의 비용:
// JavaScript로 DOM 조작
const h1 = document.querySelector('h1');
h1.textContent = '새로운 제목'; // ← 비용 큼!DOM 조작할 때마다:
- 브라우저가 레이아웃 다시 계산 (Layout)
- 화면에 다시 그리기 (Paint)
- GPU에 업로드 (Composite)
이 과정들이 매번 일어나면 성능이 떨어짐
Virtual DOM이란?
Virtual DOM은 실제 DOM의 "복사본" JavaScript 객체로 메모리에 존재하는 가벼운 DOM
// 실제 DOM (무겁고 비쌈)
<div id="root">
<h1>안녕</h1>
</div>
// Virtual DOM (가볍고 빠름 - 메모리의 객체)
{
type: 'div',
props: { id: 'root' },
children: [
{
type: 'h1',
props: {},
children: ['안녕']
}
]
}React의 렌더링 흐름:
┌─────────────────────────────────────────┐
│ 1. 상태 변화 (setState 호출) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. 새로운 Virtual DOM 생성 │
│ (JavaScript 객체 - 빠름!) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. Reconciliation │
│ (이전 VDOM과 새로운 VDOM 비교) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. Diff 계산 │
│ (실제로 바뀐 부분만 찾음) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 5. 실제 DOM만 업데이트 │
│ (필요한 부분만 - 빠름!) │
└─────────────────────────────────────────┘Reconciliation (재조정) 상세히 보기
Reconciliation은 "이전 VDOM과 새로운 VDOM을 비교해서 무엇이 바뀌었는지 찾는 과정"
예시:
// 첫 번째 렌더링
function App() {
return (
<ul>
<li>항목 1</li>
<li>항목 2</li>
</ul>
);
}
// Virtual DOM (이전)
{
type: 'ul',
children: [
{ type: 'li', children: ['항목 1'] },
{ type: 'li', children: ['항목 2'] }
]
}// 상태 변화 후 (새 항목 추가)
function App() {
return (
<ul>
<li>항목 1</li>
<li>항목 2</li>
<li>항목 3</li>
</ul>
);
}
// Virtual DOM (새로운)
{
type: 'ul',
children: [
{ type: 'li', children: ['항목 1'] },
{ type: 'li', children: ['항목 2'] },
{ type: 'li', children: ['항목 3'] } // ← 새로 추가됨
]
}Reconciliation 과정:
- 루트부터 비교 시작 (ul은 같음)
- 첫 번째 li: "항목 1" → "항목 1" (같음)
- 두 번째 li: "항목 2" → "항목 2" (같음)
- 세 번째 li: 없음 → "항목 3" (추가됨)
- 결론: 세 번째 li만 실제 DOM에 추가
만약 Reconciliation이 없었다면:
// ❌ 모든 요소를 다시 그리는 비효율
ul.innerHTML = `
<li>항목 1</li>
<li>항목 2</li>
<li>항목 3</li>
`;
// 메모리, CPU 낭비!React의 Diffing 알고리즘
React는 두 가지 가정으로 효율적인 비교 알고리즘을 만듦
가정 1: 같은 타입의 요소는 같은 DOM을 생성
// ✅ 이 두 컴포넌트는 같은 구조
<div className="greeting">안녕</div>
<div className="greeting">안녕하세요</div>
// React: div 타입은 같으니까 내용만 바꾸면 됨가정 2: 다른 타입의 요소는 다른 트리를 생성
// ❌ 이 두 컴포넌트는 완전히 다름
<div>내용</div>
<span>내용</span>
// React: div와 span은 다르니까 전체 트리를 다시 생성해야 함Diffing 알고리즘의 핵심: Key
// ❌ Key가 없는 경우 (비효율)
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li> // 인덱스를 key로 쓰는 건 위험!
))}
</ul>
);
}
// 상황: todos = ['청소', '공부'] → ['공부', '청소']
// 문제: 순서가 바뀌면 React가 모든 li를 다시 렌더링함
// li[0]: "청소" → "공부" (변경)
// li[1]: "공부" → "청소" (변경)
// → 비효율!// ✅ Key를 제대로 쓰는 경우 (효율)
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li> // id를 key로 씀
))}
</ul>
);
}
// 상황: todos = [
// { id: 1, text: '청소' },
// { id: 2, text: '공부' }
// ] → [
// { id: 2, text: '공부' },
// { id: 1, text: '청소' }
// ]
// React의 생각:
// key=2인 li는 이전에 두 번째였는데 이제 첫 번째 → 순서만 바꿈
// key=1인 li는 이전에 첫 번째였는데 이제 두 번째 → 순서만 바꿈
// → 효율적!Reconciliation 과정을 단계별로 보면
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>
증가
</button>
</div>
);
}1단계: 버튼 클릭
onClick 이벤트 발생
↓
setCount(count + 1) 호출
↓
count 상태 변경 (0 → 1)2단계: Re-render (새 VDOM 생성)
React가 Counter() 함수를 다시 호출
새로운 VDOM 생성:
{
type: 'div',
children: [
{ type: 'p', children: ['카운트: ', 1] }, // ← count가 1로 변함
{ type: 'button', ... }
]
}3단계: Reconciliation (비교)
이전 VDOM 새로운 VDOM
{ {
type: 'div', type: 'div',
children: [ children: [
{ {
type: 'p', type: 'p',
children: children:
['카운트: ', 0] ['카운트: ', 1] ← 다름!
}, },
{ type: 'button' } { type: 'button' } ← 같음
] ]
} }
Diff 결과:
- div: 같음 (변경 없음)
- p: 텍스트 내용이 다름 (변경 필요)
- button: 같음 (변경 없음)4단계: 실제 DOM 업데이트
// React가 수행:
const pElement = document.querySelector('p');
pElement.textContent = '카운트: 1'; // ← 이 부분만 업데이트
// button, div는 건드리지 않음!Virtual DOM의 성능 우위
시나리오: 리스트의 1000개 항목 중 1개 변경
방법 1: DOM 직접 조작 (비효율)
// ❌ 전체 리스트 다시 그리기
const ul = document.querySelector('ul');
ul.innerHTML = newListHTML; // 1000개 모두 재생성!
성능:
- DOM 조작: 1000번
- 레이아웃 계산: 1000번
- Paint: 1000번방법 2: React Virtual DOM (효율)
// ✅ VDOM 비교로 1개만 업데이트
setState({ items: newItems });
성능:
- VDOM 생성: O(n) - 메모리만 사용 (빠름)
- Diff 비교: O(n) - 메모리에서만 (빠름)
- DOM 조작: 1번만! (1개 li만 업데이트)
- 레이아웃 계산: 1번
- Paint: 1번시간 비교:
DOM 직접 조작: ████████ (느림)
React VDOM: ██ (빠름)
↑
매우 큰 차이!Fiber Architecture (React 16+)
React 16에서부터 렌더링 엔진이 바뀜 더 효율적인 "Fiber" 아키텍처 도입
Fiber의 장점:
// 이전: 렌더링하면 완전히 끝날 때까지 대기 (동기)
// → 오래 걸리는 작업이 UI를 블로킹
render(App); // 1000ms 걸리면 UI가 프리징됨
// 이후: 렌더링을 작은 단위로 분할 (비동기)
// → 16ms마다 약간씩 작업해서 60fps 유지
commit work → pause → check events → commit work → ...실제 코드에서의 영향:
function HeavyComponent() {
const [items, setItems] = useState([]);
// 부하 큰 연산
const processedItems = useMemo(() => {
return items.map(item => expensiveCalculation(item));
}, [items]);
return <List items={processedItems} />;
}
// React Fiber 덕분에 이 과정이 UI를 블로킹하지 않음Virtual DOM 비용
Virtual DOM도 비용이 있음!
// ❌ Virtual DOM의 비용
const vdom = {
type: 'div',
props: { className: 'container' },
children: [
{ type: 'p', children: ['Text'] }
]
};
// → 메모리 사용, Garbage Collection 비용
// ✅ Virtual DOM의 이점
// → 실제 DOM 조작보다 훨씬 저렴언제 Virtual DOM이 도움이 되는가?
// ✅ Virtual DOM이 도움되는 경우
// - 자주 변하는 요소들
// - 복잡한 업데이트 로직
// - 많은 양의 데이터 렌더링
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ❌ Virtual DOM이 오버헤드인 경우
// - 거의 변하지 않는 정적 콘텐츠
// - 단순한 HTML
const StaticPage = () => <div>이 내용은 절대 안 바뀜</div>;Virtual DOM 최적화 팁
1. Key를 제대로 사용
// ❌ 나쁜 예
{items.map((item, index) => (
<div key={index}>{item}</div>
))}
// ✅ 좋은 예
{items.map((item) => (
<div key={item.id}>{item}</div>
))}2. 컴포넌트 구조 최적화
// ❌ 전체가 한 번에 리렌더링
function BadApp() {
const [count, setCount] = useState(0);
const [list, setList] = useState([]);
return (
<div>
<Counter count={count} setCount={setCount} />
<List list={list} setList={setList} />
</div>
);
}
// count 변경 → 전체 BadApp 리렌더링
// → List도 불필요하게 리렌더링!
// ✅ 분리된 컴포넌트
function GoodApp() {
return (
<div>
<CounterSection />
<ListSection />
</div>
);
}
// count 변경 → CounterSection만 리렌더링
// → ListSection은 영향 없음3. React.memo 활용
// ❌ props가 같아도 항상 리렌더링
function UserCard({ user }) {
return <div>{user.name}</div>;
}
// ✅ props가 같으면 리렌더링 안 함
const UserCard = React.memo(function UserCard({ user }) {
return <div>{user.name}</div>;
});결론
Virtual DOM과 Reconciliation은 React의 핵심 최적화 기법
실제 DOM을 직접 조작하는 대신 JavaScript 객체인 Virtual DOM으로 비교해서 필요한 부분만 실제 DOM을 업데이트함
이 덕분에 React는:
- 복잡한 상태 변화를 효율적으로 처리
- 많은 데이터를 빠르게 렌더링
- 개발자가 성능을 크게 신경 쓰지 않아도 됨
Reconciliation의 Diffing 알고리즘과 Key 사용이 성능을 크게 좌우함 따라서 Key를 제대로 사용하고, 컴포넌트 구조를 잘 설계하는 게 중요!
Virtual DOM의 원리를 이해하면 React 애플리케이션을 더 효율적으로 최적화할 수 있음!
'프론트엔드 > React' 카테고리의 다른 글
| (React) React Query vs SWR vs 직접 fetch (1) | 2025.12.08 |
|---|---|
| (React) Custom Hooks 만들기 (0) | 2025.12.02 |
| (React) Framer Motion 애니메이션 만들어보기 (0) | 2025.12.01 |
| (React) useMemo vs useCallback 차이 (0) | 2025.11.20 |
| (React) props drilling 문제와 해결법 (0) | 2025.11.18 |