React

React 재조정(Reconciliation)

Chrysans 2025. 11. 24. 16:24
728x90
반응형

Reconc - reconcilation 의 줄임말

React Reconciliation - 리액트 리컨실리에이션(재조정)


1. React Reconciliation 개념 및 동작 과정

1.1. 정의

Reconciliation은 리액트가 UI를 효율적으로 업데이트하는데 사용하는 핵심 과정으로, state/props가 변경될 때 새로운 Virtual DOM 트리를 생성하여 (실제 컴포넌트 인스턴스가 아닌 가상의 React Element 트리(Virtual DOM)) 이전 트리와 비교하여 변경된 부분만 DOM에 반영합니다.


1.2. 주요 알고리즘 - Diffing

React는 O(n³) 복잡도의 완벽한 트리 비교 대신, 두 가지 가정을 기반으로 O(n) 휴리스틱 알고리즘을 사용합니다:

  • 가정 1: 다른 타입의 엘리먼트는 다른 트리를 생성한다
  • 가정 2: 개발자가 key prop으로 안정적인 자식 엘리먼트를 알려줄 수 있다
휴리스틱은 완벽하지는 않지만, 실용적으로 충분히 좋은 해결책을 빠르게 찾기 위한 경험 기반의 방법론입니다.

1.2.1. 같은 타입 요소

이전 인스턴스 재사용, state 보존, props만 갱신

import { useState } from "react";

function App() {
  const [isActive, setIsActive] = useState(false);

  return (
    <div>
      {/* MyBox 컴포넌트는 항상 동일 타입! */}
      <MyBox className={isActive ? "active" : "box"} />
      
      <button onClick={() => setIsActive(!isActive)}>
        Toggle Class
      </button>
    </div>
  );
}

function MyBox({ className }) {
  // 이 내부 state는 MyBox 인스턴스가 계속 보존된다면 항상 유지!
  const [text, setText] = useState("hello");

  return (
    <div className={className}>
      {text}
      <button onClick={() => setText("updated!")}>Update Text</button>
    </div>
  );
}

내부 동작:

  • MyBox 컴포넌트는 항상 같은 타입(div)으로 렌더링 됨
  • className(props) 값만 변할 때, MyBox의 인스턴스(컴포넌트)는 재사용되고, 내부 state(text)는 보존됨.
  • MyBox 내부의 state(text)는 그대로 유지됨.

1.2.2. 다른 타입 요소

완전히 새로 생성, 이전 인스턴스 파괴

function App() {
  const [useSpan, setUseSpan] = useState(false);

  return (
    <div>
      {/* 타입 자체가 변경됨 */}
      {useSpan ? (
        <span>Hello</span>
      ) : (
        <div>Hello</div>
      )}
      
      <button onClick={() => setUseSpan(!useSpan)}>
        Toggle Element Type
      </button>
    </div>
  );
}

내부 동작:

  • React는 두 타입이 다른 것을 확인 (div ≠ span)
  • 기존 div DOM 삭제
  • 새로운 span DOM 생성
  • 하위 트리도 전부 재생성 (비효율적이지만 대부분의 경우 정확함)
  • useEffect cleanup 실행 → 새로운 useEffect 실행

1.2.3. Depth-First Traversal (깊이 우선 탐색)

React는 트리를 깊이 우선으로 순회하며 비교합니다.

  • 루트에서 시작해 자식 노드를 따라 재귀적으로 끝까지 내려가며 순회
  • 부모 → 첫 번째 자식 → 손자... 끝까지 내려갔다가, 형제 노드로 이동
  • 비교 순서: NodeType → Props → Children

순회 순서 예시:

<App>                    // 1. App 비교
  <Header>               // 2. Header 비교
    <Logo />             // 3. Logo 비교
    <Nav />              // 4. Nav 비교
  </Header>
  <Main>                 // 5. Main 비교
    <Content />          // 6. Content 비교
  </Main>
</App>

1.2.4. 배열/리스트의 자식 비교

Key 속성의 중요성

 

리스트 렌더링 시 key를 명확하게 설정하면 '동일 레벨' 노드 비교를 최적화할 수 있습니다.

 

key 없을 때 (위치 기반 매칭 - 내용 업데이트 3회):

['빨강', '파랑', '초록'] → ['초록', '빨강', '파랑']

위치 0: "빨강" 텍스트를 지우고 → "초록" 텍스트 새로 그림
위치 1: "파랑" 텍스트를 지우고 → "빨강" 텍스트 새로 그림
위치 2: "초록" 텍스트를 지우고 → "파랑" 텍스트 새로 그림

