Virtual DOM 알아보기
Virtual DOM 알아보기
React는 왜 Virtual DOM을 사용할까요? 이번 글에서는 React의 Virtual DOM에 대해 학습하고, React가 이를 통해 실제로 해결하고자 했던 문제는 무엇인지 함께 알아보도록 하겠습니다.
DOM, 웹의 기초가 되는 인터페이스
DOM을 이해하기 위해, 우리가 자주 마주치는 웹 개발 상황을 생각해보겠습니다. 사용자가 쇼핑몰에서 상품을 장바구니에 담거나, SNS에서 좋아요를 누르면 어떤 일이 일어날까요? 브라우저는 이러한 변경사항을 어떻게 화면에 반영할까요?
이러한 동작들에는 DOM이 관련되어 있습니다.
DOM은 HTML 문서의 프로그래밍 인터페이스로, 웹 페이지가 로드되면 브라우저는 이 문서를 파싱하여 트리 구조의 DOM을 생성합니다.
예를 들어, 다음과 같은 간단한 장바구니 HTML이 있다고 해보겠습니다.
<div class="cart">
<h1>장바구니</h1>
<p class="item-count">상품 1개</p>
<ul class="item-list">
<li>맥북 프로 16인치</li>
</ul>
</div>
이 HTML은 아래와 같은 트리 구조의 DOM으로 표현됩니다.

현대 웹에서 마주치는 DOM의 한계
이제 사용자가 장바구니에 새로운 상품을 추가했다고 가정해봅시다. 우리는 다음과 같은 JavaScript 코드로 DOM을 업데이트해야 합니다.
const itemList = document.querySelector('.item-list');
const newItem = document.createElement('li');
newItem.textContent = '에어팟 프로 2세대';
itemList.appendChild(newItem);
const itemCount = document.querySelector('.item-count');
itemCount.textContent = '상품 2개';
이러한 DOM 직접 조작 방식에는 몇 가지 문제가 있습니다.
먼저 상품 추가, 수량 변경, 삭제 등 다양한 상태 변화를 모두 DOM 조작으로 관리하다 보니 코드가 복잡해질 수 있습니다. 또, 각각의 DOM 조작은 브라우저가 레이아웃을 다시 계산하고, 화면을 다시 그리도록 만들게 되어 성능적으로 좋지 못합니다. 추가로 장바구니의 상품 목록, 총액, 할인가 등 여러 요소가 서로 연관되어 있다면 이들의 상태를 일관성 있게 유지하기가 어렵다는 문제도 있습니다.
Virtual DOM, DOM 조작의 새로운 패러다임
이러한 문제들을 해결하기 위해 React는 Virtual DOM이라는 접근 방식을 도입했습니다. Virtual DOM은 실제 DOM의 가벼운 복사본으로, 자바스크립트 객체 형태로 메모리에 존재합니다.
앞서 본 장바구니 예시를 Virtual DOM으로 표현하면 이렇게 됩니다.
// Virtual DOM의 자바스크립트 객체 표현
const virtualDOM = {
type: 'div',
props: { className: 'cart' },
children: [
{
type: 'h1',
props: {},
children: ['장바구니'],
},
{
type: 'p',
props: { className: 'item-count' },
children: ['상품 1개'],
},
{
type: 'ul',
props: { className: 'item-list' },
children: [
{
type: 'li',
props: {},
children: ['맥북 프로 16인치'],
},
],
},
],
};
이제 새로운 상품을 추가할 때는 어떻게 될까요?
React는 다음과 같은 과정을 거칩니다.
- 상태 변경 감지: 새로운 상품이 추가되면 React는 이를 감지합니다.
- 새로운 Virtual DOM 생성: 변경된 상태를 반영한 새로운 Virtual DOM 트리를 만듭니다.
- Diffing: 이전 Virtual DOM과 새로운 Virtual DOM을 비교합니다.
- 실제 DOM 업데이트: 차이가 있는 부분만 실제 DOM에 적용합니다.

