
어려운 내용은 최대한 빼고 작성하였습니다. 머리로 그림을 그리듯 읽어가면 좋을 것 같습니다.
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로 이어집니다.