React 톺아보기 4편 - 재조정

2025년 05월 05일

개발React

1. 도입부

React는 직접 DOM을 조작함으로 발생했던 성능 이슈를 해결하기 위해 가상 DOM을 도입했습니다. DOM의 변경 사항을 확인하기 위해 모든 부분을 직접 비교하는 것이 아닌, 가상 DOM을 생성하여 변경된 부분만 실제 DOM에 적용합니다.

특히 React는 원하는 UI의 상태를 알려주면 자동으로 DOM은 업데이트가 됩니다. 즉, 선언적 API가 가능하여 사용자는 깊게 고민하지 않고도 원하는 상태의 UI를 확인할 수 있습니다. 그러나 내부적으로는 기존의 가상 DOM과 변경 사항이 생긴 가상 DOM을 비교하여 반영하는 작업이 발생하는데 이러한 과정을 재조정 이라고 합니다.

2. 재조정이란?

재조정(Reconciliation)이란 React가 UI를 갱신할 때 가상 DOM에서 변경 사항을 식별하여 실제 DOM에 반영하는 프로세스를 의미합니다.

쉽게 말해, React는 UI를 그릴 때 전체를 새롭게 그리지 않고, 이전 상태와 새로운 상태를 비교하여 달라진 부분만 최소한의 비용으로 업데이트 합니다.

이렇게 하면 성능이 크게 향상됩니다.

3. 왜 재조정이 필요할까?

재조정의 필요성을 이해하려면 React가 등장하기 이전의 DOM 조작 방식을 떠올려 보면 됩니다. 이전 가상 DOM에 대해 알아봤으므로 이번에는 간단하게만 살펴보겠습니다.

과거의 DOM 조작 방식

과거에는 자바스크립트로 DOM을 직접 변경했습니다. 이전에도 말했지만 이러한 직접적인 조작은 빈번하고 복잡한 DOM 조작에서 성능 저하를 유발했습니다.

const element = document.getElementById('title')
element.textContent = 'Hello World'

React 등장 이후의 방식

React는 이를 해결하기 위해 가상 DOM을 도입합니다. 따라서 정리하자면 아래와 같습니다.

  • 가상 DOM(Virtual DOM): 메모리에 있는 실제 DOM의 가상 표현
  • 재조정: 가상 DOM과 실제 DOM 간 차이를 효율적으로 식별하고 최소한의 조작만 수행

이렇게 직접적인 DOM 조작을 피함으로 성능 향상을 이루어냈습니다.

4. React에서 재조정의 작동 원리

  1. 가상 DOM 생성

React는 컴포넌트를 처음 렌더링할 때 메모리에 가상 DOM 트리를 만듭니다.

// 컴포넌트 최초 렌더링
function App() {
  return <h1>Hello World</h1>
}

이 JSX는 다음과 같은 형태로 변환됩니다.

{
    type: 'h1',
    props: { children: 'Hello World'}
}
  1. 변경된 가상 DOM 생성

상태(state)나 속성(props)이 변경되면 React는 새로운 가상 DOM 트리를 만듭니다. 예를 들어, 상태가 변경되었다고 가정해보겠습니다.

function App({name}) {
    return <h1>Hello {name}</h1>
}

// 이전 상태
<App name="World" />

// 새로운 상태
<App name="React" />

위 코드에서 두 경우의 가상 DOM은 다음과 같습니다.

// 이전 상태
{type: 'h1', props: {children: 'Hello World'}}

// 이후
{type: 'h1', props: {children: 'Hello React'}}
  1. Diffing 알고리즘을 이용한 비교하는

React는 이전 가상 DOM과 새로운 가상 DOM의 차이를 식별합니다. 이 때 사용하는 알고리즘을 Diffing 알고리즘 이라고 합니다.

Diffing 알고리즘은 다음 원칙을 따릅니다.

  • 서로 다른 타입의 엘리먼트는 트리에서 완전히 제거하고 새로 생성
  • 같은 타입일 경우, 속성과 자식을 확인하여 변경된 부분만 수정

예를 들어 아래와 같은 경우는 <h1>은 변화가 없고, <p> 태그의 자식만 변경되는데, React는 이 점을 인식하여 <p>의 내용만 업데이트합니다.

// 이전 렌더링
<div>
  <h1>React</h1>
  <p>Hello</p>
</div>

// 이후 렌더링
<div>
  <h1>React</h1>
  <p>World</p>
</div>

또한 React는 재조정 과정에서 여러 가상 DOM 업데이트를 모아 한 번의 DOM 업데이트로 결합한 후 실제 DOM에 대한 업데이트를 일괄 처리 합니다. 이 과정은 아래 단계에서 자세히 볼 수 있습니다.

  1. 최소한의 DOM 업데이트 (Commit 단계)

Diffing 과정에서 발견한 변경 사항을 실제 DOM에 적용하는 단계를 Commit 단계 라고 합니다.

이 때 React는 최소한의 DOM 연산만 수행합니다.

  • 변경된 노드의 텍스트나 속성 업데이트
  • 필요하면 노드를 추가하거나 제거

이렇게 하면 불필요한 DOM 연산을 줄이고 성능을 향상시킬 수 있습니다.

5. 키(Key)의 중요성

리스트를 다룰 때 React에서 재조정 효율성을 높이려면 키(Key) 사용이 필수입니다.

  1. 키(Key)가 없을 때의 문제도

리스트 아이템이 추가되거나 삭제 될 때 React가 변경된 위치를 찾기 어렵습니다. 그러면 DOM을 불필요하게 많이 업데이트 하는 경우가 생길 수 있습니다.

