Virtual DOM은 실제 DOM을 직접 조작하기 전에 메모리상에 가벼운 자바스크립트 객체 트리를 생성하고, Reconciliation 과정에서 이전 트리와 비교하여 변경된 부분만 실제 DOM에 반영하는 React의 렌더링 최적화 기술입니다.
DOM을 수정하는 것 자체보다 비용이 큰 것은 **브라우저의 렌더링 과정(Reflow, Repaint)**입니다.
Virtual DOM은 여러 번 발생할 수 있는 상태 변경을 메모리에서 계산한 후 실제 DOM 업데이트를 최소화하여 브라우저 렌더링 비용을 줄입니다.
개발자는 DOM을 직접 조작하는 방식이 아니라
“현재 상태라면 화면은 이렇게 보여야 합니다”
라고 결과만 선언적으로 작성하면 됩니다.
React가 내부적으로 상태와 UI를 동기화해주기 때문에 코드의 가독성과 유지보수성이 향상됩니다.
Virtual DOM은 실제 DOM 구조를 모방한 가벼운 JavaScript 객체 트리입니다.
실제 DOM 노드는 브라우저 엔진이 관리하지만
Virtual DOM은 JavaScript 메모리 상에 존재하는 객체이기 때문에 생성 및 비교가 매우 빠릅니다.
예를 들어 다음 JSX가 있다고 가정합니다.
<divclassName="container"><h1>Hello</h1></div>
React 내부에서는 다음과 유사한 객체 구조로 표현됩니다.
{type:"div",props: {className:"container",children: [{type:"h1",props: {children:"Hello"}}]}}
즉 Virtual DOM은 화면을 그리는 구조를 표현한 JavaScript 객체 트리입니다.
React에서 상태가 변경되면 다음 과정이 발생합니다.
state 또는 props가 변경되면 React는 새로운 Virtual DOM 트리를 생성합니다.
이전 Virtual DOM과 새로운 Virtual DOM을 비교하여
어떤 부분이 변경되었는지 계산합니다.
차이가 있는 부분만 실제 DOM에 패치(Patch) 합니다.
즉 흐름은 다음과 같습니다.
State 변경↓새 Virtual DOM 생성↓이전 Virtual DOM과 비교↓차이 계산↓실제 DOM 업데이트
이 과정을 통해 불필요한 DOM 업데이트를 최소화합니다.
Reconciliation은 이전 Virtual DOM 트리와 새로운 Virtual DOM 트리를 비교하여 실제 DOM에 어떤 변경을 적용할지 결정하는 React의 알고리즘 과정입니다.
이 과정은 흔히 Diffing 알고리즘이라고도 불립니다.
두 개의 트리를 비교하는 일반적인 알고리즘은 **O(n³)**의 시간복잡도를 가집니다.
예를 들어 노드가 1000개라면
1000³ = 1,000,000,000
약 10억 번의 연산이 필요합니다.
이는 UI 렌더링에서 사용하기에는 매우 비효율적입니다.
React는 다음 두 가지 가정을 사용하여 복잡도를 **O(n)**으로 줄였습니다.
예
<div>
→
<span>
이 경우 React는
을 수행합니다.
즉 하위 노드까지 모두 새로 렌더링합니다.
예
<div className="before">
→
<div className="after">
이 경우 React는
합니다.
React는 리스트의 자식 노드를 비교할 때
key 값을 기준으로 노드를 매칭합니다.
예를 들어
<likey="A">A</li><li key="B">B</li>
에서 새로운 항목이 추가되면 React는
key 기준으로 동일 노드 매칭
을 수행합니다.
이 덕분에 불필요한 DOM 이동이나 삭제를 줄일 수 있습니다.
많은 사람들이 Virtual DOM이 DOM보다 빠르다고 오해하지만 실제 이유는 조금 다릅니다.
핵심은 DOM 조작 횟수를 줄이는 것입니다.
브라우저 렌더링 과정은 다음 단계로 이루어집니다.
DOM 변경↓Layout (Reflow)↓Paint (Repaint)↓Composite
이 과정은 비용이 큽니다.
React는 Virtual DOM을 통해
여러 state 변경↓Virtual DOM에서 계산↓실제 DOM 업데이트 1번
처럼 DOM 업데이트를 묶어서 처리합니다.
이를 Batch Update라고 합니다.
예를 들어 다음 리스트가 있다고 가정합니다.
초기 상태
A B C
업데이트
D A B C
React는 다음처럼 판단합니다.
A → DB → AC → B새로 C 생성
즉 모든 노드가 변경된 것으로 판단합니다.
key="A"key="B"key="C"
React는
D 추가A 유지B 유지C 유지
로 판단합니다.
따라서 최소한의 DOM 변경만 발생합니다.
다음과 같은 코드가 있다고 가정합니다.
{todos.map((todo,index) => (<TodoItemkey={index}/>))}
리스트 앞에 요소가 추가되면
0 1 2
→
0 1 2 3
기존 요소들의 key가 전부 변경됩니다.
A B C
→
D A B C
React는 이를
A → DB → AC → B
로 잘못 판단합니다.
이로 인해
같은 문제가 발생합니다.
따라서 항상 안정적인 고유 ID를 key로 사용해야 합니다.
왜 이렇게 설계됐을까? (Design Decision)
💡 초창기 웹은 바닐라 자바스크립트나 jQuery로 DOM 요소(document.getElementById 등)를 직접 찾아 일일이 수정했습니다.
게다가 데이터를 10번 바꾸면 화면도 10번 다시 그리는 비효율이 발생했어. 그래서 리액트 개발진은 "상태가 바뀌면 그냥 화면을 처음부터 새로 그리자!"라는 미친 아이디어를 냈고, 그걸 성능 하락 없이 구현하기 위해 중간에 '가벼운 가짜 DOM'을 두어 변경된 부분만 모아서 칠하는 방식을 고안한 것.
실무에서는 어떻게 쓰이나?
리스트를 렌더링할 때 key에 index를 쓰면 안 되는 이유가 여기서 나옵니다.
// ❌ 나쁜 예시: 배열의 index를 key로 사용{todos.map((todo, index) => (<TodoItem key={index} data={todo} />))}// ✅ 좋은 예시: 고유한 ID를 key로 사용
만약 리스트 맨 앞에 새로운 할 일이 추가되면, index를 key로 썼을 때는 기존 항목들의 key(0, 1, 2...)가 전부 뒤밀리면서 리액트의 재조정 알고리즘이 "어? 모든 항목이 다 바뀌었네?"라고 착각하고 DOM을 전부 다시 그리거나 내부 state가 꼬이는 치명적인 버그 발생, 데이터의 고유 ID를 써야 리액트가 "아, 0번에 새 노드가 추가됐고 나머지는 그대로구나!" 하고 효율적으로 처리할 수 있습니다.
"Virtual DOM과 Reconciliation에 대해 설명해주세요." 질문에 대한 답변 스크립트
💡 "가상 돔은 실제 DOM의 구조를 흉내 낸 가벼운 자바스크립트 객체입니다. 리액트에서 상태가 변경되면, 직접 실제 DOM을 건드리는 대신 새로운 가상 돔 트리를 메모리에 생성합니다.그 다음, 리액트의 '재조정(Reconciliation)' 과정을 통해 이전 가상 돔 트리와 새로운 트리를 비교(Diffing)합니다. 이 알고리즘은 엘리먼트의 타입이나 key prop을 기준으로 변경 사항을 빠르고 효율적으로 파악합니다. 최종적으로 변경된 부분들만 모아서 한 번에 실제 DOM에 반영(Batch Update)하기 때문에, 불필요한 브라우저 렌더링 연산을 줄이고 좋은 성능을 낼 수 있습니다."
⏱️ 1분 요약 버전
💡 "DOM 업데이트의 비효율성을 해결하기 위한 리액트의 핵심 기술입니다. 상태가 바뀌면 일단 가벼운 JS 객체인 가상 돔을 새로 그리고, 이전 가상 돔과 비교(Reconciliation)해서 진짜 바뀐 딱 그 부분만 실제 DOM에 패치(Patch)해 주며, 그 덕분에 성능 걱정 없이 '데이터가 이러면 화면은 이래야 해'라고 선언적으로 코딩할 수 있습니다.
기본 질문
A: 아니, 항상 그런 건 아니야. 뷰가 매우 단순하거나 인터랙션이 거의 없는 페이지라면 굳이 중간에 가상 돔을 생성하고 비교하는 연산이 추가되기 때문에 바닐라 JS로 직접 DOM을 조작하는 게 더 빠를 수 있습니다. 가상 돔의 진짜 가치는 복잡한 SPA 환경에서 '충분히 빠른 성능'과 '개발의 편의성(선언적 UI)'을 동시에 가져다준다는 데 있습니다.
A: 리액트의 재조정 알고리즘이 자식 노드들을 비교할 때, 어떤 엘리먼트가 추가/수정/삭제되었는지 식별하는 고유 이름표 역할을 하기 때문입니다 이게 없거나 매번 바뀌면 엉뚱한 컴포넌트의 상태가 유지되거나 불필요한 리렌더링이 발생합니다.
심화 질문 (꼬리질문 대비)
A: 기존의 재조정 과정은 동기적으로 한 번 시작되면 끝날 때까지 멈출 수 없어서(Stack 구조), 트리가 깊어지면 메인 스레드를 블로킹해 화면이 버벅대는(프레임 드랍) 문제가 있었습니다. Fiber는 이 작업을 작은 단위로 쪼개서 우선순위를 매기고, 급한 작업(애니메이션 등)이 들어오면 기존 작업을 일시 정지하거나 폐기하고 나중에 다시 재개할 수 있도록 비동기적 렌더링을 가능하게 만든 엔진입니다.
오늘 이 주제에서 가장 중요한 것 3가지
헷갈렸던 부분 & 해결
더 공부하고 싶은 것