React 동시성(Concurrency) 이해하기 — 1편: Fiber 아키텍처
리액트에서 Fiber 아키텍처를 통해 동시성이 실행되는 과정을 순서대로 이해해봅니다.
이해를 돕기 위한
remotion으로 제작된 가이드 영상입니다.
1. React란
React는 createRoot()를 통해 root 엘리먼트 하위에 JSX를 렌더링하는 UI 프레임워크입니다.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);내부적으로 JSX는 React.createElement()로 변환되어 일반 객체(React 엘리먼트)가 되고,
React는 이를 실제 DOM과 동기화합니다. 이 과정을 재조정(Reconciliation) 이라고 합니다.
JSX 작성
↓
React.createElement() → React 엘리먼트(일반 객체)
↓
Reconciliation (재조정) → 이전 트리와 비교
↓
실제 DOM 업데이트2. 왜 동시성이 필요했나
JavaScript는 싱글 스레드 언어입니다. 즉, 한 번에 하나의 작업만 처리할 수 있습니다.
React 16 이전의 재조정 방식은 Stack Reconciler 기반이었는데, 콜스택에 올라간 렌더링 작업은 끝날 때까지 중단이 불가능했습니다.
예를 들어, 검색창에 타이핑하면서 10,000개의 리스트를 필터링한다고 가정해봅시다.
[React 16 이전 — 블로킹 렌더링]
메인 스레드
┌─────────────────────────────────────┬───────┬───────┐
│ Rendering (Non-interruptible) │ Ready │ Input │
└─────────────────────────────────────┴───────┴───────┘
0ms 2100ms 2300ms
→ 렌더링이 끝나야만 사용자 입력에 반응 가능
→ 그 사이 타이핑은 멈추거나 버벅임렌더링이 메인 스레드를 점령하는 동안 사용자 입력은 큐에서 대기하고, 렌더링이 끝나야만 반응할 수 있었습니다.
3. Fiber 아키텍처 (React 16)
이 문제를 해결하기 위해 React 16에서 Fiber 아키텍처가 도입됩니다.
Fiber의 핵심은 렌더링 작업을 잘게 쪼갤 수 있다는 것입니다.
각 컴포넌트는 하나의 Fiber 노드가 되고, 이 노드들이 트리를 이루며 재조정 작업의 단위가 됩니다.
[Fiber 트리 구조]
HostRoot
│
<App />
/ \
<Header /> <Main />
/ \
<List /> <Footer />
│
<Item /> → <Item /> → <Item />
(child) (sibling) (sibling)
각 노드(Fiber)는 아래 정보를 가짐
├── 컴포넌트 정보 (type, props, state)
├── 트리 구조 (child, sibling, return)
└── 작업 상태 (어디까지 처리했는지)React의 렌더링은 내부적으로 세 단계로 나뉩니다.
[React 렌더링 3단계]
Render Reconcile Commit
┌──────────┐ ┌─────────────┐ ┌───────────┐
│ JSX → │ │ Prev Tree │ │ Changes │
│ React │→ │ vs │→ │ Reflected │
│ Element │ │ New Tree │ │ in DOM │
└──────────┘ └─────────────┘ └───────────┘
(객체 생성) (변경점 파악) (DOM 업데이트)Fiber는 이 과정을 단위별로 쪼개서 “여기까지 했다”를 기억할 수 있게 되었습니다.
[Fiber 도입 전 vs 후]
도입 전 (Stack Reconciler)
┌──────────────────────────────────┐
│ App → Header → Main → List → ... │ 한 덩어리로 끝까지 실행
└──────────────────────────────────┘
도입 후 (Fiber Reconciler)
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ App │ │Header│ │ Main │ │ List │ 단위별로 쪼개서 실행
└──────┘ └──────┘ └──────┘ └──────┘
✓ 완료 ✓ 완료 진행중 대기중
↑
"여기까지 했다" 기억 가능단, React 16~17에서는 이 구조를 갖췄지만 여전히 동기적(Sync)으로 순서대로 실행했습니다.
쪼갤 수 있는 구조는 마련됐지만, 우선순위를 부여하는 스케줄러는 아직 비활성화 상태였습니다.
4. 동시성 (React 18)
React 18에서는 Fiber 구조를 활용해 우선순위 기반 스케줄링이 본격적으로 활성화됩니다.
이것이 바로 동시성(Concurrency) 입니다.
[업데이트 우선순위]
긴급한 업데이트 (Urgent)
└── 사용자 입력, 클릭, 키 입력
└── 즉각 반응 필요 → 최우선 처리
전환 업데이트 (Transition)
└── 검색 결과 렌더링, 필터링, 페이지 전환
└── 잠깐 지연돼도 OK → 후순위 처리이제 React는 렌더링 도중 더 긴급한 작업이 들어오면 기존 렌더링을 중단하고, 긴급한 작업을 먼저 처리한 뒤 돌아와서 재개합니다.
[React 17 vs React 18 타임라인]
React 17
메인 스레드
┌─────────────────────────────────────┬───────┬───────┐
│ Rendering (Non-interruptible) │ Ready │ Input │
└─────────────────────────────────────┴───────┴───────┘
React 18
메인 스레드
┌────────┬───────┬────────┬───────┬────────┬───────┬──────────┐
│ Render │ Input │ Render │ Input │ Render │ Input │ Complete │
└────────┴───────┴────────┴───────┴────────┴───────┴──────────┘
↑ ↑
렌더링 중단 긴급한 입력 처리 후 재개startTransition을 사용하면 어떤 업데이트가 긴급하고, 어떤 게 전환인지 React에 알려줄 수 있습니다.
import { startTransition } from 'react';
// 긴급한 업데이트 — 즉시 반영
setInputValue(input);
// 전환 업데이트 — 후순위로 처리
startTransition(() => {
setSearchQuery(input);
});5. 오해하기 쉬운 포인트
“동시성 = 동시에 렌더링한다?”
아닙니다. JavaScript는 여전히 싱글 스레드입니다.
[동시성의 실제 동작]
실제로 일어나는 일 사용자 눈에 보이는 것
┌──────────────────────────┐ ┌──────────────────┐
│ Render (5ms) │ │ │
│ Input (2ms) │ → │ Responsive and │
│ Render (5ms) │ │ smooth UI! │
│ Input (2ms) │ │ │
│ ... │ └──────────────────┘
└──────────────────────────┘
빠르게 번갈아 실행 (싱글 스레드)렌더링을 잘게 쪼개서 우선순위에 따라 빠르게 번갈아 실행하는 것이고, 사용자 눈에는 마치 동시에 처리되는 것처럼 보이는 겁니다.
마치 영화 필름처럼 — 사실은 연속된 사진인데, 빠르게 넘기면 움직이는 것처럼 보이는 것과 같습니다.
마치며
[1편 핵심 요약]
React 15 이전 → Stack Reconciler
렌더링 = 한 덩어리, 중단 불가
React 16~17 → Fiber 아키텍처 도입
렌더링을 Fiber 단위로 쪼갬
단, 여전히 동기적으로 실행
React 18 → Concurrency 활성화
Fiber + 우선순위 스케줄러
중단 / 재개 / 폐기 가능
✨ 오해 바로잡기
1. 동시성 ≠ 동시에 실행
2. 동시성 = 우선순위에 따라 빠르게 번갈아 실행2편에서는 이 동시성을 기반으로 한 Hydration과 Streaming SSR로 이어집니다.