본문 바로가기

가상 DOM

1. 가상 DOM이란?

React의 핵심 개념 중 하나는 가상 DOM(Virtual DOM) 입니다. 가상 DOM은 실제 DOM의 가벼운 복사본이며, React가 UI 업데이트를 효율적으로 처리할 수 있게 해주는 개념입니다.

1.1 웹 브라우저 렌더링 과정의 이해

웹 브라우저가 HTML, CSS, JavaScript를 화면에 그리는 과정을 렌더링이라고 합니다. 이 과정은 다음과 같은 단계로 이루어집니다.

  1. 사용자가 URL 입력 하면 브라우저는 서버로부터 받은 HTML 문서를 다운로드합니다.
  2. HTML 문서를 파싱하여 DOM(Document Object Model) 트리 생성합니다.
  3. CSS 문서가 만나면 해당 문서를 파싱해 CSSOM(CSS Object Model) 트리 생성합니다.
  4. DOM 트리와 CSSOM 트리를 결합하여 렌더 트리 구성합니다.
  5. 렌더 트리의 각 노드에 대해 화면에서의 정확한 위치/크기 등 레이아웃을 계산합니다.
  6. 레이아웃 정보를 바탕으로 페인팅(화면에 그리기)을 진행합니다.
브라우저 렌더링 과정

이 중 DOM 조작(변경)이 발생하면 브라우저는 아래 과정을 다시 거칩니다.

이런 과정은 비용이 많이 드는 작업이며, 특히 복잡한 웹 애플리케이션에서는 성능 저하를 가져올 수 있습니다.

1.2 DOM 조작의 문제점

전통적인 웹 개발에서는 DOM을 직접 조작합니다.

// 요소 생성
const div = document.createElement('div');
div.className = 'container';

// 내용 추가
const p = document.createElement('p');
p.textContent = '안녕하세요!';
div.appendChild(p);

// DOM에 추가
document.body.appendChild(div);

// 내용 변경
p.textContent = '반갑습니다!';
p.style.color = 'red';

이러한 방식은 다음과 같은 문제점이 있습니다.

  1. 비효율적인 업데이트: 작은 변경에도 전체 렌더 트리가 재계산될 수 있습니다.
  2. 코드 복잡성: DOM 조작 코드는 복잡하고 이해하기 어려울 수 있습니다.
  3. 성능 저하: 잦은 DOM 조작은 애플리케이션 성능을 저하시킵니다.

1.3 가상 DOM의 등장 배경

이러한 문제를 해결하기 위해 React는 가상 DOM 개념을 도입했습니다. 가상 DOM은 실제 DOM의 추상화된 복사본으로, JavaScript 객체 형태로 메모리에 존재합니다.

// 실제 DOM 요소
const element = document.createElement('div');
element.className = 'container';
element.innerHTML = '<p>안녕하세요!</p>';

// 가상 DOM 표현 (React 내부)
const virtualElement = {
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'p',
        props: {
          children: '안녕하세요!',
        },
      },
    ],
  },
};

가상 DOM은 실제 DOM 요소와 달리 브라우저 렌더링 엔진과 직접 연결되어 있지 않아 변경 비용이 매우 적습니다.

2. 가상 DOM의 작동 원리

React의 가상 DOM은 어떻게 작동하는지 자세히 살펴보겠습니다.

2.1 가상 DOM의 동작 방식

가상 DOM은 다음과 같은 과정으로 작동합니다.

브라우저 렌더링 과정
  1. 초기 렌더링: React는 컴포넌트를 렌더링하여 첫 번째 가상 DOM 트리를 생성합니다.
  2. 실제 DOM에 반영: 이 가상 DOM 트리는 실제 DOM으로 변환되어 화면에 표시됩니다.
  3. 상태 변경 감지: 컴포넌트의 상태나 속성이 변경되면 React는 새로운 가상 DOM 트리를 생성합니다.
  4. 차이점 계산(Diffing): React는 이전 가상 DOM 트리와 새 가상 DOM 트리를 비교하여 차이점을 찾습니다.
  5. 변경사항만 적용(Reconciliation): 찾아낸 차이점만 실제 DOM에 적용합니다.

이 과정을 통해 React는 최소한의 DOM 조작으로 UI를 업데이트할 수 있습니다.

2.2 재조정(Reconciliation) 과정

재조정은 가상 DOM의 변경사항을 실제 DOM에 효율적으로 적용하는 과정입니다.

  1. 요소 유형 비교: 같은 위치에 있는 요소의 유형을 비교합니다.

    • 유형이 같으면: 속성만 업데이트합니다.
    • 유형이 다르면: 이전 트리를 제거하고 새 트리를 생성합니다.
  2. 같은 유형의 DOM 요소 처리: 두 요소의 속성만 비교하여 변경된 속성만 업데이트합니다.

  3. 같은 유형의 컴포넌트 요소 처리: 컴포넌트 인스턴스는 유지되고, props만 업데이트됩니다.

  4. 자식 요소 재귀적 처리: 자식 요소들을 재귀적으로 처리합니다.

    • 리스트의 경우 key를 사용하여 효율적으로 처리합니다.

