
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 보장
수행 작업:
- Before Mutation: DOM 변경 전
- getSnapshotBeforeUpdate 실행
- Mutation: DOM 변경
- 실제 DOM 삽입/삭제/업데이트
- ref 업데이트
- Layout: DOM 변경 직후
- useLayoutEffect 실행 (동기)
- componentDidMount/Update 실행
- 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 동작:
- 초기 렌더링
Current: [App] → [Header] → [Main] (화면 표시 중)
WIP: null - 상태 변경 발생
Current: [App] → [Header] → [Main] (화면은 이전 상태 유지)
WIP: [App'] → [Header'] → [Main'] (백그라운드에서 계산 중)- Current.alternate = WIP
- WIP.alternate = Current
- 작업 완료 (Commit Phase)
포인터만 교체: Current = WIP
화면 즉시 업데이트 (원자적) - 다음 업데이트 대기
새로운 Current: [App'] → [Header'] → [Main']
이전 Current는 다음 WIP로 재사용 대기
장점:
- 작업 중에도 이전 화면 유지 (일관성)
- 에러 발생 시 이전 상태로 롤백 가능
- 완료되면 포인터만 바꾸면 됨 (빠름)
- 메모리 재사용 (GC 압력 감소)
2.5. Fiber와 재조정 전체 흐름
- 상태 변경 발생 (setState, props 변경 등)
↓ - 우선순위 결정 (Lane 할당)
↓ - Work-in-Progress 트리 생성 (Current.alternate 활용)
↓ - Work Loop 시작
- Fiber 노드 처리 (beginWork)
- 자식으로 내려가거나 (child)
- 형제로 이동하거나 (sibling)
- 부모로 올라감 (return)
- shouldYield() 체크
- 반복
↓
- 모든 Fiber 처리 완료
↓ - Commit Phase (중단 불가)
- Before Mutation
- Mutation (DOM 변경)
- Layout (useLayoutEffect)
- Passive (useEffect)
↓
- Current ↔ WIP 포인터 교체
↓ - 화면 업데이트 완료
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
'React' 카테고리의 다른 글
| React의 실제 내부 구현으로 이해하는 useRef - useRef(2) (0) | 2025.05.14 |
|---|---|
| React의 useRef: 언제, 왜 사용하는가? - useRef(1) (0) | 2025.05.14 |
| TanStack Query 시작하기 , Tanstack Query를 사용하는 이유를 설명해 주세요 (0) | 2025.04.21 |
| React에서 데이터 로딩 상태 관리: useEffect vs Suspense (0) | 2025.04.15 |
| useEffect와 useLayoutEffect의 차이점에 대해서 설명해주세요. (0) | 2025.03.19 |