가상돔(Virtual DOM)과 재조정(Reconciliation)

heyj·2024년 12월 11일
0

React 공부하기

목록 보기
6/10

들어가며

HTML element들을 담고 있는 웹페이지를 Document라고 부르고, DOM이란 이 html element들을 tree형태로 표현한 것을 말합니다.

출처: https://www.linode.com/docs/guides/traversing-the-dom/

DOM tree안에는 node가 있어, 개발자들이 DOM이 제공하는 API를 통해 DOM구조에 접근하고 원하는 element를 조작할 수 있습니다. 자주 사용되는 getElementById, getElementByClass등의 API를 이용해 DOM안의 element에 접근해 내용, 스타일 등을 변경할 수 있습니다.

이렇게 변경된 내용 및 스타일들은 화면에 어떻게 적용될까요?

출처: https://web.dev/articles/howbrowserswork?hl=ko
HTML을 파싱하면 DOM트리가 생성되고 CSS를 파싱한 CSSOM트리와 함께 렌더 트리를 만들게 됩니다. 그 뒤 레이아웃 과정(리플로우)을 거쳐 각 요소의 크기와 위치가 계산되고 실제 화면에 픽셀을 그리는 페인팅 과정이 일어나게 됩니다. 만약 DOM에 변경이 생긴다면 이 과정을 처음부터 다시 진행해야 되는 거죠. 이로 인해 잦은 리플로우/리페인팅이 발생되고, 대규모의 DOM조작 시 성능 저하가 심각해진다는 문제점이 있었습니다. 이런 문제를 해결하기 위해 가상DOM과 같은 추상화 계층을 React, Vue등에서 도입하게 됩니다.

가상돔(Virtual DOM)

SPA의 가상돔은 실제돔과 같은 내용을 담고 있는 복사본이라고 생각하면 됩니다. 이 복사본은 실제돔이 아닌 JS객체형태로 메모리 안에 저장됩니다. 이 가상돔은 실제돔의 복사본이기 때문에 실제 DOM의 모든 element들과 속성을 공유합니다.

작동방식은 다음과 같습니다.
1. 데이터가 변경되면 전체 UI를 가상돔에 리렌더링
2. 이전 가상돔과 새로운 가상돔을 비교(Diffing)
3. 실제로 변경된 부분만 실제돔에 적용(Reconciliation)

다시 말해 실제돔의 변경 부분만 랜더링되고 나머지 트리에 대해서는 렌더링이 되지 않습니다. 이는 앱과 페이지의 속도와 효율성에 영향을 미칩니다.


출처:https://javascript.plainenglish.io/a-guide-for-understanding-virtual-dom-tekolio-2f107d5122cc

위 그림에서 파란색 원은 DOM의 원래 상태를 나타내고, 빨간색 원은 업데이트 된 상태를 나타냅니다.
실제돔은 랜더링 과정에서 변경된 부분만 적용시켰을 뿐 그 이전에 다른 변화는 없었습니다. 가상돔이 실제돔보다 먼저 업데이트되고 변경사항을 감지하기 때문입니다. 이로써 실제돔에서는 변경된 부분만 적용시켜 렌더링이 되기 때문에 더 빠르고 효율적입니다. 또한 모든 작업이 batch update로 이뤄지기 때문에 효율적입니다.

이 과정을 재조정(Reconciliation)이라고 부릅니다.

재조정(Reconiliation)

앞서 설명한 바와 같이 재조정 과정을 통해 React는 불필요한 DOM조작을 최소화하고, 효율적인 UI업데이트를 수행합니다. 이로써 사용자들에게 더 나은 경험을 제공하죠.

재조정의 기본 원리를 코드로 표현하면 아래와 같습니다.

// 변경 전 컴포넌트
<div>
  <Counter value={1} />
  <TodoList items={['A', 'B']} />
</div>

// 변경 후 컴포넌트
<div>
  <Counter value={2} />
  <TodoList items={['A', 'B', 'C']} />
</div>

React는 두 트리를 비교할 때

1) 서로 다른 타입의 엘리먼트는 다른 트리를 만든다.
2) key prop을 통해 여러 렌더링 사이에서 유지되어야 할 자식 엘리먼트를 식별할 수 있다.

이 2가지 가정을 기반으로 동작합니다.

아래 코드에서 div가 span 태그로 변경됐을 경우 전체 하위 트리를 재구성하며 재조정 과정이 일어납니다.

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

두 번째 key prop을 통해 유지되어야 할 자식 엘리먼트를 식별할 수 있는데 사용 예제는 다음과 같습니다.

<ul>
  {items.map(item => (
    <li key={item.id}>{item.text}</li>
  ))}
</ul>

주의해야 할 점은 index로 key를 설정할 경우 렌더링 과정에서 index값이 변경되면 이에 따라 key값도 변경되는 문제가 일어날 수 있다는 것입니다. 최후의 수단으로 배열이 index를 key로 이용할 수 있지만, 이는 항목들이 재배열되지 않을 것이라는 확신이 있을 때만 이용해야 합니다. key는 id와 같은 고유한 키값을 이용하는 것이 좋습니다.

function TodoList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item.text}</li>  // 인덱스를 key로 사용
      ))}
    </ul>
  );
}

function TodoList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li>  // 고유 ID를 key로 사용
      ))}
    </ul>
  );
}

0개의 댓글