이러한 방식의 장점은 개발자가 DOM을 직접 조작하지 않고, 상태 변경만 선언적으로 처리하면 된다는 것입니다.
이에 따라 장바구니에 상품을 추가하는 코드는 아래와 같이 단순화됩니다.
function ShoppingCart({ items }) {
return (
<div className="cart">
<h1>장바구니</h1>
<p className="item-count">상품 {items.length}개</p>
<ul className="item-list">
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

Virtual DOM으로 얻는 이점
이러한 Virtual DOM의 방식은 어떠한 장점이 있을까요?
Virtual DOM을 사용하게 되면 DOM을 직접 조작하는 대신, UI가 어떻게 보여야 하는지만 선언하면 됩니다. 또, 복잡한 UI 상태 변화를 React가 자동으로 처리해준다는 장점도 있습니다.
React의 렌더링 프로세스 이해하기
이제 React가 실제로 어떻게 Virtual DOM을 처리하는지 자세히 살펴보겠습니다.
React의 렌더링 프로세스는 크게 Render Phase와 Commit Phase로 나뉩니다.
Render Phase - 변경 사항 계산하기
Render Phase에서는 다음과 같은 작업들이 이루어집니다.
- 컴포넌트 렌더링: 컴포넌트의 render 함수를 호출하여 새로운 Virtual DOM을 생성합니다.
- Diffing: 이전 Virtual DOM과 새로운 Virtual DOM을 비교합니다.
위에서 예로들은 장바구니 예시에서 장바구니에 새로운 상품을 추가할 때 React는 이 두 상태를 비교하여 실제로 변경된 부분(새로 추가된 에어팟 프로 항목)만을 찾아냅니다.
function CartItem({ name, price }) {
return (
<li className="cart-item">
<span>{name}</span>
<span>{price}원</span>
</li>
);
}
// 이전 상태
const prevItems = [{ id: 1, name: '맥북 프로 16인치', price: 3600000 }];
// 새로운 상태
const newItems = [
{ id: 1, name: '맥북 프로 16인치', price: 3600000 },
{ id: 2, name: '에어팟 프로 2세대', price: 359000 },
];
Commit Phase - 변경 사항 적용하기
Commit Phase는 React가 계산된 변경 사항을 실제 DOM에 반영하는 단계입니다. 이 단계에서는 모든 변경이 일괄적으로 적용되며, 중간에 중단될 수 없습니다. 즉 위에서 장바구니에 상품을 추가할 때, 새로운 상품이 화면에 나타나는 것과 함께 상품 개수도 동시에 업데이트되어야 하므로, 하나의 작업처럼 처리되어야 합니다.
또한 Commit Phase 단계에서는 실제 DOM 조작과 함께 여러 중요한 작업들이 수행됩니다.
componentDidMount나 componentDidUpdate 같은 생명주기 메서드가 호출되어 컴포넌트가 화면에 완전히 반영된 후의 작업을 처리할 수 있습니다.
또, useEffect 훅의 실행이나 DOM 노드에 대한 ref 업데이트도 이 시점에 이루어집니다.
이 과정에서 Commit Phase는 동기적으로 실행됩니다. 즉, 모든 변경 사항이 화면에 반영되기 전까지 다른 작업이 중간에 끼어들 수 없습니다. 이러한 특성 덕분에 React는 일관된 UI 상태를 보장할 수 있습니다.
효율적인 상태 업데이트 - Batch Update
React는 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 처리하는 'Batch Update' 기능을 제공합니다. 이는 여러 상태가 동시에 변경되는 경우에 유용합니다.
function handleAddToCart(product) {
// 이 세 가지 상태 업데이트는 하나의 리렌더링으로 처리됩니다
setCartItems((items) => [...items, product]); // 상품 추가
setTotalPrice((price) => price + product.price); // 총액 업데이트
setItemCount((count) => count + 1); // 상품 개수 업데이트
}
이러한 배치 처리를 통해 여러 번의 리렌더링이 한 번으로 줄어들게 되고 모든 상태 업데이트가 동시에 반영되게 된다는 이점이 있습니다.