Fiber architecture

hong·2026년 4월 15일

javascript

목록 보기
10/12
post-thumbnail

Fiber architecture

리액트가 화면 업데이트 작업을 더 잘게 쪼개서, 우선순위를 나누고, 중간에 멈췄다가, 다시 이어서 할 수 있게 만든 내부 구조.

왜 하필 fiber라는 이름일까? (한국어로 ‘섬유’)
아주 가벼운 실행 단위/ 촘촘한 연결구조라는 느낌을 주기 위해서다.
업데이트를 한 덩어리로 처리하지 않고 작은 단위의 작업들로 쪼개서 관리하고싶었고, 그 작은 단위가 fiber인것.

시작하기전에…

노드와 포인터

노드 : 데이터를 담는 단위 객체
포인터 : 다른 노드로 이어지는 참조/연결 정보

예 )

[철수] ----친구----> [영희]

철수 : 노드
영희를 가리키는 정보 : 포인터

🙋‍♀️DOM vs DOM 노드

DOM : 문서 전체를 객체 구조로 표현한 것 (나무 전체 느낌)
DOM 노드 : 그 DOM을 이루는 개별 요소 하나하나 (나무의 가지, 잎, 줄기 하나하나 느낌)

재귀 트리 순회

우선 트리 구조 예시 :

A
├─ B
│  ├─ D
│  └─ E
└─ C
   └─ F

A는 루트, B,C는 A의 자식, D,E는 B의 자식, F는 C의 자식.
트리 순회란 트리 안의 노드들을 어떤 순서로 방문할지 정하는 것.
재귀는 함수가 자기 자신을 다시 호출하는 방식.

그래서 재귀 트리 순회는 이런 방식으로 동작 : A를 처리하고, 자식들을 각각 다시 같은 방식으로 처리.

예시 : 부모먼저, 자식 나중 식으로 순회하면 A - B - D - E - C - F 순서가 됨

포인터 구조

포인터 : 다른 노드를 가리키는 참조/연결 정보 . 예를 들어 A -> B -> C 여기서 화살표가 포인터 느낌.
포인터 구조란, 노드들이 서로를 가리키는 구조.

current tree / work-in-progress tree

Current tree : 지금 화면에 반영된 현재 트리
work-in-progress tree : 다음 화면을 만들기 위해 작업 중인 트리

즉 지금 보이는 UI는 건드리지않고 뒤에서 다음 버전을 준비해두는 느낌.
작업이 다 끝나면 둘을 바꿔치기하듯 교체함.

  • current fiber tree와 새 React element를 바탕으로 work-in-progress tree를 만들어 가는 과정 : reconciliation
  • 바꿔치기하듯 교체 : commit

linked list

노드들이 포인터(참조)로 서로 연결된 자료구조.

배열처럼 한 덩어리로 쭉 들어있는게 아니라, 각 칸이 자기 다음 칸이 누군지 알고 있는 구조라고 보면 됨

[A] -> [B] -> [C] -> null

렌더 단계와 커밋 단계

리액트 업데이트는 크게 2단계로 생각하면 됨. Render phase, commit phase.

Render phase 는 어떤 변경이 필요한지 계산하고, 새 fiber 트리 만들거나 비교하고, 어떤 DOM 변경이 필요한지 결정함.
Commit phase는 실제 DOM 반영하고, ref 적용하고, lifecycle / effect 처리함.
Commit phase단계는 보통 끊지 않고 한 번에 처리됨.

왜 fiber가 필요한가?

예를들어 화면에 엄청 큰 리스트가 있고 상태가 바뀌어서 전부 다시 계산해봐야한다고 하면.

렌더링 시작 -> 끝날 때까지 계속 작업 -> 그동안 브라우저는 다른일 처리 못함

이렇게 됨. 그러면 사용자는 ‘입력이 늦게 먹히네’ ‘스크롤이 버벅이네’ 라고 느낌 .

이걸 바꾸기 위해서 작업을 작은 단위로 나누고, 급한 작업을 먼저 처리하고, 필요하면 하던 작업 잠시 멈추고, 나중에 다시 이어서 처리하는 식으로 만들게 됨

이덕에 작업 우선순위 관리, time slicing, interruptible rendering, concurrent rendering, suspense같은 비동기 친화적 렌더링 모델을 지원할 수 있게됨