3. 가상 DOM의 이점

가상 DOM은 React 애플리케이션에 여러 가지 중요한 이점을 제공합니다.

3.1 성능 최적화

가상 DOM의 주요 이점은 성능 최적화입니다.

  1. 최소한의 DOM 조작: 실제로 변경된 부분만 DOM에 적용하여 렌더링 비용을 최소화합니다.

  2. 일괄 업데이트(Batch Updates): 여러 상태 변경을 하나의 렌더링 사이클로 묶어 처리합니다.

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // 이러한 연속적인 상태 업데이트는 React에 의해 일괄 처리됨
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // 실제로는 마지막 setCount만 적용되며 한 번의 렌더링만 발생
  };

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={handleClick}>증가</button>
    </div>
  );
}

3.2 선언적 UI 패러다임

가상 DOM은 React의 선언적 프로그래밍 방식을 가능하게 합니다.

// 명령적 방식 (jQuery)
$('#counter').text('0');
$('#increment').on('click', function () {
  const count = parseInt($('#counter').text());
  $('#counter').text(count + 1);
});

// 선언적 방식 (React)
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p id="counter">{count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

선언적 UI는 다음과 같은 이점이 있습니다.

  1. 코드의 가독성 향상: "무엇을" 보여줄지에 집중하여 코드 이해가 쉬워집니다.
  2. 유지보수성 향상: 상태와 UI의 관계가 명확하여 버그 발생 가능성이 줄어듭니다.
  3. 테스트 용이성: 입력에 따른 출력을 예측하기 쉬워 테스트가 용이합니다.

3.3 컴포넌트 기반 아키텍처 지원

가상 DOM은 React의 컴포넌트 기반 아키텍처를 효과적으로 지원합니다.

  1. 컴포넌트 재사용: 동일한 컴포넌트가 여러 번 사용되어도 효율적으로 렌더링됩니다.
  2. 독립적인 업데이트: 각 컴포넌트는 독립적으로 업데이트될 수 있습니다.
  3. 컴포지션: 작은 컴포넌트를 조합하여 복잡한 UI를 구성할 수 있습니다.
function App() {
  return (
    <div>
      <Header />
      <main>
        <Sidebar />
        <Content>
          <ProductList />
          <Pagination />
        </Content>
      </main>
      <Footer />
    </div>
  );
}

4. 가상 DOM 사용 시 고려사항

가상 DOM을 효과적으로 사용하기 위해 알아야 할 몇 가지 고려사항이 있습니다.

4.1 key 속성의 중요성

리스트를 렌더링할 때 key 속성은 매우 중요합니다.

// 나쁜 예 - 인덱스를 key로 사용
{
  items.map((item, index) => <ListItem key={index} item={item} />);
}

// 좋은 예 - 고유 ID를 key로 사용
{
  items.map((item) => <ListItem key={item.id} item={item} />);
}

[key의 역할]

  1. 요소 식별: React가 각 요소를 식별하는 데 사용됩니다.
  2. 효율적인 재조정: 요소가 추가, 제거, 재정렬될 때 효율적인 업데이트를 가능하게 합니다.
  3. 상태 보존: 같은 key를 가진 요소의 상태를 보존합니다.

4.2 불필요한 렌더링 방지

가상 DOM은 효율적이지만, 불필요한 렌더링은 여전히 피해야 합니다.

  1. 컴포넌트 분리: 상태가 자주 변경되는 부분을 별도의 컴포넌트로 분리합니다.
  2. 메모이제이션: React.memo, useMemo, useCallback을 사용하여 불필요한 재계산과 렌더링을 방지합니다.
// 메모이제이션을 사용한 컴포넌트
const MemoizedComponent = React.memo(function MyComponent(props) {
  // props가 변경되지 않으면 재렌더링되지 않음
  return <div>{props.value}</div>;
});

// useMemo를 사용한 계산 결과 캐싱
const memoizedValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

// useCallback을 사용한 함수 메모이제이션
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

4.3 상태 관리 최적화

효율적인 상태 관리는 가상 DOM의 성능을 극대화합니다.

  1. 상태 분할: 관련 없는 상태는 분리하여 불필요한 렌더링을 방지합니다.
  2. 컨텍스트 최적화: 컨텍스트를 적절히 분할하여 변경 범위를 최소화합니다.
  3. 불변성 유지: 상태를 직접 수정하지 않고 새 객체를 생성하여 변경 감지를 용이하게 합니다.
// 잘못된 예 - 상태 직접 수정
const handleClick = () => {
  const newItems = items;
  newItems.push({ id: Date.now(), text: 'New Item' });
  setItems(newItems); // React가 변경을 감지하지 못할 수 있음
};

// 올바른 예 - 불변성 유지
const handleClick = () => {
  setItems([...items, { id: Date.now(), text: 'New Item' }]);
};