작업 내용: JavaScript 실행 + 화면 다시 그리기 × 3
문제점: DOM 노드는 재사용하지만 내용을 3번 업데이트 (비효율)

 

key 있을 때 (ID 기반 매칭 - DOM 이동 2회):

[key=빨강, key=파랑, key=초록] → [key=초록, key=빨강, key=파랑]

React 판단: "아, 순서만 바뀌었네"

DOM 이동 1: 초록 요소를 맨 앞으로 옮김
DOM 이동 2: 파랑 요소를 맨 뒤로 옮김
(빨강은 자동으로 중간 위치됨)

작업 내용: 이미 그려진 요소의 위치만 이동
장점: 내용 업데이트 없이 DOM 이동만 수행 (효율적)


1.3. Render Phase vs Commit Phase

Reconciliation은 내부적으로 두 단계로 나뉩니다.

 

1.3.1. Render Phase (재조정/비교 단계)

특징:

  • Virtual DOM을 비교하고 변경사항을 계산
  • 중단 가능(interruptible): 우선순위 높은 작업이 오면 멈출 수 있음
  • 순수 계산만 수행, 부수효과(side effect) 없음
  • 여러 번 실행될 수 있음 (중단되었다가 다시 시작)
  • 비동기적으로 처리 가능

수행 작업:

  • 컴포넌트 함수 실행
  • Hooks 처리 (useState, useEffect 등록)
  • Virtual DOM 트리 생성
  • 이전 트리와 비교 (Diffing)
  • 변경이 필요한 노드에 effectTag 표시

1.3.2. Commit Phase (반영 단계)

특징:

  • 계산된 변경사항을 실제 DOM에 반영
  • 중단 불가능(non-interruptible): 한번 시작하면 끝까지 실행
  • 부수효과(side effect) 실행
  • 동기적으로 한 번만 실행
  • 사용자에게 일관된 UI 보장

수행 작업:

  1. Before Mutation: DOM 변경 전
    • getSnapshotBeforeUpdate 실행
  2. Mutation: DOM 변경
    • 실제 DOM 삽입/삭제/업데이트
    • ref 업데이트
  3. Layout: DOM 변경 직후
    • useLayoutEffect 실행 (동기)
    • componentDidMount/Update 실행
  4. Passive: 브라우저 paint 이후
    • useEffect 실행 (비동기)

전체 흐름:

[Render Phase - 중단 가능]

Virtual DOM 비교 완료

[Commit Phase - 중단 불가능]

Before Mutation (DOM 변경 전)

Mutation (실제 DOM 변경)

Layout (useLayoutEffect 동기 실행)

브라우저 Paint (화면 업데이트)

Passive (useEffect 비동기 실행)


2. Fiber 아키텍처

2.1. 기존 구조의 한계 (Stack Reconciler)

React 16 이전에는 중단 불가능하고 재귀적인 구조(Stack Reconciler)였기 때문에 다음과 같은 문제가 발생했습니다.

 

문제점:

  • 트리 규모가 커질수록 전체 렌더링 지연
  • 메인 스레드 블로킹 (UI 프리즈 현상)
  • 사용자 입력 무시
  • 우선순위 개념 없음

예시:

// Stack Reconciler (React 15)
function reconcile(element) {
  updateNode(element);
  element.children.forEach(child => {
    reconcile(child); // 재귀 - 끝까지 중단 불가
  });
}

// 1000개 노드 업데이트 = 300ms 동안 멈춤

2.2. Fiber 아키텍처란?

Fiber는 트리를 작은 작업 단위(Fiber node)로 분할해, 우선순위 기반 작업 스케줄링과 일시정지/재개를 지원하는 새 재조정 엔진입니다. 이는 React 18의 동시성 기능을 가능하게 하는 기반이 되었습니다.

 

핵심 개념:

  • 각 React Element = 하나의 Fiber 노드
  • Fiber 노드 = 작업의 최소 단위
  • 링크드 리스트 구조로 순회 제어 가능
  • 작업 중단/재개 가능

2.3. Fiber 노드 구조

각 컴포넌트는 Fiber 구조로 추상화됩니다.

Fiber 노드의 주요 속성:

