프로필 로고
2026-05-17

React Fiber

React 16의 Fiber가 렌더링을 중단·재개 가능하게 만든 작업 단위이자 컴포넌트 상태를 보관하는 객체임을, Stack Reconciler의 한계, return·child·sibling 포인터 순회, alternate 더블 버퍼링, lanes 우선순위, 훅 연결 리스트, 렌더·커밋 페이즈, 호스트/합성 컴포넌트, 엘리먼트와의 차이로 설명한다.

  • React
  • Fiber

파이버는 리액트의 작업 단위이자 컴포넌트의 상태를 보관하는 객체로, 렌더링을 중단·재개 가능하게 만들기 위해 도입된 자료구조이다.

파이버가 등장한 배경

  1. 리액트 16 이전에는 스택 재조정자(Stack Reconciler)라는 방식으로 렌더링을 수행했다.

  2. 이 방식은 컴포넌트 트리를 재귀 호출로 순회하면서 변경 사항을 한 번에 처리했다.

  3. 재귀 호출은 호출 스택 안에 현재 작업 위치가 암묵적으로 저장되기 때문에 한 번 시작되면 중간에 멈출 수 없다.

  4. 트리가 크거나 계산이 무거운 컴포넌트가 있으면 메인 스레드가 오래 점유된다.

  5. 60fps 환경에서는 한 프레임이 약 16ms이며, 이 시간 안에 브라우저가 JS 실행, 스타일 계산, 레이아웃, 페인트, 합성을 모두 끝내야 한다.

  6. 따라서 렌더링이 메인 스레드를 오래 점유하면 입력, 애니메이션, 페인트가 지연되어 화면이 끊겨 보인다.

  7. 결국 렌더링 작업을 잘게 쪼개고, 메인 스레드에 주기적으로 제어권을 양보하면서, 우선순위에 따라 중단·재개할 수 있는 새로운 구조가 필요해졌다.

파이버의 정체

  1. 파이버는 두 가지 의미를 동시에 가진다.

  2. 하나는 리액트 16에서 도입된 새로운 재조정 알고리즘의 이름이다.

  3. 다른 하나는 그 알고리즘이 다루는 작업 단위, 즉 하나의 컴포넌트(또는 DOM 요소)에 대응하는 객체이다.

  4. 이번 글에서 다루는 "컴포넌트 안의 정보를 저장하는 객체"라는 관점은 후자의 의미에 가깝다.

  5. 즉 파이버 노드 하나는 컴포넌트 인스턴스 하나에 대한 메타데이터를 담는 자바스크립트 객체이다.

  6. 리액트는 컴포넌트 트리 전체를 파이버 노드의 연결 구조로 표현한 파이버 트리를 가지고 동작한다.

  7. 기존 재귀 기반 트리는 호출 스택 안에 현재 작업 위치가 암묵적으로 저장되지만, 파이버는 각 노드가 부모·자식·형제를 직접 가리키도록 만들어 현재 작업 위치를 객체 그래프로 명시적으로 저장한다.

  8. 이 덕분에 렌더링을 중간에 멈췄다가 같은 위치부터 다시 이어서 실행할 수 있다.

파이버 노드의 구조

  1. 파이버 노드는 자신의 위치, 자신이 어떤 컴포넌트인지, 그리고 어떤 상태를 가졌는지를 모두 담는다.

  2. 단순화하면 다음과 같은 필드들로 이루어져 있다.

    const fiber = {
      type,          // 함수/클래스 컴포넌트 또는 'div' 같은 태그
      key,           // 리스트 식별자
      stateNode,     // 실제 DOM 노드 또는 클래스 인스턴스
      return,        // 부모 파이버
      child,         // 첫 번째 자식 파이버
      sibling,       // 다음 형제 파이버
      alternate,     // current ↔ workInProgress 짝
      pendingProps,  // 새로 들어온 props
      memoizedProps, // 이전 렌더에서 사용한 props
      memoizedState, // 현재 파이버가 보관하는 상태 정보, 함수 컴포넌트에서는 훅 리스트의 시작점
      updateQueue,   // 처리 대기 중인 업데이트들
      flags,         // 수행해야 할 작업 표시 (추가/삭제/갱신 등)
      lanes,         // 우선순위 비트마스크
    };
  3. return, child, sibling 포인터를 사용해 순회 상태를 파이버 객체 자체에 저장하므로, 호출 스택에 의존하지 않고도 작업을 중단·재개할 수 있다.

  4. 리액트는 이 포인터들을 따라 깊이 우선 탐색(DFS) 방식으로 트리를 순회한다.

  5. 자식이 있으면 자식 방향으로 끝까지 내려가고, 더 내려갈 자식이 없으면 형제로 이동하며, 형제도 없으면 부모로 거슬러 올라가 다음 형제를 찾는다.

  6. alternate 필드는 더블 버퍼링을 위한 장치이다.

  7. 리액트는 화면에 반영된 현재 트리(current)와 작업 중인 트리(workInProgress)를 동시에 두고, 두 트리의 같은 위치 노드를 alternate로 서로 가리키게 한다.

  8. 작업이 완료되면 두 트리의 역할을 통째로 교체하기 때문에 변경을 원자적으로 반영할 수 있다.

  9. lanes는 이 파이버에 걸린 업데이트의 우선순위를 표현하는 32비트 비트마스크이다.

  10. 리액트 16 초기에는 단일 숫자 기반의 ExpirationTime 방식이었지만, 17 이후로는 여러 종류의 업데이트를 독립된 차선(Lane)에 할당하는 Lanes 시스템으로 바뀌었다.

  11. 이 구조 덕분에 사용자 입력 같은 우선순위 높은 작업이 백그라운드 렌더링 같은 낮은 작업을 인터럽트하고 먼저 처리될 수 있다.

