본문 바로가기

개발 공부/React

React Virtual DOM

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에 적용합니다.

 

이 과정은 다음 단계로 이루어집니다:

  1. 상태(state)나 props가 변경됨
  2. 새로운 Virtual DOM을 생성
  3. 이전 Virtual DOM과 비교(diffing)
  4. 변경 사항을 추출 (patch)
  5. 실제 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는 다음과 같은 원칙을 따릅니다:

  1. 타입이 다른 노드는 완전히 교체
    • 예: <div> -> <span> 으로 바뀌면 전체 컴포넌트를 unmount 하고 다시 mount합니다.
  2. 같은 타입의 노드는 props만 비교하여 업데이트
    • 예: <button disabled={true}> -> <button disabled={false}>disabled 속성만 바뀝니다.
  3. 자식 노드 비교 시 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의 작동 방식과 최적화 전략을 더욱 깊이 이해할 수 있습니다