[React] 가상 DOM

seungjun.dev·2025년 10월 14일
0

React

목록 보기
8/11

실제 DOM의 추상화된 인메모리 복사본

실제 DOM을 직접 조작하는 대신, 메모리에 가상 DOM을 두고 변경 사항을 먼저 적용하여 성능을 최적화하는 프로그래밍 개념이다.

왜 사용할까?

문제

전통적인 웹 앱에서는 데이터가 변경될 때마다 실제 DOM을 직접 조작한다.

DOM 조작은 브라우저의 렌더링 엔진을 다시 동작시키기 때문에, 특히 복잡하고 동적인 앱에서는 많은 비용이 드는 작업이다.

번경이 잦을수록 불필요한 리렌더링이 발생하여 성능 저하의 주된 원인이 된다.

즉, 리플로우 및 리페인트에 대한 비용이 많이 발생하게 된다.

리플로우(Reflow): DOM의 구조나 레이아웃이 변경되면 브라우저는 새로운 레이아웃을 계산하고 화면을 다시 그린다. 이를 리플로우라고 한다.

리페인트(Repaint): 요소의 색상이나 테두리 등 외형이 변경되면, 브라우저는 해당 요소를 다시 그린다. 이를 리페인트라고 한다.

그렇다고 DOM은 꼭 느릴까?

정답은 아니다.

리액트는 대규모 SPA와 동적 UI의 웹 페이지를 만들기 위해서 존재하며, 만약 규모가 작고 정적인 이전의 웹 앱이라면 일반 DOM이 성능이 더 좋다고 한다.

즉, 상황에 따라 어느 쪽이 좋은지 다를 수 있다는 것이다.

그러나, 현재의 DOM은 트렌드에는 맞지 않는다.

초기에 정적 웹 앱에 맞게 설계된 DOM은 정적인 성격을 가지고 있으며 현재 트렌드인 동적 웹 앱에 사용하려면 성능 상 문제가 발생한다.

최근 트렌드의 복잡한 SPA에서는 DOM 조작이 굉장히 빈번하게 발생하며, 그 변화를 적용하기 위해서는 브라우저가 많은 연산을 하게 되고, 결국 전체적인 프로세스가 비효율적이게 된다.

해결

리액트는 이런 문제를 해결하기 위해 가상 DOM을 도입했다.

상태가 변경되면 실제 DOM에 바로 적용하는 것이 아니라, 가상 DOM에 먼저 변경 사항을 적용하고 이전 가상 DOM과 비교하여 바뀐 부분만 찾아내어 실제 DOM에 딱 한 번만 적용한다.

이 과정을 통해 DOM 조작 횟수를 최소화하여 성능을 향상시키는 것이다.

React 업데이트 과정

동작 방식을 알기 전에 전체적인 React의 동작 방식에 대해 간단하게 정리한다.

1. 트리거 (Trigger)

컴포넌트의 state가 변경되거나 새로운 props가 전달되면 리렌더링이 시작된다.

2. 렌더 (Render)

React는 변경된 컴포넌트를 다시 호출하여 메모리에 새로운 가상 DOM 트리를 생성한다.

3. 재조정 (Reconciliation)

이전 가상 DOM과 새로 생성된 가상 DOM을 비교한다 (Diffing).

이 비교 과정을 통해 변경된 부분만 정확히 찾아낸다.

4. 커밋 (Commit)

재조정 단계에서 찾아낸 모든 변경 사항을 실제 DOM에 한 번에 적용한다.

이 커밋 단계가 완료되면 브라우저가 변경된 실제 DOM을 기반으로 화면을 다시 그린다(리페인트/리플로우).

동작 방식

1. 생성

React의 업데이트 과정에서 렌더에 해당된다.

컴포넌트의 상태나 속성이 변경되면, 새로운 가상 DOM 트리를 메모리에 생성한다.

이 가상 DOM은 실제 DOM 구조를 본뜬 간단한 JS 객체들의 집합이다.

예를 들어 다음과 같은 JSX 코드가 있다면

const element = (
  <div className="container">
    <h1>Hello, React!</h1>
  </div>
);

리액트는 이를 아래와 같은 JS 객체 형태의 가상 DOM 트리 구조로 변환하여 메모리에 보관한다.

결정적으로 실제 DOM의 노드 하나하나는 수많은 속성과 API를 가진 무거운 객체인 반면, 가상 DOM의 노드는 아주 단순한 속성만 가진 가벼운 JS 객체라는 점이다.

{
  type: 'div',
  props: {
    className: 'container',
    children: {
      type: 'h1',
      props: {
        children: 'Hello, React!'
      }
    }
  }
}

브라우저의 실제 DOM을 직접 만들고 수정하는 것은 매우 느리고 비용이 큰 작업이다.

하지만 메모리 상에서 이런 간단한 JS 객체를 만들고 비교하는 것은 비교할 수 없을 정도로 빠르다.

리액트는 비용이 비싼 실제 DOM 조작 횟수를 최소화하고, 비용이 싼 JS 연산을 최대한 활용하는 전략을 택한 것이다.

2. 비교 (Diffing)

새로운 가상 DOM 트리와 이전 가상 DOM 트리를 비교하여 변경된 부분을 찾아낸다.