(interruptible rendering : 렌더링 작업을 하다가 중간에 멈출 수 있는 것)

fiber가 정확히 뭐냐?

fiber는 추상적인 개념이 아니라 리액트가 각 컴포넌트를 표현하는 ‘작업 단위 객체’ 같은 것임.

리액트는 컴포넌트 트리를 그대로 다루는게 아니라 그걸 내부적으로 fiber 노드들의 트리로 관리함.

예를 들어 이런 컴포넌트가 있다면

<App>
  <Header />
  <Main>
    <Sidebar />
    <Content />
  </Main>
</App>

리액트 내부에서는 이런식의 fiber 트리로 관리함.

App fiber
* Header fiber
* Main fiber
    * Sidebar fiber
    * Content fiber

렌더링 단위마다 대응되는 fiber 노드가 있다고 보면 됨.

Fiber 노드에는 뭐가 들어있나

fiber노드는 단순히 ‘이 컴포넌트가 있다’만 저장하는게 아니고 렌더링에 필요한 정보들을 많이 들고있음.
예를들면 이 노드의 타입은 뭔지, 어떤 prop를 받았는지, 어떤 state를 갖고있는지, 부모/자식/형제 노드가 누군지, 이번 업데이트에서 해야할 작업이 뭔지, DOM에 반영해야할 변경사항이 뭔지.

즉 렌더링 작업을 위한 메모장 + 연결 리스트 + 작업 큐 역할을 같이 한다고 보면 됨.

🙋‍♀️ 그렇담 reconciliation도 여기서 하는건가? 라는 궁금증이…
(Reconciliation: 이전 UI와 다음 UI를 비교해서 뭐가 바뀌었는지 판단하는 과정)

Reconciliation도 규모가 커지면 무거워지는데, fiber는 그 reconciliation을 더 잘게 나눠서 처리할 수 있게 만든 기반 구조임.
즉 reconciliation이 무엇이 바뀌었는지 비교하는 과정이라면 fiber architecture는 그 비교와 업데이트를 유연하게 수행하기 위한 내부엔진이라고 보면 됨.

reconciliation은 트리 전체에 대해 수행되지만, 그 비교 작업은 fiber node를 방문하면서 node 단위로 진행됨.

Fiber node 구조에 관해서

Fiber node는 렌더링 단위마다 하나씩!

JSX :

<App>
  <Header />
  <Main>
    <Sidebar />
    <Content />
  </Main>
</App>

트리 :

App
├─ Header
└─ Main
   ├─ Sidebar
   └─ Content

리액트 내부에서는 이걸 Fiber node들의 연결 구조로 관리함.
일반적인 ‘트리 객체 안에 children 배열’ 느낌으로만 관리하는게 아니라 linked list 비슷한 방식으로 관리함.

Child / sibling / return

Fiber node의 핵심 연결 포인터는 이 3개임 :

child : 첫번째 자식
Sibling : 다음 형제
Return : 부모

여기서 return은 예약어처럼 보여도 리액트 내부 필드 이름이 진짜 return임 !
의미는 부모로 돌아가는 포인터.

아까의 트리를 fiber 식으로 연결하면 대충 이런 느낌 :

App
 └─ child -> Header
Header
 └─ sibling -> Main
Main
 └─ child -> Sidebar
Sidebar
 └─ sibling -> Content

그리고 각 노드는 자기 부모를 가리키는 return도 가지고 있음

App
  child = Header
  return = null

Header
  sibling = Main
  return = App

Main
  child = Sidebar
  return = App

Sidebar
  sibling = Content
  return = Main

Content
  return = Main

이런 식으로!

왜 children 배열로 안 하고 이렇게 하냐?

이렇게 하면 리액트가 트리를 순회하고 작업 단위를 쪼개기가 쉬워짐.

예를들어 리액트는 렌더링할 때 대충 이런 흐름으로 돌아다닐 수 있음 :
현재 노드 방문 -> child 있으면 child로 내려감 -> child 없으면 sibling으로 감 -> sibling도 없으면 return 타고 부모로 올라감 -> 올라가다가 부모의 sibling있으면 거기로 감

즉 재귀 트리 순회같은 걸 더 명시적인 포인터 구조로 들고있는 느낌

실제로 이동하는 방식

App
├─ Header
└─ Main
   ├─ Sidebar
   └─ Content

이걸 fiber 포인터로 움직이면

