필사 모드: React Fiber 내부 완전 가이드 2025: Reconciler, Scheduler, Concurrent Rendering, Hooks 심층 분석
한국어들어가며: 가장 많이 쓰이는 라이브러리의 내부
React의 지배력
2013년 Facebook이 공개한 React는 **프런트엔드의 표준**이 되었다:
- **npm 다운로드**: 주당 수천만 건.
- **Stack Overflow**: 가장 많이 질문되는 프레임워크.
- **사용하는 사이트**: 수백만 개.
- **기업 채택**: Meta, Netflix, Airbnb, Uber, Microsoft, 그리고 수많은 스타트업.
**하지만** 대부분의 React 개발자는 **내부를 모른다**. `useState`가 어떻게 작동하는지, `useEffect`의 cleanup이 언제 실행되는지, concurrent mode가 정확히 무엇을 하는지.
이 지식 없이도 React를 쓸 수 있다. 하지만 **깊은 이해**가 있으면:
- 디버깅이 쉬워진다.
- 성능 최적화가 **정확해진다**.
- 버그 패턴을 **미리 피한다**.
- 더 좋은 코드를 쓴다.
이 글에서 다룰 것
1. **가상 DOM (Virtual DOM)**: React의 기본 아이디어.
2. **Reconciliation**: 어떻게 차이를 찾나.
3. **Fiber 아키텍처**: React 16의 대변화.
4. **Scheduler**: 언제 무엇을 할지.
5. **Concurrent Rendering (React 18)**: Time slicing, Suspense.
6. **Hooks의 내부**: 어떻게 클로저로 구현되나.
7. **React 19 **: Compiler, Server Components.
8. **성능 최적화**: useMemo, useCallback, Memoization.
1. Virtual DOM: React의 시작점
DOM의 문제
**DOM (Document Object Model)**: 브라우저가 HTML을 표현하는 객체 트리.
**DOM 조작의 비용**:
- **느림**: CSS 재계산, 레이아웃, 페인트.
- **직접 조작 복잡**: `document.getElementById`, `innerHTML` 등.
- **State 관리 어려움**: UI와 state 동기화.
**2013년 이전**:
- jQuery가 주류.
- 직접 DOM 조작.
- State를 DOM에 저장 (혼란).
React의 아이디어
**"UI를 함수처럼 다루자"**:
$$UI = f(state)$$
- State가 변하면 **새 UI**.
- 사용자는 UI 선언만, **어떻게 바꿀지**는 React가.
**Virtual DOM**이 이를 가능하게 한다:
1. **가상 DOM 트리**: JavaScript 객체로 UI 표현.
2. State 변경 시 **새 가상 DOM** 생성.
3. **이전 가상 DOM과 비교** (diffing).
4. **차이만 실제 DOM에** 적용.
예시
function Counter() {
const [count, setCount] = useState(0);
return (
);
}
**처음**:
Virtual DOM:
div
h1: "Count: 0"
button: "+"
이것을 **실제 DOM**으로 렌더.
**클릭 후**:
새 Virtual DOM:
div
h1: "Count: 1" ← 변경
button: "+" ← 동일
**Diff**: `h1`의 textContent만 바뀜.
**실제 DOM 업데이트**: `h1.textContent = "Count: 1"` 만.
**Result**: 효율적, 선언적, 관리 쉬움.
JSX
**JSX**: React의 "HTML 같은" 문법.
const element = <h1>Hello</h1>;
Babel이 다음으로 컴파일:
const element = React.createElement('h1', null, 'Hello');
**React.createElement의 결과**:
{
type: 'h1',
props: { children: 'Hello' },
key: null,
ref: null,
// ...
}
이것이 **virtual DOM 노드**. 평범한 JavaScript 객체.
2. Reconciliation: 어떻게 비교하나
완벽한 diff는 비싸다
**두 트리를 완벽히 비교**하는 알고리즘: **O(n^3)**. 수천 노드가 있으면 **수 초**.
**React의 가정들**:
**1. 다른 타입 → 완전히 다른 트리**:
// Before
// After
`div` vs `span`. React는 **전체 트리 재구성**. Counter도 unmount 후 새로 mount.
**왜?**: 그런 변화는 드물고, 정확한 diff는 너무 비쌈.
**2. Key로 리스트 항목 식별**:
{items.map(item => <li key={item.id}>{item.name}</li>)}
Key가 **안정적 ID** 역할. React가 항목을 **추적** 가능.
**Key 없으면**: 인덱스 기반. 삽입/삭제 시 문제.
Diff Algorithm의 복잡도
위 가정들로 **O(n)** 에 근접. 실용적.
Reconciliation 단계
**1. Type 비교**:
- 같은 type: 속성만 업데이트.
- 다른 type: 전체 재구성.
**2. Props 비교**:
- 변경된 props만 적용.
- 이벤트 리스너 추가/제거.
**3. Children 비교**:
- Key 기반 매칭.
- 순서 변경 감지.
예시: Key의 중요성
**Key 없음**:
// 앞에 추가:
React는 **인덱스 기반**:
- 0: Apple → Cherry (update)
- 1: Banana → Apple (update)
- 2: (새로 추가) Banana
**3개 update/create**.
**Key 있음**:
// 앞에 추가:
React는 **key 기반**:
- "c": 새로 생성.
- "a": 이동만.
- "b": 이동만.
**1개만 create**. 훨씬 효율적.
**특히 중요한 경우**:
- 큰 리스트.
- 내부 state가 있는 컴포넌트.
- Animation.
3. Fiber 아키텍처 (React 16)
기존 Reconciler의 문제
**React 15 이전의 "Stack Reconciler"**:
render() {
// 1. 전체 트리 diff
// 2. 전체 트리 업데이트
// 3. 완료까지 차단
}
**문제**: **중단 불가**. 큰 트리 업데이트 시 **수백 ms 블로킹**. 애니메이션 끊김.
**브라우저 프레임**: **16.67 ms** (60fps). 그 안에 모든 작업 끝내야 부드러움.
React 15: 큰 업데이트면 16ms 초과 → **프레임 드롭**.
Fiber의 해결
**React 16** (2017): **Fiber Reconciler** 도입.
**핵심 변경**:
- **작업을 작은 단위로** 쪼갬.
- 각 단위 실행 후 **브라우저에 양보**.
- 우선순위 높은 작업 (사용자 입력 등)을 **먼저**.
- **중단과 재개 가능**.
**결과**: 부드러운 UI, 응답성 향상.
Fiber란
**Fiber**: 작업 단위를 표현하는 **JavaScript 객체**.
{
type: 'div',
key: null,
props: { ... },
stateNode: (실제 DOM 노드),
// Tree 구조
return: parent_fiber,
child: first_child_fiber,
sibling: next_sibling_fiber,
// 작업 정보
pendingProps: newProps,
memoizedProps: oldProps,
effectTag: Update,
// Alternate: 이전 버전
alternate: previousFiber,
// ... 더 많은 필드
}
**Fiber 트리**는 virtual DOM 트리에 대응하지만, **더 많은 정보**.
Fiber Tree 구조
**Linked list** 구조:
- `return`: 부모.
- `child`: 첫 자식.
- `sibling`: 다음 형제.
트리 순회가 **iterative** 가능. Stack 없이.
A
/ | \
B C D
A.child = B
B.sibling = C
C.sibling = D
B.return = A
C.return = A
D.return = A
Double Buffering
**두 개의 Fiber tree**:
**current**: 현재 화면에 반영된 트리.
**workInProgress**: 작업 중인 트리.
**작동**:
1. State 변경.
2. **workInProgress 트리** 생성 (current를 복사).
3. 변경 사항 적용.
4. 완료 시 **swap**: workInProgress가 current가 됨.
5. 실제 DOM 업데이트.
**이점**:
- 작업 중에도 현재 UI 보존.
- Commit을 원자적으로.
- Rollback 가능.
Fiber Phases
**1. Render Phase** (중단 가능):
- Fiber 트리 생성/업데이트.
- Diff 계산.
- Effects 수집.
- **부작용 없음** (DOM 수정 안 함).
- 언제든 중단, 재개, 폐기 가능.
**2. Commit Phase** (동기):
- 실제 DOM 변경 적용.
- Side effects (useEffect 등) 실행.
- **중단 불가**.
- **빠르게** 완료되어야.
Render는 여러 번 실행될 수 있지만, commit은 **한 번**. 이것이 "render는 순수해야 한다"는 이유.
4. Scheduler: 언제 무엇을 할지
Scheduler의 역할
**Scheduler**: 작업의 **우선순위**와 **타이밍**을 관리.
**목표**:
- 높은 우선순위 작업 **먼저**.
- 브라우저가 할 일 있으면 **양보**.
- 60fps 유지.
우선순위 레벨
React 18의 priorities:
1. **Immediate**: 즉시. 동기적으로.
2. **User-blocking**: 250 ms 이내. 사용자 입력 등.
3. **Normal**: 5 초 이내. 일반 업데이트.
4. **Low**: 10 초 이내. 저우선순위.
5. **Idle**: 시간 남을 때.
Time Slicing
**작업을 조각내서** 실행:
work | check time | if > 5ms, yield | work | check...
각 조각 후 브라우저에 제어권. 브라우저가 **이벤트 처리, 페인트** 등 수행 후 돌아옴.
requestIdleCallback?
초기 계획은 `requestIdleCallback` 사용. 하지만:
- **지원 부족** (Safari).
- **지연 너무 김**.
대신 **`MessageChannel`** 기반 자체 스케줄러:
const channel = new MessageChannel();
channel.port1.onmessage = performWorkUntilDeadline;
channel.port2.postMessage(null); // 다음 틱에 실행
**5ms** 기본 deadline. 각 조각 후 deadline 확인.
예시
function App() {
const [query, setQuery] = useState('');
const [list, setList] = useState(generateLargeList());
// 비싼 필터링
const filtered = useMemo(() =>
list.filter(item => item.name.includes(query)),
[list, query]
);
return (
<>
</>
);
}
**문제**: 각 글자 입력마다:
1. Input 업데이트 (즉시 원함).
2. 큰 리스트 re-render (느림).
**React 17 이전**: 둘 다 같은 우선순위. Input이 버벅임.
**React 18 + useDeferredValue**:
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(() =>
list.filter(item => item.name.includes(deferredQuery)),
[list, deferredQuery]
);
Input update는 **즉시**. 리스트는 **low priority**. Input은 부드럽게, 리스트는 조금 늦게.
5. Concurrent Rendering (React 18)
React 18의 혁명
**2022년 3월**: React 18 출시. **Concurrent Rendering** 기본 활성.
**주요 기능**:
- **Automatic batching**.
- **Transitions** (`useTransition`, `useDeferredValue`).
- **Suspense for data fetching**.
- **Streaming SSR**.
- **New APIs**: `useId`, `useSyncExternalStore`.
Concurrent vs Blocking
**Blocking rendering** (React 17):
User input
↓
setState
↓
Render (중단 불가, 200ms)
↓ (UI 멈춤)
Commit
↓
User sees new UI
**Concurrent rendering** (React 18):
User input
↓
setState (low priority)
↓
Start render (중단 가능)
↓
Another input arrives (high priority)
↓
Abort current render, handle input
↓
Render input result immediately
↓
Resume low priority render
Automatic Batching
**React 17**:
- Event handler 내 여러 setState: **한 번만** re-render.
- 그 외 (promise, setTimeout): **각각** re-render.
// React 17
setTimeout(() => {
setCount(1); // re-render
setFlag(true); // re-render
}, 100);
**React 18**:
- **모든 곳**에서 batching.
- 한 번의 re-render.
// React 18: 한 번의 re-render
setTimeout(() => {
setCount(1);
setFlag(true);
}, 100);
**결과**: 불필요한 re-render 감소.
Transitions
**중요하지 않은 업데이트를 "transition"으로 표시**:
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // 높은 우선순위 (input)
startTransition(() => {
// 낮은 우선순위 (결과)
setResults(computeExpensiveResults(e.target.value));
});
}
return (
<>
{isPending && <Spinner />}
</>
);
}
**효과**:
- Input update **즉시**.
- Results computation 백그라운드.
- `isPending` 으로 "계산 중" 상태 표시.
- 사용자가 계속 타이핑 가능.
Suspense
**Suspense**: "로딩 상태를 선언적으로".
**UserProfile**이 데이터를 fetch하는 동안 **fallback** 표시.
**이전**: 각 컴포넌트가 자기 로딩 상태 관리. 복잡, inconsistent.
**Suspense**: **컴포넌트 트리** 수준에서 로딩. 일관된 UX.
Streaming SSR
서버에서 HTML을 **스트리밍**으로:
- Skeleton 먼저 전송.
- 데이터 fetch 완료되면 **stream**으로 추가.
- **Progressive rendering**.
**Next.js 13+** 가 이 기능 적극 활용.
6. Hooks의 내부 구현
Hook이란
**Hooks** (React 16.8, 2019):
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
**함수 컴포넌트**에서 state, lifecycle 사용 가능.
**왜 혁명적**:
- 클래스 없이.
- 재사용 가능한 로직 (custom hooks).
- 더 단순한 코드.
기본 아이디어
**Hook은 어떻게 state를 기억하나**?
**핵심**: **Fiber에 hook 정보 저장**.
각 fiber에 **memoizedState** 필드. Linked list 형태로 여러 hook 저장.
fiber.memoizedState = {
// useState(0)
memoizedState: 0,
queue: updateQueue,
next: {
// useState('')
memoizedState: '',
queue: updateQueue2,
next: {
// useEffect(...)
memoizedState: { deps: [...] },
next: null,
}
}
}
**순서대로** 호출되는 hook이 순서대로 저장됨.
Hook 호출의 규칙
**왜 "Rules of Hooks"가 있는가**:
function Bad() {
if (condition) {
const [a] = useState(1); // ❌ 조건부 호출
}
const [b] = useState(2);
}
**이유**: React는 **호출 순서**로 hook을 매칭. 조건부 호출하면 순서가 바뀜.
**올바른 방법**:
function Good() {
const [a] = useState(1); // 항상 호출
const [b] = useState(2);
if (condition) {
// a 사용
}
}
useState의 구현 (단순화)
let currentFiber = null;
let hookIndex = 0;
function useState(initialValue) {
const fiber = currentFiber;
const index = hookIndex++;
// 이전 값 복원
if (!fiber.memoizedState) {
fiber.memoizedState = [];
}
if (fiber.memoizedState[index] === undefined) {
fiber.memoizedState[index] = initialValue;
}
const setState = (newValue) => {
fiber.memoizedState[index] = newValue;
scheduleRender(fiber);
};
return [fiber.memoizedState[index], setState];
}
**단순화된 모델**:
- Fiber에 hook 배열.
- 각 useState가 인덱스로 접근.
- setState가 scheduler에게 re-render 요청.
**실제 구현**은 linked list + updateQueue. 더 복잡.
useEffect의 구현 (단순화)
function useEffect(callback, deps) {
const fiber = currentFiber;
const index = hookIndex++;
const oldDeps = fiber.memoizedState[index]?.deps;
const hasChanged = !oldDeps || deps.some((d, i) => d !== oldDeps[i]);
if (hasChanged) {
fiber.effects.push({
callback,
cleanup: fiber.memoizedState[index]?.cleanup,
});
fiber.memoizedState[index] = { deps };
}
}
**Effects는 commit phase 후** 실행:
1. Render phase: effect 수집.
2. Commit phase: DOM 업데이트.
3. Effects 실행 (이전 cleanup → 새 effect).
Custom Hooks
**Hook을 재사용**:
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
return { count, increment, decrement };
}
function MyComponent() {
const { count, increment } = useCounter(10);
return <button onClick={increment}>{count}</button>;
}
**내부**: Custom hook도 그냥 함수. 내부의 `useState`가 호출자의 fiber에 저장됨.
**순서 중요**: Custom hook 호출 순서가 일정해야.
7. React 19: Compiler와 Server Components
React Compiler (2024)
**React Compiler** (이전 "React Forget"): 자동 메모이제이션.
**문제**:
function MyComponent({ items }) {
const sorted = items.sort(); // 매 render마다 새 배열
return <List items={sorted} />; // List가 매번 re-render
}
**기존 해결**:
const sorted = useMemo(() => items.sort(), [items]);
수동으로 `useMemo`, `useCallback`, `React.memo`. 번거로움.
**React Compiler**: **자동**으로 이들을 추가. 개발자는 평범한 코드 작성.
// 개발자가 쓰는 코드
function MyComponent({ items }) {
const sorted = items.sort();
return <List items={sorted} />;
}
// Compiler가 변환
function MyComponent({ items }) {
const sorted = useMemo(() => items.sort(), [items]);
return <List items={sorted} />;
}
**자동 최적화**. **2024년 실험적, 2025+ 안정화**.
Server Components
**React Server Components (RSC)**: 서버에서 렌더링되는 컴포넌트.
// ServerComponent.jsx (.server.jsx)
export default async function BlogPost({ id }) {
const post = await db.posts.get(id);
return (
);
}
**특징**:
- **서버에서만** 실행.
- JavaScript가 클라이언트로 전송 안 됨.
- DB, 파일 시스템 직접 접근.
- **제로 번들 크기**.
**Client Components**와 혼합 가능:
// 서버
export default function Page() {
return (
);
}
**Next.js 13+**, **Remix** 등이 활용.
React 19 기타
**use() hook**: Promise 직접 언랩.
function Component() {
const data = use(fetchData()); // Suspense 자동
return <div>{data}</div>;
}
**Actions**: 폼과 mutation 단순화.
**useOptimistic**: Optimistic updates.
8. 성능 최적화
React 성능의 기본 원칙
**"과도한 re-render를 피하라"**.
Re-render 발생 조건:
- State 변경.
- Props 변경.
- Context 변경.
- 부모 re-render (기본 동작).
React.memo
**Component memoization**:
const Button = React.memo(({ onClick, children }) => {
console.log('Button rendered');
return <button onClick={onClick}>{children}</button>;
});
**Props가 같으면** re-render 스킵 (shallow comparison).
**주의**:
- 객체, 배열, 함수는 매번 새로 생성 → 항상 다름.
- `useCallback`, `useMemo`와 함께.
useCallback
**함수 참조 안정화**:
const handleClick = useCallback(() => {
doSomething(value);
}, [value]);
`value`가 같으면 같은 함수 참조. Memoized 자식이 re-render 안 함.
useMemo
**값 memoization**:
const expensiveValue = useMemo(() => {
return computeExpensive(data);
}, [data]);
`data`가 같으면 같은 값. 재계산 안 함.
과도한 memoization의 함정
**memo/useCallback/useMemo는 공짜가 아니다**:
- **메모리**: 이전 값 저장.
- **비교 비용**: 의존성 비교.
- **복잡도 증가**: 코드 난해.
**규칙**:
- **기본은 안 씀**.
- 실제 성능 문제 있을 때만 (profiler로 측정).
- 비싼 계산이거나 큰 하위 트리일 때.
**React Compiler (React 19)** 가 이 모든 걸 자동화해 줄 전망.
Key 주의
{items.map((item, index) => (
))}
**Index를 key로**: 삽입/삭제 시 문제. State 섞임.
{items.map((item) => (
))}
**안정적 ID**: 올바름.
State 위치
**State는 **쓰이는 곳에 가깝게**:
// ❌ 상위에 있으면 전체 re-render
function App() {
const [inputValue, setInputValue] = useState('');
return (
);
}
// ✅ 입력 컴포넌트로 분리
function Input() {
const [value, setValue] = useState('');
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
function App() {
return (
);
}
Input의 state 변경이 **Input만** re-render.
Profiler
**React DevTools Profiler**:
- 각 컴포넌트의 render 시간.
- Why did this render?
- Flamegraph.
성능 문제의 **근본 원인**을 찾는 최선의 도구.
9. Context와 상태 관리
Context API
**Prop drilling 해결**:
const ThemeContext = createContext('light');
function App() {
return (
);
}
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
**Context 변경 시** 모든 consumer re-render.
Context의 함정
**과도 사용**: 모든 걸 context에. Re-render 폭발.
**최적화**:
1. Context를 **세분화**.
2. Memoization.
3. `useSyncExternalStore` (React 18).
**예시 문제**:
`user`만 바뀌어도 **theme 사용자도** re-render.
**해결**:
...
각 context가 독립. 바뀐 것의 consumer만 re-render.
외부 상태 관리
**Redux, Zustand, Jotai, Recoil**: 더 복잡한 state에.
**왜 필요한가**:
- Cross-cutting state.
- 복잡한 데이터 흐름.
- Time-travel debugging.
- Devtools.
**간단한 앱**: Context + hooks 충분.
**복잡한 앱**: 전용 라이브러리.
**최신 트렌드**:
- **Zustand**: 가볍고 단순.
- **Jotai**: Atomic state.
- **TanStack Query**: 서버 상태 (cache, refetch).
Redux는 여전히 쓰이지만 **점점 줄어드는 추세**.
10. 실전 패턴
Component Composition
// ❌ Props drilling
// ✅ Composition
sidebar={<Sidebar />}
content={<Content />}
/>
Context로 필요한 곳에서 받음.
Container/Presentational
**구식 패턴** (Hooks 이전):
- Container: state, logic.
- Presentational: pure render.
**현재**: **Custom hooks**로 대체.
// Custom hook
function useUserData(id) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(id).then(setUser);
}, [id]);
return user;
}
// Component
function UserProfile({ id }) {
const user = useUserData(id);
if (!user) return <Spinner />;
return <h1>{user.name}</h1>;
}
Error Boundaries
**Error를 catch**:
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// 사용
**주의**:
- Class component만 가능.
- Event handler, async code는 catch 안 됨.
Compound Components
**공유 state를 가진 관련 컴포넌트들**:
Tabs가 children에 context로 현재 tab 정보 전달.
11. React vs 다른 프레임워크
Vue
**차이점**:
- **Templates**: HTML-like, 더 단순.
- **Reactivity**: Proxy 기반 (자동).
- **Two-way binding**: `v-model`.
- **Composition API**: Hooks와 유사 (Vue 3).
**React와의 차이**:
- 학습 곡선 낮음.
- 더 "자동".
- React보다 더 opinionated.
Svelte
**차이점**:
- **컴파일 타임** 최적화.
- **Virtual DOM 없음**.
- 더 작은 bundle.
- 더 적은 코드.
**Svelte 5** (Runes): Signal-based reactivity.
Solid.js
**차이점**:
- **Fine-grained reactivity**: React의 re-render 없음.
- JSX 유지.
- 매우 빠름.
- Signal 기반.
Qwik
**차이점**:
- **Resumability**: SSR 후 hydration 없음.
- 초기 JavaScript 매우 작음.
- Edge computing 친화적.
React가 여전히 1등인 이유
**장점**:
- **생태계**: 가장 큰 커뮤니티, 가장 많은 라이브러리.
- **검증**: 수많은 대기업이 검증.
- **고용 시장**: 가장 많은 React 개발자.
- **React Native**: 모바일까지.
- **Meta의 지원**: 장기적.
**단점**:
- Virtual DOM 오버헤드.
- Re-render 관리 복잡.
- Bundle 큼.
**선택**: 대부분의 프로젝트에 **React가 안전한 선택**. 새로운 것을 원하면 Solid, Svelte 등 실험.
12. 실전 디버깅과 튜닝
React DevTools
**필수 도구**:
- **Components tab**: 트리 탐색, props/state 확인.
- **Profiler tab**: 성능 분석.
- **Why did this render?**: 원인 파악.
일반적 문제
**1. 과도한 re-render**:
- Props reference 변경.
- Context 오남용.
- State를 너무 상위에.
**해결**: Profiler → React.memo, useCallback, useMemo.
**2. 메모리 누수**:
- useEffect cleanup 안 함.
- Subscription 해제 안 함.
**해결**:
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
**3. Stale closures**:
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // count가 항상 0
}, 1000);
return () => clearInterval(interval);
}, []); // [] 때문에 count 고정
**해결**:
setCount(c => c + 1); // functional update
**4. Infinite render loops**:
useEffect(() => {
setData({ ...data, updated: true }); // data 의존성 → 무한
}, [data]);
**해결**: 의존성 배열 확인, 논리 재검토.
Best Practices
1. **Hooks 규칙 준수**: ESLint plugin.
2. **Keys 제대로**: ID 사용, index 피하기.
3. **State 분할**: 세밀하게.
4. **Pure functions**: Side effect는 useEffect로.
5. **Dependencies**: 정확히.
6. **Testing**: React Testing Library.
퀴즈로 복습하기
**A.**
**답**: **중단 가능한 rendering (Interruptible rendering)**.
**상세 설명**:
**Fiber 이전의 문제**:
**Stack Reconciler** (React 15 이하):
- 렌더링이 **재귀 함수 호출**.
- 시작하면 **끝까지 실행**.
- 중단 불가.
render(App)
→ render(Header)
→ render(Logo)
→ render(Nav)
→ render(NavItem) × 100
→ render(Main)
→ render(Article) × 50
→ render(Sidebar)
이 전체가 **하나의 call stack**. 중간에 멈출 수 없음.
**큰 업데이트의 재앙**:
- 1000개 컴포넌트 업데이트.
- 각 컴포넌트 2 ms.
- 총 2000 ms = **2 초**.
- 이 동안 **브라우저 완전 블록**.
- 사용자 입력 무시.
- 애니메이션 끊김.
- 버벅임.
**브라우저의 60 fps**:
- 프레임당 16.67 ms.
- 그 안에 JS + layout + paint + composite.
- React가 16 ms 넘으면 **프레임 드롭**.
- 사용자가 "느림" 인식.
**Fiber의 해결**:
**작업을 작은 단위로 분할**:
- 각 fiber가 하나의 작업 단위.
- 하나 처리 후 **"양보 여부" 확인**.
- 브라우저에 급한 일 있으면 **중단**.
- 나중에 **재개**.
**구현 방식**:
function workLoop(deadline) {
while (deadline.timeRemaining() > 0 && nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork) {
// 완료: commit
commitRoot();
} else {
// 중단: 다음 idle 시간에 재개
requestIdleCallback(workLoop);
}
}
**핵심**:
- `deadline.timeRemaining()`: 브라우저가 "쉴 시간" 알려줌.
- 시간 남으면 계속 작업.
- 시간 다 쓰면 중단.
- 다음 idle에 재개.
**React 18에서는** `requestIdleCallback` 대신 `MessageChannel` 기반 자체 scheduler.
**두 가지 Phase**:
**Render phase** (중단 가능):
- Fiber 트리 생성/업데이트.
- Virtual DOM diff.
- Effect 수집.
- **부작용 없음**. 언제든 버려도 OK.
**Commit phase** (중단 불가):
- 실제 DOM 수정.
- DOM effect 실행.
- **빠르게 완료**.
- 원자적.
**이 분리가 중요**:
- Render를 여러 번 시도 가능.
- 더 나은 업데이트가 오면 **이전 render 폐기**.
- Commit만 "진짜" 변경.
**이점 1: 부드러운 애니메이션**
큰 업데이트 중에도:
- 60fps 유지.
- 애니메이션 끊김 없음.
- 사용자 입력 즉시 반응.
**이점 2: 우선순위**
**높은 우선순위**:
- 사용자 입력 (키보드, 마우스).
- 애니메이션.
- 스크롤.
**낮은 우선순위**:
- 데이터 fetch 후 업데이트.
- 큰 리스트 렌더링.
- 백그라운드 작업.
React가 **자동으로 우선순위 처리**:
사용자 입력 발생
↓
Scheduler: "high priority task"
↓
현재 render 중단
↓
입력 처리 먼저
↓
입력 완료 후 render 재개
**이점 3: Concurrent features**
Fiber의 중단/재개 능력이 **concurrent mode**의 기반:
- **useTransition**: "이 업데이트는 중요하지 않음".
- **useDeferredValue**: "이 값은 지연되어도 OK".
- **Suspense**: "로딩 중엔 fallback".
이 모든 것이 **Fiber 없이는 불가능**.
**실전 예시**:
function App() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
function handleChange(e) {
setQuery(e.target.value); // high priority
startTransition(() => {
setResults(
expensiveSearch(e.target.value) // low priority
);
});
}
return (
<>
{isPending ? <Spinner /> : <Results items={results} />}
</>
);
}
**React 17 이전**:
- 각 키 입력이 **전체 업데이트** 트리거.
- 1 키 = query state + results state 둘 다 업데이트.
- expensiveSearch가 느리면 입력 버벅임.
**React 18 (Fiber + Concurrent)**:
- `setQuery`: 즉시 (input 반영).
- `startTransition` 안의 `setResults`: 낮은 우선순위.
- 사용자가 계속 타이핑 중이면 **이전 results 계산 폐기**, 새로 시작.
- Input은 **항상 부드럽게**.
**사용자 경험**: 이전엔 "Laggy", 지금은 "Smooth".
**이점 4: Double Buffering**
Fiber는 **두 개의 트리** 유지:
- **current**: 현재 화면.
- **workInProgress**: 작업 중.
작업이 완료되기 전까지 **current는 건드리지 않음**. 중간에 중단되어도 **화면은 그대로**.
완료되면 **atomic swap**. WorkInProgress → current.
**이점 5: Error 복구 (미래)**
Render 중 에러 발생 시:
- WorkInProgress 폐기.
- Current는 그대로.
- 복구 가능성.
**Error Boundary** 동작의 기반.
**단점과 복잡성**:
**1. 구현 복잡**:
- Linked list 기반 트리 순회.
- 우선순위 관리.
- Double buffering.
- **React 팀의 수년간 작업**.
**2. Memory 증가**:
- Alternate tree.
- Effect list.
- 약간의 overhead.
**3. Debugging 어려움**:
- 비선형 실행.
- DevTools가 이를 숨김.
**4. Legacy 호환성**:
- 일부 라이브러리가 깨짐.
- `useSyncExternalStore` 등으로 해결.
**역사적 관점**:
**2017년**: Fiber 출시 (React 16).
- 내부만 변경. 사용자 API 그대로.
- Concurrent features는 **opt-in** (React 18까지).
**2022년**: React 18.
- Automatic batching, transitions, Suspense 기본.
- 진짜 concurrent mode.
**2024년**: React 19.
- React Compiler.
- Server Components 성숙.
**Fiber는 5년 이상 걸린 긴 여정의 결과**. 사용자는 대부분 몰랐지만, React의 **근본 혁신**이었다.
**교훈**:
Fiber의 가치는 **"중단 가능성"** 이라는 단순한 개념이다. 하지만 이것이:
- 애니메이션 부드러움.
- 입력 반응성.
- 우선순위.
- Concurrent features.
- 자동 최적화.
**모두를 가능하게** 한다.
**"작업을 작게 쪼개서 중단 가능하게 만들라"** 는 원칙은 운영체제, 네트워킹, 분산 시스템 어디서나 유효하다. React Fiber는 **이 원칙을 UI rendering에 적용**한 성공 사례다.
당신이 React 앱을 쓸 때, 부드러운 스크롤, 반응하는 입력, 진행 중 transitioning의 뒤에는 **Fiber**가 있다. 알아차리지 못할 때 잘 작동하고 있는 것이다.
마치며: 추상화의 예술
핵심 정리
1. **Virtual DOM**: JavaScript 객체로 UI 표현.
2. **Reconciliation**: Diff + 효율적 업데이트.
3. **Fiber**: 중단 가능한 작업 단위.
4. **Scheduler**: 우선순위 기반 작업 관리.
5. **Concurrent Rendering**: Transitions, Suspense.
6. **Hooks**: Fiber에 저장된 state.
7. **React 19**: Compiler, Server Components.
React가 잘한 것
1. **Declarative**: UI = f(state).
2. **Component-based**: 재사용 가능.
3. **Virtual DOM**: 효율적 업데이트.
4. **Ecosystem**: 무한한 라이브러리.
5. **Evolution**: Class → Hooks → Concurrent → Compiler.
실전 조언
1. **기본을 신뢰하라**: 과도한 최적화 금지.
2. **Profiler 사용**: 실제 병목 찾기.
3. **State 위치**: 쓰이는 곳에 가깝게.
4. **Keys 제대로**: ID 사용.
5. **Hooks 규칙**: ESLint로 강제.
6. **React 19 기다려**: Compiler가 많은 걸 해결.
마지막 교훈
React는 **추상화의 힘**을 보여준다. 수십억 줄의 DOM 조작 코드를 **선언적 컴포넌트**로 대체했다.
그 아래에는 **엄청난 엔지니어링**:
- Fiber 아키텍처.
- 중단 가능한 렌더링.
- 우선순위 스케줄링.
- Hooks의 클로저 마법.
- 컴파일러 최적화.
**모든 것이 숨겨져 있다**. 개발자는 그저 JSX를 쓴다. 이것이 **좋은 추상화**다.
하지만 **내부를 아는** 개발자가 더 나은 코드를 쓴다. 왜 이 렌더가 발생하는지, 언제 memoize해야 하는지, 왜 hook 규칙이 있는지 — 이해하면 모든 것이 명확해진다.
당신이 다음에 `setState`를 호출할 때, 그 뒤에서 일어나는 일을 상상해 보라:
- State update 스케줄.
- Fiber tree 생성.
- Reconciliation.
- Diff.
- Effect 수집.
- Commit.
- DOM update.
- Effects 실행.
**한 줄의 코드**가 **이 모든 작업**을 trigger한다. 마법처럼 보이지만, 실제론 **우아한 엔지니어링**이다.
React의 교훈: **복잡성을 숨기되, 원할 때 들여다볼 수 있게 만들라**. 이것이 좋은 라이브러리의 정의다. React는 이 균형을 10년 넘게 지켜왔고, 앞으로도 계속 진화할 것이다.
참고 자료
- [React 공식 문서](https://react.dev/)
- [React Fiber Architecture (Andrew Clark)](https://github.com/acdlite/react-fiber-architecture)
- [React 내부: Fiber Reconciler](https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react)
- [Dan Abramov: Overreacted.io](https://overreacted.io/)
- [React Source Code](https://github.com/facebook/react)
- [React 18 Release Notes](https://react.dev/blog/2022/03/29/react-v18)
- [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components)
- [React Compiler Docs](https://react.dev/learn/react-compiler)
- [Concurrent Rendering Adoption Guide](https://react.dev/reference/react/Suspense)
- [Kent C. Dodds Blog](https://kentcdodds.com/blog)
현재 단락 (1/899)
2013년 Facebook이 공개한 React는 **프런트엔드의 표준**이 되었다: