Skip to content
Published on

React Fiber 내부 완전 가이드 2025: Reconciler, Scheduler, Concurrent Rendering, Hooks 심층 분석

Authors

들어가며: 가장 많이 쓰이는 라이브러리의 내부

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)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 (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

처음:

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
<div><Counter /></div>

// After
<span><Counter /></span>

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 없음:

<ul>
  <li>Apple</li>
  <li>Banana</li>
</ul>

// 앞에 추가:
<ul>
  <li>Cherry</li>
  <li>Apple</li>
  <li>Banana</li>
</ul>

React는 인덱스 기반:

  • 0: Apple → Cherry (update)
  • 1: Banana → Apple (update)
  • 2: (새로 추가) Banana

3개 update/create.

Key 있음:

<ul>
  <li key="a">Apple</li>
  <li key="b">Banana</li>
</ul>

// 앞에 추가:
<ul>
  <li key="c">Cherry</li>
  <li key="a">Apple</li>
  <li key="b">Banana</li>
</ul>

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 (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <BigList items={filtered} />
    </>
  );
}

문제: 각 글자 입력마다:

  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"으로 표시:

import { useTransition } from 'react';

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 (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <Results data={results} />
    </>
  );
}

효과:

  • Input update 즉시.
  • Results computation 백그라운드.
  • isPending 으로 "계산 중" 상태 표시.
  • 사용자가 계속 타이핑 가능.

Suspense

Suspense: "로딩 상태를 선언적으로".

<Suspense fallback={<Spinner />}>
  <UserProfile id={userId} />
</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)
import db from 'db';

export default async function BlogPost({ id }) {
  const post = await db.posts.get(id);
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

특징:

  • 서버에서만 실행.
  • JavaScript가 클라이언트로 전송 안 됨.
  • DB, 파일 시스템 직접 접근.
  • 제로 번들 크기.

Client Components와 혼합 가능:

// 서버
import Button from './Button.client';

export default function Page() {
  return (
    <div>
      <h1>Server rendered</h1>
      <Button />  {/* 클라이언트에서 인터랙티브 */}
    </div>
  );
}

Next.js 13+, Remix 등이 활용.

React 19 기타

use() hook: Promise 직접 언랩.

function Component() {
  const data = use(fetchData());  // Suspense 자동
  return <div>{data}</div>;
}

Actions: 폼과 mutation 단순화.

<form action={handleSubmit}>
  <input name="title" />
  <button type="submit">Save</button>
</form>

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) => (
  <Item key={index} {...item} />
))}

Index를 key로: 삽입/삭제 시 문제. State 섞임.

{items.map((item) => (
  <Item key={item.id} {...item} />
))}

안정적 ID: 올바름.

State 위치

**State는 쓰이는 곳에 가깝게:

// ❌ 상위에 있으면 전체 re-render
function App() {
  const [inputValue, setInputValue] = useState('');
  return (
    <div>
      <HeavyComponent />
      <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
    </div>
  );
}

// ✅ 입력 컴포넌트로 분리
function Input() {
  const [value, setValue] = useState('');
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

function App() {
  return (
    <div>
      <HeavyComponent />
      <Input />
    </div>
  );
}

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 (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

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).

예시 문제:

<AppContext.Provider value={{ user, theme, locale, settings }}>

user만 바뀌어도 theme 사용자도 re-render.

해결:

<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    ...
  </ThemeContext.Provider>
</UserContext.Provider>

각 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
<Layout user={user} theme={theme}>
  <Sidebar user={user} theme={theme} />
  <Content user={user} />
</Layout>

// ✅ Composition
<Layout
  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;
  }
}

// 사용
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

주의:

  • Class component만 가능.
  • Event handler, async code는 catch 안 됨.

Compound Components

공유 state를 가진 관련 컴포넌트들:

<Tabs>
  <Tabs.List>
    <Tabs.Tab>One</Tabs.Tab>
    <Tabs.Tab>Two</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Content 1</Tabs.Panel>
    <Tabs.Panel>Content 2</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

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.

퀴즈로 복습하기

Q1. React Fiber의 가장 큰 이점은 무엇인가?

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 (
    <>
      <input value={query} onChange={handleChange} />
      {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년 넘게 지켜왔고, 앞으로도 계속 진화할 것이다.


참고 자료