파이버와 훅의 관계

  1. 커스텀 훅을 공부하다 보면 파이버 개념을 마주하게 된다.

  2. useState, useEffect 같은 훅이 호출될 때 그 상태가 저장되는 장소가 바로 현재 렌더링 중인 컴포넌트의 파이버 노드이다.

  3. 정확히는 함수 컴포넌트의 파이버에서 memoizedState 필드가 첫 번째 훅 객체를 가리키고, 각 훅 객체가 next 포인터로 다음 훅을 가리키는 단방향 연결 리스트를 이룬다.

  4. 개념적으로 표현하면 다음과 같다.

    // fiber.memoizedState → 첫 번째 훅 객체
    const hook1 = {
      memoizedState: 0,            // useState의 현재 값
      queue: { ... },              // 처리 대기 중인 업데이트 큐
      next: hook2,                 // 다음 훅을 가리키는 포인터
    };
     
    const hook2 = {
      memoizedState: { create, deps }, // useEffect의 정보
      next: null,
    };
  5. 훅은 이름이나 키로 식별되지 않고 호출 순서만으로 식별된다.

  6. 매 렌더마다 리액트는 파이버에 매달린 훅 리스트를 처음부터 순서대로 따라가며 각 훅 호출과 매칭한다.

  7. 그래서 훅을 조건문이나 반복문 안에서 호출하면 순서가 어긋나 다른 훅의 상태를 가져오는 문제가 발생한다.

  8. 커스텀 훅도 결국 내부에서 기본 훅을 호출하는 함수일 뿐이며, 호출되는 순간 그 호출이 일어난 컴포넌트의 파이버 리스트에 그대로 이어 붙는다.

  9. 즉 커스텀 훅이 "상태를 가진다"고 느껴지지만 실제로 상태는 그 훅을 호출한 컴포넌트의 파이버에 저장된다.

파이버의 작업 흐름

  1. 파이버 기반 렌더링은 크게 렌더 페이즈와 커밋 페이즈로 나뉜다.

  2. 렌더 페이즈는 기본적으로 중단 가능한 구조로 설계되어 있으며, 동시성 렌더링에서는 작업의 우선순위(lane)에 따라 중단·재개될 수 있다.

  3. 다만 동기 차선(Sync lane)에 들어간 업데이트는 사실상 끊기지 않고 끝까지 실행된다.

  4. 이 단계의 중심은 작업 루프이며, 한 번에 하나의 파이버를 처리하고 다음 파이버로 넘어간다.

  5. 개념적으로는 다음과 같은 구조이다.

    function workLoop() {
      while (workInProgress !== null && !shouldYield()) {
        workInProgress = performUnitOfWork(workInProgress);
      }
      if (workInProgress !== null) {
        scheduleCallback(workLoop); // 다음 기회에 이어서 작업
      } else {
        commitRoot();               // 모든 작업이 끝나면 커밋
      }
    }
  6. 각 파이버는 beginWork에서 자식 파이버를 생성하거나 갱신하며 트리를 내려가고, 더 내려갈 자식이 없으면 completeWork로 위로 거슬러 올라간다.

  7. 리액트 스케줄러는 현재 프레임의 실행 예산을 초과했다고 판단되면 shouldYield를 통해 작업을 멈추고 브라우저에 제어권을 양보한다.

  8. 이후 다시 기회가 오면 멈춘 파이버부터 이어서 작업을 재개한다.

  9. 모든 파이버에 대한 계산이 끝나면 커밋 페이즈로 넘어간다.

  10. 커밋 페이즈는 중단 불가능한 단계로, flags에 표시된 변경을 실제 DOM에 한 번에 적용한다.

  11. 이 시점에 currentworkInProgress의 역할이 교체되어 새 트리가 현재 트리가 된다.

  12. useEffect로 등록된 부수효과는 커밋 페이즈 내부가 아니라, 브라우저가 화면을 실제로 그린(Paint) 이후에 비동기적으로 실행된다.

  13. 만약 useEffect가 동기적으로 실행되면 효과 함수의 무거운 작업이 페인트를 지연시켜 파이버 도입 목적인 60fps 유지가 흔들리기 때문이다.

  14. 반면 useLayoutEffect는 DOM 반영 직후, 브라우저가 화면을 그리기 전에 동기적으로 실행된다.