// Key 없는 경우
{
  items.map((item) => <li>{item.text}</li>)
}
  1. 키(Key)를 이용한 해결책 키(Key)를 사용하면 React는 리스트 항목의 변경을 쉽게 파악하고, 불필요한 DOM 업데이트를 최소화합니다.
// 올바른 예시 (Key가 있는 경우)
{
  items.map((item) => <li key={item.id}>{item.text}</li>)
}

6. 파이버 재조정자(Fiber Reconciler)

그렇다면 이 재조정 과정에 근간이 되는 파이버 재조정자(Fiber Reconciler)에 대해 알아보겠습니다. 사실 React는 v16 버전부터 파이버 재조정자를 사용했는데 그 이전에는 스택 재조정자를 사용하여 재조정하였습니다.

1. 스택 재조정자

  • 스택 기반 알고리즘을 사용해 새 가상 트리를 이전 가상 트리와 비교하고 그에 따라 DOM을 업데이트합니다.
  • 동기적으로 UI를 업데이트하여 큰 규모 애플리케이션에서 성능 이슈가 발생했습니다. (긴 렌더링 시간, 애니메이션 끊김 등)
  • 즉, 필수는 아니지만 계산 비용이 비싼 컴포넌트가 렌더링을 막아버리면 사용자 입력 혹은 다음 컴포넌트가 렌더링 되기 전까지 버벅임을 겪게 할 수 있습니다.

2. 파이버 등장 배경

그래서 이러한 문제를 해결하기 위해 Fiber 아키텍처를 도입하여 이러한 문제를 해결했습니다. 파이버 재조정자는 조정자를 위한 작업 단위를 나타내는 파이버라는 데이터 구조가 사용됩니다.

파이버의 핵심 아이디어는 렌더링 작업을 작은 단위로 쪼갠 후, 우선순위를 정해 비동기적으로 처리하는 것입니다.

이렇게 하면 렌더링 작업을 더 작은 단위로 분할할 수 있고, 우선순위를 정해 중요한 작업을 먼저 처리하여 스택 재조정자의 문제를 해결할 수 있습니다.

3. 파이버 노드란?

파이버에서 모든 UI 구성 요소는 파이버 노드 라는 객체 형태로 나타납니다. 아래와 같은 예시를 보며 이해할 수 있습니다.

<div>
  <h1>Hello</h1>
  <p>World</p>
</div>

위 코드는 아래와 같은 파이버 노드로 변환됩니다. 또 이렇게 연결된 파이버 노드는 파이버 트리를 구성합니다.

FiberNode {
    type: 'div', // 컴포넌트 유형
    props: {children: [...]}, // 속성 값
    stateNode: HTMLElement, // 실제 DOM 요소에 대한 참조
    child: FiberNode (h1), // 자식
    sibling: FiberNode (p), // 형제
    return: FiberNode (parent), // 부모 노드 연결결
    alternate: FiberNode (previous state), // 이전 렌더링 상태를 담고 있는 노드 (비교 위한 용도)
}

4. 파이버 재조정자란?

정리하자면, 파이버 재조정자(Fiber Reconciler)는 파이버 트리를 이용해 UI를 업데이트 하는 React의 내부 알고리즘입니다. 쉽게 말하면, 파이버 노드 간의 차이를 계산하여, 화면을 최소 비용으로 업데이트 하는 역할을 합니다.

주요 프로세스는 두 단계로 나뉩니다.

  • Render 단계 (비동기) : 파이버 노드의 변경 사항을 계산
  • Commit 단계 (동기) : 실제 DOM에 변경 사항을 반영

5. 파이버 재조정자의 작동 과정

  1. Render 단계 (비동기 처리)
  • 변경된 컴포넌트로부터 시작하여 파이버 트리를 탐색합니다.
  • 새로운 가상 DOM과 이전의 파이버 노드를 비교하여 변경된 사항을 찾습니다.
  • 이를 작업 단위로 나누어 처리합니다. 이 때 각 작업 단위는 파이버 노드 하나를 처리합니다. => 이를 통해 높은 우선순위 작업이 발생하면 낮은 우선순위 작업을 잠시 중단하고, 중요한 작업을 먼저 처리할 수 있습니다.
  1. Commit 단계 (동기 처리)

Render 단계에서 찾은 변경 사항을 실제 DOM에 적용합니다. 반드시 동기적으로 처리됩니다. 이 단계에서는 3가지 작업이 진행됩니다.

  • Before mutation : DOM 변경 전에 실행되는 라이프사이클 메서드 실행
  • Mutation : 실제 DOM 업데이트
  • Layout : DOM 변경 후 라이프사이클 메서드 실행

이 Commit 단계에서는 중간에 중단될 수 없으며 반드시 끝까지 완료되어야합니다.

6. 파이버 재조정자의 장점

  • 우선순위 기반 업데이트 가능
  • 중단 및 재개 가능
  • 병렬 처리와 동시성 지원

결론

파이버 재조정자를 사용한 재조정은 가상 DOM을 이용해 UI 업데이트를 최적화하고자 하는 React의 핵심 원리입니다. 이러한 재조정 과정을 통해 React는 동적이고 복잡한 UI를 다루는 앱에서도 뛰어난 성능과 사용성을 제공할 수 있게 되었습니다. 아직은 개념적으로만 와닿고 있지만 실제 코드를 보면서 이해해보는 과정도 반드시 거쳐야할 것 같습니다. 나름 서서히 React를 이해하고 있는 것 같아 기분이 좋아 다음 글도 빠른 시일내에 작성해보도록 하겠습니다.