TT Lab

한국어

시작하기
블로그

블로그

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

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

React의 지배력

2013년 Facebook이 공개한 React는 프런트엔드의 표준이 되었다:

하지만 대부분의 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 조작의 비용:

2013년 이전:

React의 아이디어

"UI를 함수처럼 다루자":

UI=f(state)UI = f(state)

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 비교:

2. Props 비교:

3. Children 비교:

예시: Key의 중요성

Key 없음:

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

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

React는 인덱스 기반:

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 기반:

1개만 create. 훨씬 효율적.

특히 중요한 경우:


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 구조:

트리 순회가 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 업데이트.

이점:

Fiber Phases

1. Render Phase (중단 가능):

2. Commit Phase (동기):

Render는 여러 번 실행될 수 있지만, commit은 한 번. 이것이 "render는 순수해야 한다"는 이유.


4. Scheduler: 언제 무엇을 할지

Scheduler의 역할

Scheduler: 작업의 우선순위와 타이밍을 관리.

목표:

우선순위 레벨

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 사용. 하지만:

대신 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 기본 활성.

주요 기능:

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:

// React 17
setTimeout(() => {
  setCount(1);  // re-render
  setFlag(true);  // re-render
}, 100);

React 18:

// 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} />
    </>
  );
}

효과:

Suspense

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

<Suspense fallback={<Spinner />}>
  <UserProfile id={userId} />
</Suspense>

UserProfile이 데이터를 fetch하는 동안 fallback 표시.

이전: 각 컴포넌트가 자기 로딩 상태 관리. 복잡, inconsistent.

Suspense: 컴포넌트 트리 수준에서 로딩. 일관된 UX.

Streaming SSR

서버에서 HTML을 스트리밍으로:

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 사용 가능.

왜 혁명적:

기본 아이디어

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];
}

단순화된 모델:

실제 구현은 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>
  );
}

특징:

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 발생 조건:

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

함수 참조 안정화:

const handleClick = useCallback(() => {
  doSomething(value);
}, [value]);

value가 같으면 같은 함수 참조. Memoized 자식이 re-render 안 함.

useMemo

값 memoization:

const expensiveValue = useMemo(() => {
  return computeExpensive(data);
}, [data]);

data가 같으면 같은 값. 재계산 안 함.

과도한 memoization의 함정

memo/useCallback/useMemo는 공짜가 아니다:

규칙:

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:

성능 문제의 근본 원인을 찾는 최선의 도구.


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

왜 필요한가:

간단한 앱: Context + hooks 충분. 복잡한 앱: 전용 라이브러리.

최신 트렌드:

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 이전):

현재: 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>

주의:

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

차이점:

React와의 차이:

Svelte

차이점:

Svelte 5 (Runes): Signal-based reactivity.

Solid.js

차이점:

Qwik

차이점:

React가 여전히 1등인 이유

장점:

단점:

선택: 대부분의 프로젝트에 React가 안전한 선택. 새로운 것을 원하면 Solid, Svelte 등 실험.


12. 실전 디버깅과 튜닝

React DevTools

필수 도구:

일반적 문제

1. 과도한 re-render:

해결: Profiler → React.memo, useCallback, useMemo.

2. 메모리 누수:

해결:

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 조작 코드를 선언적 컴포넌트로 대체했다.

그 아래에는 엄청난 엔지니어링:

모든 것이 숨겨져 있다. 개발자는 그저 JSX를 쓴다. 이것이 좋은 추상화다.

하지만 내부를 아는 개발자가 더 나은 코드를 쓴다. 왜 이 렌더가 발생하는지, 언제 memoize해야 하는지, 왜 hook 규칙이 있는지 — 이해하면 모든 것이 명확해진다.

당신이 다음에 setState를 호출할 때, 그 뒤에서 일어나는 일을 상상해 보라:

한 줄의 코드가 이 모든 작업을 trigger한다. 마법처럼 보이지만, 실제론 우아한 엔지니어링이다.

React의 교훈: 복잡성을 숨기되, 원할 때 들여다볼 수 있게 만들라. 이것이 좋은 라이브러리의 정의다. React는 이 균형을 10년 넘게 지켜왔고, 앞으로도 계속 진화할 것이다.


참고 자료

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다