파이버의 의의

  1. 파이버 덕분에 리액트는 렌더링을 잘게 쪼개어 우선순위에 따라 처리할 수 있게 되었다.

  2. 이를 기반으로 Suspense, useTransition, 동시성 렌더링 같은 기능이 가능해졌다.

  3. 또한 훅이라는 패러다임 자체도 파이버가 있었기에 등장할 수 있었는데, 함수 컴포넌트가 매번 다시 호출되어도 파이버가 상태를 외부에서 안정적으로 보관해 주기 때문이다.

  4. 결국 파이버는 "리액트가 어떻게 렌더링하는가"와 "리액트가 상태를 어디에 보관하는가"라는 두 질문에 동시에 답하는 자료구조이다.

호스트 컴포넌트와 합성 컴포넌트

  1. 리액트 트리의 모든 노드는 자기만의 파이버를 가진다.

  2. 함수 컴포넌트, 클래스 컴포넌트뿐 아니라 div, p 같은 HTML 태그도 각각 하나의 파이버 노드로 표현된다.

  3. 리액트 내부에서는 이 둘을 구분하기 위해 합성 컴포넌트(composite component)와 호스트 컴포넌트(host component)라는 용어를 쓴다.

  4. 합성 컴포넌트는 함수나 클래스로 정의된 사용자 컴포넌트로, 다른 컴포넌트나 호스트 컴포넌트를 자식으로 반환한다.

  5. 호스트 컴포넌트는 렌더링 환경의 말단 노드이며, 브라우저에서는 div, span, p 같은 HTML 태그가 이에 해당한다.

  6. 파이버에서 둘을 구분하는 기준은 type 필드의 값이다.

  7. 합성 컴포넌트의 type은 함수나 클래스 자체이고, 호스트 컴포넌트의 type'div', 'p' 같은 문자열이다.

  8. <App><div><p /></div></App> 같은 트리에서는 App, div, p 모두 각자의 파이버 노드를 가지며, 부모-자식 관계는 child·return 포인터로 연결된다.

  9. 실제 DOM 변경이 발생하는 지점은 오직 호스트 컴포넌트의 파이버이며, 합성 컴포넌트의 파이버는 어떤 호스트 컴포넌트를 만들지 계산하는 역할만 한다.

리액트 엘리먼트와 파이버의 차이

  1. JSX로 작성한 코드는 빌드 시 React.createElement() 또는 _jsx() 호출로 변환되어 리액트 엘리먼트라는 일반 자바스크립트 객체를 만든다.

  2. 엘리먼트는 대략 다음과 같은 형태의 단순한 객체이다.

    // <div className="box">Hello</div> 가 만드는 엘리먼트
    const element = {
      type: 'div',
      props: { className: 'box', children: 'Hello' },
      key: null,
      ref: null,
    };
  3. 엘리먼트는 "무엇을 그려야 하는가"를 설명하는 불변 객체이며, 매 렌더마다 새로 생성되고 버려진다.

  4. 반면 파이버는 "그 엘리먼트를 언제, 어떻게 처리할 것인가"를 추적하는 가변 객체이며, 컴포넌트 인스턴스 단위로 살아남아 재사용된다.

  5. 처음 마운트할 때 리액트는 각 엘리먼트로부터 대응되는 파이버를 생성하고, 엘리먼트의 typekey를 그대로 파이버에 복사한다.

  6. 이후 재렌더가 일어나면 새로 만들어진 엘리먼트를 같은 자리의 기존 파이버와 비교하고, typekey가 같으면 파이버를 재사용하면서 pendingProps만 갱신한다.

  7. 정리하면 엘리먼트는 매번 새로 만들어지는 일회용 설명서이고, 파이버는 그 설명서들을 받아 작업 상태를 누적 관리하는 영속적인 작업 단위이다.