보통 두 개의 일반적인 트리 구조의 차이점을 완벽하게 찾아내는 알고리즘(Tree Edit Distance)은 매우 복잡하고 느리다. 알고리즘 복잡도로는 약 O(N^3) 수준이라고 하며, UI가 조금만 복잡해져도 UI 업데이트에 사용하기에는 너무 느리다.

그래서 리액트는 몇 가지 규칙을 바탕으로 복잡도를 O(N) 수준으로 낮춘, 훨씬 바른 비교 알고리즘을 사용한다.

이것이 재조정(Reconiliation) 과정에서 사용되는 Diffing Algorithm이다.

주요 규칙은 다음과 같다.

규칙 1. 아예 다른 종류인가? (엘리먼트 타입 비교)

먼저 두 가상 DOM의 최상위 엘리먼트부터 종류가 같은지 확인한다.

  • 변경 전: <div> 안에 무언가 들어있음
  • 변경 후: <p> 안에 무언가 들어있음

이 경우 <div><p>는 완전히 다른 종류라고 판단한다.

그러면 내용물을 하나하나 비교하지 않고, 그냥 기존의 <div>를 통째로 버리고 새로운 <p>를 만든다.

즉, 불필요한 비교 과정을 생략하고 매우 빠르게 차이점을 찾아내는 것이다.

규칙 2. 종류는 같은데, 속성이 바뀌었나? (속성 비교)

만약 엘리먼트의 종류가 같다면, 그 엘리먼트를 재사용하기로 결정한다.

그리고 그 안의 속성(props, attributes)들을 비교해서 바뀐 부분만 찾아낸다.

  • 변경 전: <div className="blue">
  • 변경 후: <div className="red">

이 경우 div는 그대로고 className만 바뀌었구나라고 인식하고, 실제 DOM에 가서 className만 'blue'에서 'red'로 딱 그 부분만 수정한다.

규칙 3. 여러 개의 리스트 순서가 바뀌었나? (key를 이용한 리스트 비교)

여러 개의 자식 엘리먼트(리스트)를 비교하는 것은 조금 더 복잡하지만, key 덕분에 매우 효율적으로 처리된다.

key는 각 항목을 구별하는 고유한 ID이다.

key가 없을 때의 문제점을 생각해보자.

만약 리스트의 맨 앞에 새로운 항목이 추가되면, 리액트는 순서대로 비교하다가 모든 항목이 다 바뀌었다고 착각할 수 있다.

  • 변경 전: <li>사과</li>, <li>바나나</li>
  • 변경 후: <li>포도</li>, <li>사과</li>, <li>바나나</li>

key가 없다면 리액트는 이렇게 생각한다.

  1. 첫 번째 항목이 '사과'에서 '포도로 바뀌었다 (수정)
  2. 두 번째 항목이 '바나나'에서 '사과로 바뀌었다 (수정)
  3. 세 번째 항목 '바나나'가 새로 생겼다 (추가)

결과적으로 불필요한 수정 작업을 많이 하게 된다.

각 항목에 고유한 key를 주면 리액트는 훨씬 똑똑하게 작동한다.

  • 변경 전: <li key="A">사과</li>, <li key="B">바나나</li>
  • 변경 후: <li key="C">포도</li>, <li key="A">사과</li>, <li key="B">바나나</li>

key가 있다면 리액트는 이렇게 생각한다.

  1. key="A"(사과)랑 key="B"(바나나)는 원래 있던 애들이고 위치만 바뀌었다
  2. key="C"(포도)는 새로 생긴 애니까 맨 앞에 추가하자

결과적으로 '포도' 항목 하나만 새로 만들어서 추가하고, 기존 항목들은 재사용하여 위치만 옮긴다.

3. 재조정 (Reconciliation)

리액트가 화면을 업데이트해야 할 때, 가상 DOM과 실제 DOM의 상태를 일치시키는 모든 과정을 재조정이라고 한다.

비교 과정을 통해 발견된 변경 사항들을 모아서 묶고 실제 DOM에 한 번에 적용한다.

여기서 묶기란 React의 업데이트 과정 중 커밋 단계에서 변경 사항을 일괄적으로(Batching) 처리하는 개념이다.

즉, 비교 과정은 재조정 과정의 핵심적인 일부이다. 재조정을 하기 위해, 즉 무엇을 바꿔야 할지 알기 위해 이전 가상 DOM과 새 가상 DOM을 비교하는 알고리즘이 바로 Diffing이다.

변경된 부분만 최소한으로 실제 DOM에 업데이트함으로써 불필요한 DOM 조작을 줄이고, 브라우저의 렌더링 과정을 최소화한다.

즉, 수많은 변경 사항이 발생하더라도 이를 모아서 가장 효율적인 방식으로 단 한 번의 DOM 업데이트를 수행하는 것과 같은 효과를 낸다.

이러한 작동 방식 덕분에 개발자는 DOM을 직접 제어하지 않아도 되며, 데이터를 변경하는 것만으로 UI가 자동적으로 업데이트되는 개발 방식을 누릴 수 있다.

profile
Web FE Dev | Microsoft Student Ambassadors Alumni

0개의 댓글