App에서 child로 Header감
Header에서는 child 없으니까 sibling으로 Main감
Main에서는 child로 Sidebar 감
Sidebar는 child 없으니까 sibling으로 Content감
Content는 sibling 없으니까 return으로 Main 올라감
Main도 sibling 없으니까 return으로 App 올라감
App도 끝

핵심 포인트: child는 “첫 번째 자식”만 가리킴

child는 모든 자식을 배열처럼 들고있는게 아니라 첫번째 자식 하나만 가리킴.
나머지 자식들은 sibling 체인으로 연결됨

<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

이런게 fiber 라면

ul.child = li(A)

li(A).sibling = li(B)
li(B).sibling = li(C)
li(C).sibling = null

즉, 부모는 첫째만 알고있고 형제들은 서로 다음 형제들을 알고있음

alternate는 뭐냐

alternate는 현재 fiber와 그에 대응하는 반대편 fiber를 연결하는 포인터

리액트는 보통 두 트리를 생각함 (current tree /지금 화면에 반영된 트리, workInProgress tree/다음 렌더 준비중인 트리)
그래서 같은 컴포넌트라도 이 두 트리 안에 각각 fiber node가 하나씩 있을 수 있음

이 둘을 연결하는게 alternate.

 Current App Fiber  <-->  WorkInProgress App Fiber
       alternate            alternate

즉 alternate는 같은 컴포넌트의 현재버전/작업 중 버전을 연결하는 선으로 보면 됨

🙋‍♀️ 왜 필요함?

작업 중간에 실패하거나 멈춰도 지금 사용자에게 보이는 화면은 안전하게 보이게 하기 위해서.
그래서 현재 화면은 current, 다음 화면 계산은 workInProgress이렇게 따로 들고가고
둘을 alternate로 연결해서 비교하거나 재사용함.

이 덕분에 리액트는 이전 상태를 참조하고, 새 작업 트리를 생성하고, 완료되면 트리를 교체하는게 가능한거임

Fiber node에 자주 들어있는 다른 필드들

Type : 어떤 컴포넌트/태그인지
stateNode : 실제 DOM 노드나 클래스 인스턴스

pendingProps : 새로 들어온 props
memoizedProps: 이전에 반영된 props
memoizedState: 현재 state
flags: 이 노드에서 어떤 작업을 해야 하는지
Child
Sibling
Return
Alternate

🙋‍♀️ flags는 뭐지

리액트가 reconciliation하다가 이 노드는 DOM 생성해야해, 이 노드는 업데이트해야해, 이 노드는 삭제해야해 이런걸 표시해두는 곳.
여기 있는걸 commit phase에서 실제 반영함.

헷갈리는거

🙋‍♀️fiber node와 DOM node의 차이

Fiber node : React 내부의 작업용 노드
DOM node : 브라우저가 실제 화면을 위해 갖고 있는 노드

React는 자기 내부에서 Fiber tree를 관리하고, 브라우저는 자기 내부에서 DOM tree를 관리함.
그리고 React가 필요할 때 Fiber를 바탕으로 DOM node를 만들거나 수정함.

Fiber tree   <----연결/참조---->   DOM tree

이런 느낌.

JSX :

function App() {
  return (
    <div>
      <h1>Hello</h1>
    </div>
  );
}

React :

Fiber(App)
  child -> Fiber(div)

Fiber(div)
  child -> Fiber(h1)
  stateNode -> 실제 <div> DOM node

Fiber(h1)
  stateNode -> 실제 <h1> DOM node

브라우저 :

DOM <div>
 └ DOM <h1>
    └ DOM Text("Hello")

즉 Fiber tree가 있고, DOM tree가 있고, 일부 Fiber가 대응되는 DOM node를 참조함
하지만 모든 Fiber node가 DOM node를 가지는 것(1:1 대응)은 아님.

함수 컴포넌트 Fiber나 Fragment Fiber 이런건 직접 대응되는 DOM node가 없을 수 있음.

function MyComponent() {
  return <div>Hello</div>;
}

이런 경우는 MyComponent용 Fiber가 있지만 MyComponent 자체에 대응되는 DOM node는 없음.
실제 DOM node는 < div > 쪽에만 있음

Fiber(MyComponent)   // DOM node 없음
  child -> Fiber(div)

Fiber(div)
  stateNode -> DOM <div>