{
  // 컴포넌트 정보
  type: Component,           // 컴포넌트 타입 (div, span, Function 등)
  key: 'item-1',            // key prop
  
  // 데이터
  props: { ... },           // 현재 props
  memoizedProps: { ... },   // 이전 props
  memoizedState: { ... },   // 현재 state (Hooks 정보)
  updateQueue: [...],       // 대기 중인 업데이트
  
  // 트리 구조 (링크드 리스트)
  child: Fiber,             // 첫 번째 자식
  sibling: Fiber,           // 다음 형제
  return: Fiber,            // 부모 (return up)
  
  // DOM 연결
  stateNode: DOMNode,       // 실제 DOM 노드 참조
  
  // Double Buffering
  alternate: Fiber,         // Current ↔ Work-in-progress 연결
  
  // 작업 정보
  flags: number,            // 어떤 작업? (Placement, Update, Deletion 등)
  subtreeFlags: number,     // 하위 트리의 flags
  deletions: [Fiber],       // 삭제할 자식들
  
  // 우선순위
  lanes: number,            // 이 Fiber의 우선순위
  childLanes: number,       // 자식들의 우선순위
}

 

트리 구조 표현 (링크드 리스트):

<div>
  <h1>Title</h1>
  <p>Content</p>
</div>

위 구조는 다음과 같이 Fiber로 표현됩니다:

Fiber_div
├─ child: Fiber_h1
│  └─ sibling: Fiber_p
│     └─ sibling: null
└─ return: (parent)

순회: div → h1 (child) → p (sibling) → null

이 구조 덕분에:

  • 재귀 없이 반복문으로 순회 가능 (이전은 단방향 참조로 순회 불가능 / fiber 부터는 부모<->자식 참조 양방향)
  • 언제든 멈추고 다시 시작 가능
  • JavaScript 콜 스택과 독립적

2.4. 두 종류의 Fiber 트리 (Double Buffering)

React는 항상 두 개의 Fiber 트리를 유지합니다.

Current Fiber Tree:

  • 현재 화면에 렌더링된 트리
  • 사용자가 보고 있는 상태

Work-in-Progress Fiber Tree (WIP):

  • 변경 작업 중인 트리
  • 백그라운드에서 계산 중
  • 완료 시 Current와 교체

Double Buffering 동작:

  1. 초기 렌더링
    Current: [App] → [Header] → [Main] (화면 표시 중)
    WIP: null
  2. 상태 변경 발생
    Current: [App] → [Header] → [Main] (화면은 이전 상태 유지)
    WIP: [App'] → [Header'] → [Main'] (백그라운드에서 계산 중)
    • Current.alternate = WIP
    • WIP.alternate = Current
  3. 작업 완료 (Commit Phase)
    포인터만 교체: Current = WIP
    화면 즉시 업데이트 (원자적)
  4. 다음 업데이트 대기
    새로운 Current: [App'] → [Header'] → [Main']
    이전 Current는 다음 WIP로 재사용 대기

장점:

  • 작업 중에도 이전 화면 유지 (일관성)
  • 에러 발생 시 이전 상태로 롤백 가능
  • 완료되면 포인터만 바꾸면 됨 (빠름)
  • 메모리 재사용 (GC 압력 감소)

2.5. Fiber와 재조정 전체 흐름

  1. 상태 변경 발생 (setState, props 변경 등)
  2. 우선순위 결정 (Lane 할당)
  3. Work-in-Progress 트리 생성 (Current.alternate 활용)
  4. Work Loop 시작
    • Fiber 노드 처리 (beginWork)
    • 자식으로 내려가거나 (child)
    • 형제로 이동하거나 (sibling)
    • 부모로 올라감 (return)
    • shouldYield() 체크
    • 반복
  5. 모든 Fiber 처리 완료
  6. Commit Phase (중단 불가)
    • Before Mutation
    • Mutation (DOM 변경)
    • Layout (useLayoutEffect)
    • Passive (useEffect)
  7. Current ↔ WIP 포인터 교체
  8. 화면 업데이트 완료

3. 정리

3.1. Reconciliation의 핵심

  • 목적: 효율적인 UI 업데이트
  • 방법: Virtual DOM Diffing (O(n) 휴리스틱)
  • 원칙:
    • 다른 타입 = 다른 트리
    • key로 자식 추적
    • 같은 레벨만 비교

3.2. Fiber의 핵심

  • 목적: 중단 가능한 재조정
  • 구조: 링크드 리스트 기반 노드
  • 기능:
    • 작업 분할 (Work Loop)
    • 우선순위 스케줄링 (Lanes)
    • Double Buffering (안정성)
  • 결과: React 18 동시성 기능의 기반

 

https://ko.legacy.reactjs.org/docs/reconciliation.html

 

재조정 (Reconciliation) – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

728x90
반응형