React를 처음 접하면 많이 듣게 되는 단어 중 하나가 바로 Virtual DOM입니다.
하지만 Virtual DOM이 무엇이고, 왜 중요한지에 대해 개념만 알고 실제 동작 원리나 필요성을 잘 이해하지 못하는 경우가 많습니다.
DOM
DOM(Document Object Model)은 HTML 문서를 객체 트리로 표현한 구조입니다.
즉, 웹 브라우저는 HTML을 파싱해서 DOM이라는 구조화된 트리를 만들고, JavaScript는 이 DOM을 조작함으로써 화면을 바꿉니다.
예를 들어 아래와 같은 HTML이 있다면
<div>
<p>Hello</p>
</div>
브라우저는 이를 다음과 같은 트리 구조로 이해합니다:
DOM
└── div
└── p
└── "Hello"
JavaScript를 통해 p 요소의 텍스트를 바꾸는 것은 DOM을 직접 수정하는 행위입니다:
document.querySelector('p').textContent = 'World';
- 이런 DOM 조작은 작아 보여도 브라우저에게 큰 부담이 될 수 있습니다.
- DOM이 복잡하고 변경이 잦을수록 리렌더링(reflow/repaint) 비용이 커집니다.
Virtual DOM
Virtual DOM은 메모리 상에 존재하는 가상의 DOM 구조입니다.
React는 UI를 변경할 때 직접 DOM을 조작하지 않고, 먼저 Virtual DOM에서 UI 변화를 계산한 후, 실제 DOM과 비교하여 필요한 최소 변경만 실제 DOM에 적용합니다.
이 과정은 다음 단계로 이루어집니다:
- 상태(state)나 props가 변경됨
- 새로운 Virtual DOM을 생성
- 이전 Virtual DOM과 비교(diffing)
- 변경 사항을 추출 (patch)
- 실제 DOM에 반영 (reconciliation)
예제 Virtual DOM
다음은 간단한 React 코드입니다:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
- 이 컴포넌트에서 버튼을 클릭하면 count가 증가하고 화면이 업데이트됩니다.
실제 DOM 방식
- 버튼 클릭 시 전체 div 안의 내용을 새로 그릴 수 있습니다.
- 리렌더링 범위가 넓어지고 성능 저하의 원인이 됩니다.
Virtual DOM 방식
- count 값만 변했기 때문에 h1 태그의 텍스트 노드만 변경됩니다.
- React는 이전 Virtual DOM (<h1>Count: 0</h1>)과 새로운 Virtual DOM (<h1>Count: 1</h1>)을 비교하고, 텍스트만 업데이트합니다.
결과적으로 불필요한 DOM 조작을 방지하고, 성능 최적화를 이룹니다.
key 속성과 Virtual DOM의 최적화
React에서 리스트를 렌더링할 때 key 속성을 지정하라는 경고를 자주 보셨을 겁니다.
이는 Virtual DOM의 diffing 최적화와 관련이 깊습니다.
{items.map(item => <li key={item.id}>{item.text}</li>)}
- 이처럼 고유한 key를 부여하면, React는 각 항목의 변화를 정확히 파악할 수 있어 효율적인 업데이트가 가능합니다.
- key가 없거나 index를 key로 쓴다면, 순서가 바뀌는 경우 잘못된 DOM 변경이 일어날 수 있습니다.
reconciliation 알고리즘
React의 Virtual DOM이 성능을 높일 수 있는 핵심은 바로 reconciliation (재조정) 알고리즘 덕분입니다.
이 알고리즘은 두 Virtual DOM 트리를 비교해서 어떤 변경이 실제로 필요한지를 판단합니다.
기본 비교 원칙
React는 다음과 같은 원칙을 따릅니다:
- 타입이 다른 노드는 완전히 교체
- 예: <div> -> <span> 으로 바뀌면 전체 컴포넌트를 unmount 하고 다시 mount합니다.
- 같은 타입의 노드는 props만 비교하여 업데이트
- 예: <button disabled={true}> -> <button disabled={false}> 는 disabled 속성만 바뀝니다.
- 자식 노드 비교 시 key를 기준으로 최적화
- key가 같으면 재사용
- key가 다르면 새로 생성하고 기존 노드는 제거
예시: key로 인한 diffing 차이
잘못된 key 사용
{["A", "B", "C"].map((item, index) => (
<li key={index}>{item}</li>
))}
- 이제 "B"를 제거합니다
["A", "C"].map((item, index) => (
<li key={index}>{item}</li>
))
- 이 경우 key가 index이기 때문에
- 기존 "C"가 index 2 → 새로운 index 1 로 이동
- React는 "C"를 새로 만들고, 기존 "B" 자리에 "C"를 렌더링함
- 결과적으로 불필요한 DOM 변경 발생
올바른 key 사용
[{id: 1, text: "A"}, {id: 2, text: "B"}, {id: 3, text: "C"}].map(item => (
<li key={item.id}>{item.text}</li>
))
- "B"를 제거해도 id가 고정되어 있으므로
- "C"는 여전히 key=3이므로 재사용됨
- 성능 향상 + 버그 방지
표 정리
| 구분 | 실제 DOM | Virtual DOM |
| 위치 | 브라우저 내부 | 메모리 내부 |
| 성능 | 느림 (전체 리렌더링 가능) | 빠름 (변경된 부분만 적용) |
| 업데이트 방식 | 직접 조작 | 선언형으로 상태만 변경 |
| React 활용 | ❌ | ✅ 필수 개념 |
Virtual DOM은 단순한 추상화가 아니라, 성능을 최적화하기 위한 핵심 기술입니다.
React를 사용하는 이유 중 하나이기도 하죠. 이 원리를 이해하면, React의 작동 방식과 최적화 전략을 더욱 깊이 이해할 수 있습니다
'개발 공부 > React' 카테고리의 다른 글
| getStaticProps, getStaticPaths, getServerSideProps (0) | 2025.04.15 |
|---|---|
| Static Generation / Server-side Rendering (0) | 2025.04.13 |
| React 함수형 컴포넌트 vs 클래스형 컴포넌트 (0) | 2025.04.06 |
| Next.js loading.tsx (0) | 2025.03.23 |
| React Focus Lock (0) | 2025.03.22 |