Fiber node가 공사 설계/작업 관리 카드라면 DOM node는 실제 건물 부품임.
즉 설계 카드 안에 건물 전체가 들어있는 게 아니라, 그 카드가 ‘이 부품은 이거야’하고 실제 부품을 가리킬 수 있는것.

🙋‍♀️Virtual DOM은 어떻게 다른거임?

DOM도 있고 DOM node도 있고 Fiber node도 있고 Virtual DOM도 있고…
헷갈림

DOM : 브라우저가 실제 화면을 표현하기 위해 갖고 있는 객체 트리
DOM node : 그 DOM 트리의 개별 노드
Virtual DOM : React가 ‘다음 UI가 어떻게 생겨야 하는지’ 표현하는 js 객체 개념
Fiber node : React가 렌더링 작업을 수행하기 위해 쓰는 내부 작업 단위 노드

즉 실제 화면 쪽은 DOM, React 내부 계산/관리 쪽은 Virtual DOM, Fiber

DOM 예시 :

<div id="app">
  <h1>Hello</h1>
</div>

브라우저는 이걸 객체 트리로 바꿔서 관리하고, div,h1,”Hello”하나하나가 DOM node

Document
 └ html
    └ body
       └ div#app
          └ h1
             └ "Hello"

Virtual DOM 예시 :

<div>
  <h1>Hello</h1>
</div>

->

{
  type: 'div',
  props: {
    children: {
      type: 'h1',
      props: {
        children: 'Hello'
      }
    }
  }
}

실제 브라우저 DOM이 아니라 React가 UI를 설명하기 위해 만든 가상 표현.

🙋‍♀️Virtual DOM이랑 Fiber는 같은건지?

Virtual DOM은 UI를 표현하는 개념/객체
Fiber는 그 UI를 렌더링하기 위해 React가 내부적으로 쓰는 작업 노드 구조

🙋‍♀️ 순서가 어떻게되는건지?

  • 컴포넌트가 JSX 반환
  • 그 JSX는 React element/Virtual DOM 같은 JS 표현이 됨
  • React는 그걸 바탕으로 Fiber node들을 만들거나 업데이트함
  • Fiber에서 reconciliation/render 작업 수행
  • commit phase에서 실제 DOM node 생성/수정

컴포넌트 :

function App() {
  return (
    <div>
      <h1>Hello</h1>
    </div>
  );
}

Virtual DOM :

App returns:
div
 └ h1
    └ "Hello"

Fiber : 
Fiber(App)
  child -> Fiber(div)

Fiber(div)
  child -> Fiber(h1)
  stateNode -> DOM <div>

Fiber(h1)
  stateNode -> DOM <h1>

DOM :

DOM <div>
 └ DOM <h1>
    └ DOM Text("Hello")

🙋‍♀️Virtual DOM vs Fiber vs DOM

Virtual DOM : React가 UI를 설명하는 가상 표현 (설명서)
Fiber : 그 UI를 업데이트하기 위해 쓰는 작업 구조 (작업 관리자)
DOM : 최종 실제 화면 구조 (실물)

🙋‍♀️실제 흐름 질문

<div>
  <h1>{text1}</h1>
  <h2>{text2}</h2>
  <input type="text" />
</div>

이 경우

Text1, text2가 바뀌고 React가 새 렌더 결과를 계산하기 시작하고, work-in-progress tree를 만들고 있음, text1까지 만들었고 아직 commit은 안함. 그 사이 input에 타이핑이 들어오면 어떻게 되는가? 

입력 반영이 더 중요해서 현재 진행 중이던 render work를 중단하고, input 관련 render/commit을 먼저 처리될 수 있음.
그 뒤 나머지 text1, text2 쪽 작업을 이어서 하거나 다시 시작함.

text1까지 만들었는데 어떻게 되나? -text1까지 work-in-progress를 만들어놨다고 해도 commit 전이라면 작업 중인 초안이기 때문에, 잠깐 보류할 수도 있고 필요하면 버리고 다시 계산할 수도 있음. 즉 render phase에서 계산한 내용은 commit 전까지는 확정이 아님.

🙋‍♀️ Fiber = concurrent rendering ?

No. fiber는 concurrent 기능을 가능하게 만든 기반 구조고, concurrent rendering은 그 위에서 동작하는 렌더링 방식임

profile
프론트엔드 개발을 하고 있습니다 ⌨️

0개의 댓글