- Published on
React Fiber는 왜 만들어졌을까?
- Authors

- Name
- GW
개요
React를 공부하다 보면 한 번쯤 이런 이야기를 들어봤을 것입니다.
React 16부터 Fiber가 도입되었다.
동시성 렌더링은 Fiber 덕분에 가능해졌다.
그런데 정작 Fiber가 무엇인지 물어보면 명확하게 설명하기는 쉽지 않습니다.
많은 글에서는 Fiber Node, Linked List 같은 내부 구현부터 설명합니다.
물론 중요한 내용이지만, 그보다 먼저 이해해야 하는 질문이 있습니다.
React는 왜 기존 구조를 버리고 Fiber를 새로 만들었을까?
이 질문을 이해하면 다음 내용까지 자연스럽게 이해할 수 있습니다.
- Fiber가 해결하려던 문제
- 동시성 렌더링이 가능한 이유
- Render와 Commit이 왜 분리되어 있는지
이번 글에서는 Fiber의 왜 Fiber가 필요했는지를 중심으로 살펴보겠습니다.
기존 React는 무엇이 문제였을까?
React는 state가 변경되면 새로운 화면을 계산(Render)한 뒤, 실제 DOM을 변경(Commit)합니다.
그런데 React 15까지의 렌더링 방식에는 한 가지 큰 문제가 있었습니다.
한 번 Render를 시작하면 끝날 때까지 멈출 수 없었다는 것입니다.
예를 들어 10,000개의 리스트를 렌더링한다고 가정해 보겠습니다.
Render 시작
↓
1
↓
2
↓
3
↓
...
↓
10000
↓
Render 완료
이 과정이 시작되면 React는 중간에 작업을 멈추지 않고 끝까지 실행했습니다.
이를 동기(Synchronous) 렌더링이라고 합니다.
문제는 Render가 오래 걸리는 경우입니다.
Render가 진행되는 동안에는 JavaScript가 해당 작업을 계속 수행해야 하므로 다른 작업을 처리하지 못합니다.
예를 들어 사용자가 버튼을 클릭하거나 입력을 시작해도 아래처럼 Render가 끝난 뒤에야 이벤트를 처리할 수 있습니다.
긴 Render
↓
브라우저 대기 발생
↓
Render 종료
↓
클릭 이벤트 처리
그래서 화면이 잠시 멈춘 것처럼 느껴질 수 있습니다.
리스트가 많거나 컴포넌트 트리가 복잡할수록 이러한 현상은 더욱 두드러집니다.
React Fiber란 무엇일까?
기존 React의 가장 큰 문제는 Render를 시작하면 중간에 멈출 수 없었다는 것이었습니다.
React Fiber는 이러한 문제를 해결하기 위해 도입된 새로운 렌더링 아키텍처입니다.
Fiber의 핵심 아이디어는 생각보다 단순합니다.
큰 작업을 한 번에 처리하지 말고, 작은 작업으로 나누어 처리하자.
예를 들어 이전에는 10,000개의 컴포넌트를 렌더링해야 한다면 하나의 긴 Render 작업을 끝까지 처리해야 했습니다.
10,000개 Render
↓
한 번에 끝까지 실행
↓
Render 완료
Fiber에서는 하나의 큰 Render를 여러 개의 작은 작업 단위로 나누어 처리할 수 있습니다.
예를 들어 Render 작업을 여러 구간으로 나누어 처리한다고 가정하면 다음과 같이 이해할 수 있습니다.
Render 시작
↓
1 ~ 100 처리
↓
필요하면 잠시 중단
↓
101 ~ 200 처리
↓
필요하면 잠시 중단
↓
...
↓
Render 완료
여기서 1 ~ 100, 101 ~ 200은 Fiber가 실제로 100개씩 작업을 나누어 처리한다는 의미는 아닙니다.
하나의 긴 Render를 여러 개의 작은 작업 단위로 나누고, 각 작업 사이에서 필요에 따라 중단하거나 다시 이어서 처리할 수 있다는 개념을 단순화한 예시입니다.
이렇게 작업을 나누면 React는 단순히 Render를 끝까지 빠르게 처리하는 것뿐만 아니라, 상황에 따라 Render를 중단하고 더 중요한 작업을 먼저 처리할 수 있습니다.
예를 들어 사용자가 입력창에 글자를 입력하는 동시에 화면에서 많은 컴포넌트를 렌더링하고 있다고 가정해 보겠습니다.
기존 React에서는 긴 Render가 끝날 때까지 입력 처리가 지연될 수 있었습니다.
기존 React
긴 Render
↓
사용자 입력 발생
↓
Render 계속 진행
↓
Render 완료
↓
입력 처리
반면 Fiber에서는 긴 Render를 처리하는 도중 더 우선순위가 높은 업데이트가 발생하면 현재 Render를 중단하고 중요한 작업을 먼저 처리할 수 있습니다.
Fiber
긴 Render
↓
사용자 입력 발생
↓
현재 Render 중단
↓
입력 먼저 처리
↓
Render 이어서 진행
즉, Fiber의 목적은 단순히 Render 자체를 더 빠르게 만드는 것이 아닙니다.
Render 작업을 상황에 맞게 조절하여 사용자 입력과 같은 중요한 업데이트에 더 빠르게 반응할 수 있도록 만드는 것입니다.
Fiber는 렌더링을 어떻게 나눌까?
그렇다면 Fiber는 하나의 Render를 어떻게 작은 작업으로 나눌 수 있을까요?
Fiber는 컴포넌트 트리를 Fiber Node라는 단위로 구성하고, 각 Fiber Node의 처리를 하나의 작업 단위(Unit of Work)로 다룰 수 있도록 합니다.
이를 단순화하면 다음과 같습니다.
Render 시작
↓
Fiber Node1 처리
↓
Fiber Node2 처리
↓
Fiber Node3 처리
↓
...
↓
Render 완료
↓
Commit
React는 이러한 작은 작업 단위를 하나씩 처리하기 때문에 Render 도중 필요에 따라 작업을 중단하거나 다시 시작하고, 우선순위가 높은 업데이트를 먼저 처리할 수 있습니다.
여기서 중요한 점은 이러한 방식으로 중단하고 다시 시작할 수 있는 것은 Render 단계라는 것입니다.
Commit 단계에서는 Render에서 계산한 변경 사항을 실제 DOM에 반영하기 때문에 중간에 작업을 멈추지 않습니다.
Render
→ 작은 작업 단위로 처리
→ 중단 가능
→ 다시 시작 가능
→ 우선순위 조절 가능
Commit
→ 중단하지 않음
→ 실제 DOM에 변경 사항 반영
따라서 Fiber는 Render를 작은 작업 단위로 나누고 React가 그 작업의 실행 순서를 직접 제어할 수 있도록 만든 구조라고 볼 수 있습니다.
Fiber가 가능하게 만든 기능들
Fiber가 도입되면서 React는 단순히 Render를 여러 번 나누어 실행하는 것 이상의 기능을 지원할 수 있게 되었습니다.
대표적으로 다음과 같은 기능들이 Fiber를 기반으로 동작합니다.
- 동시성 렌더링
- Transition (
startTransition) - Suspense
- 오프스크린 렌더링
이 기능들의 공통점은 모두 Render를 유연하게 제어할 수 있어야 한다는 것입니다.
예를 들어 사용자가 검색창에 입력하는 상황을 생각해 보겠습니다.
검색 결과를 계산하는 작업은 오래 걸릴 수 있지만, 사용자의 입력은 즉시 반응해야 합니다.
Fiber가 없다면 아래와 같이 긴 Render가 끝날 때까지 입력이 지연될 수 있습니다.
검색 결과 Render
↓
입력 이벤트 대기
↓
Render 완료
↓
입력 반영
반면 Fiber에서는 아래와 같이 더 중요한 작업을 먼저 처리할 수 있습니다.
검색 결과 Render
↓
사용자 입력 발생
↓
입력 먼저 처리
↓
검색 결과 Render 이어서 진행
덕분에 React는 화면이 복잡해져도 사용자에게 더 부드러운 경험을 제공할 수 있게 되었습니다.
Fiber는 어떻게 Render를 제어할 수 있을까?
지금까지는 Fiber를 Render를 작은 작업으로 나누고 유연하게 처리하기 위한 렌더링 아키텍처라고 설명했습니다.
조금 더 내부적으로 들어가면 Fiber는 React 트리를 구성하는 하나의 노드이자, React가 처리해야 할 하나의 작업 단위(Unit of Work) 입니다.
예를 들어 다음과 같은 컴포넌트가 있다고 해보겠습니다.
const App = () => {
return (
<main>
<Header />
<Content />
</main>
);
};
React는 이 구조를 단순한 중첩 객체로만 관리하지 않습니다.
각 컴포넌트와 DOM 요소에 대응하는 Fiber Node를 만들고, 노드들을 서로 연결하여 Fiber Tree를 구성합니다.
App
│ child
▼
main
│ child
▼
Header ── sibling ──▶ Content
위의 App, main, Header, Content는 각각 하나의 Fiber Node를 나타냅니다.
여기서 각 Fiber Node는 대표적으로 다음과 같은 정보를 가집니다.
Fiber Node
├── type: 어떤 컴포넌트 또는 DOM 요소인지
├── pendingProps: 새롭게 전달받은 props
├── memoizedProps: 이전 Render에서 사용한 props
├── memoizedState: 현재 Fiber가 유지하는 상태 정보
├── child: 첫 번째 자식 Fiber
├── sibling: 다음 형제 Fiber
├── return: 부모 Fiber
├── flags: Commit 단계에서 처리할 작업 정보
└── alternate: Current와 WorkInProgress의 대응 Fiber를 연결
Fiber Node에는 화면에 무엇을 보여줄지 계산하는 데 필요한 정보뿐 아니라, 다음에 어떤 노드를 처리해야 하는지와 Commit 단계에서 어떤 변경을 적용해야 하는지도 함께 저장됩니다.
Fiber Node에는 화면을 계산하는 데 필요한 props와 state 정보, Fiber Tree를 탐색하기 위한 child, sibling, return, 그리고 Commit 단계에서 처리할 변경 사항 등이 저장됩니다.
child, sibling, return으로 트리를 연결한다
일반적인 트리는 하나의 부모가 자식 배열을 가지는 형태로 표현할 수 있습니다.
{
children: [child1, child2, child3],
}
Fiber Tree는 조금 다릅니다.
각 Fiber Node는 주로 다음 세 방향의 참조를 이용해 서로 연결됩니다.
child
→ 첫 번째 자식으로 이동
sibling
→ 다음 형제로 이동
return
→ 부모로 돌아감
예를 들어 다음과 같은 JSX가 있다고 해보겠습니다.
const App = () => {
return (
<main>
<Header />
<Content />
<Footer />
</main>
);
};
Fiber의 연결 구조를 단순하게 표현하면 다음과 같습니다.
App
│ child
▼
main
│ child
▼
Header ── sibling ──▶ Content ── sibling ──▶ Footer
│ │ │
└────── return ─────────┼────── return ─────────┘
▼
main
main의 child는 첫 번째 자식인 Header를 가리킵니다.
두 번째 자식부터는 sibling으로 연결됩니다. Header의 sibling은 Content를, Content의 sibling은 Footer를 가리킵니다.
반대로 각 자식의 return은 부모 Fiber인 main을 가리킵니다.
이 구조 덕분에 React는 JavaScript의 재귀 호출 스택에 전체 탐색 과정을 맡기지 않고, 현재 Fiber Node를 기준으로 다음에 처리할 노드를 직접 결정할 수 있습니다.
현재 Fiber 처리
↓
child가 있으면 자식으로 이동
↓
자식이 없으면 sibling으로 이동
↓
sibling도 없으면 return을 따라 부모로 이동
React는 이러한 연결을 따라 Fiber Tree를 순회하면서 각 노드를 하나의 작업 단위로 처리합니다.
Fiber Node가 왜 작업 단위일까?
Fiber가 도입되기 전 React 15까지는 기존 재조정 방식(Stack Reconciler) 을 사용했습니다.
여기서 Reconciler는 컴포넌트 트리를 확인하면서 어떤 부분을 업데이트해야 할지 결정하는 React의 내부 시스템입니다.
기존 재조정 방식은 컴포넌트 트리를 재귀 호출 방식으로 탐색했습니다.
App()
↓
Main()
↓
Header()
↓
...
이 방식에서는 컴포넌트를 처리하면서 자식 컴포넌트를 계속 호출하기 때문에 JavaScript의 호출 스택(Call Stack)에 작업이 쌓이게 됩니다.
App() 실행
↓
Main() 호출
↓
Header() 호출
↓
...
↓
작업이 끝나면서 호출 스택에서 하나씩 제거
한 번 이러한 재귀 호출이 시작되면 JavaScript 호출 스택이 전체 실행 흐름을 관리하기 때문에, React가 중간에 작업을 임의로 멈추고 나중에 다시 이어서 처리하기 어려웠습니다.
Fiber에서는 이러한 작업을 Fiber Node라는 작은 작업 단위로 나누고 React가 직접 관리할 수 있도록 구조를 변경했습니다.
현재 Fiber 처리
↓
다음 Fiber 위치 기억
↓
필요하면 작업 중단
↓
브라우저에 제어권 반환
↓
작업 재개
↓
다음 Fiber부터 처리
React가 모든 작업을 JavaScript 호출 스택에 의존하지 않고, 현재 어디까지 처리했는지 직접 관리할 수 있게 된 것입니다.
따라서 Render 도중 더 높은 우선순위의 작업이 들어오면 현재 작업을 잠시 멈추거나, 진행하던 결과를 버리고 새로운 Render를 시작할 수 있습니다.
Fiber를 작업 단위(Unit of Work) 라고 부르는 이유도 각 Fiber Node의 처리를 기준으로 Render 작업을 나누고 관리할 수 있기 때문입니다.
Current Tree와 WorkInProgress Tree
Fiber를 이해할 때 중요한 구조가 하나 더 있습니다.
React는 현재 화면에 반영된 Fiber Tree를 직접 수정하면서 다음 화면을 계산하지 않습니다.
대신 현재 화면을 나타내는 트리와 다음 화면을 계산하기 위한 트리를 구분해서 관리합니다.
Current Tree
→ 현재 화면에 반영된 Fiber Tree
WorkInProgress Tree
→ 다음 화면을 계산하고 있는 Fiber Tree
예를 들어 현재 화면에 다음과 같이 0이 표시되어 있다고 해보겠습니다.
<div>0</div>
state가 변경되어 1을 표시해야 한다면 React는 Current Tree를 직접 변경하는 대신, 기존 Fiber를 바탕으로 WorkInProgress Tree를 구성하면서 다음 화면을 계산합니다.
현재 화면 (Current Tree)
<div>0</div>
↓ state 변경
다음 화면 계산 (WorkInProgress Tree)
<div>1</div>
이때 Current Tree와 WorkInProgress Tree에서 서로 대응하는 Fiber Node는 alternate를 통해 연결됩니다.
Current Tree WorkInProgress Tree
App Fiber App Fiber
│ │
▼ ▼
div Fiber ◀─── alternate ───▶ div Fiber
React는 이 연결을 통해 이전 Fiber의 정보를 참고하면서 새로운 Fiber를 구성하고, 어떤 부분을 변경해야 하는지 계산할 수 있습니다.
Render 단계
Render 단계에서는 Current Tree의 기존 정보를 바탕으로 WorkInProgress Tree를 구성하면서 필요한 변경 사항을 계산합니다.
Current Tree
(현재 화면)
↓
Render
↓
WorkInProgress Tree 구성
↓
필요한 변경 사항 기록
이 과정에서는 아직 실제 DOM을 변경하지 않습니다.
즉, 다음 화면을 계산하는 동안에도 사용자가 보고 있는 화면은 Current Tree에 해당하는 상태로 유지됩니다.
Render가 완료되면
WorkInProgress Tree의 계산이 정상적으로 완료되면 Commit 단계가 시작됩니다.
Commit 단계에서는 Render에서 계산한 변경 사항을 실제 DOM에 반영하고, 완료된 WorkInProgress Tree가 새로운 Current Tree가 됩니다.
현재 화면 (Current Tree)
↓
업데이트 발생
↓
WorkInProgress Tree에서 다음 화면 계산
↓
Render 완료
↓
Commit
↓
실제 DOM에 변경 사항 반영
↓
WorkInProgress Tree가 새로운 Current Tree가 됨
Render가 중단되면
반대로 Render 도중 더 높은 우선순위의 업데이트가 발생하거나 진행 중인 Render가 더 이상 필요하지 않게 되면, 아직 Commit되지 않은 작업 결과는 화면에 반영되지 않습니다.
WorkInProgress Tree 계산 중
↓
Render 중단
↓
계산 중인 결과는 Commit되지 않음
↓
Current Tree는 그대로 유지
↓
기존 화면 유지
필요한 경우 진행 중이던 WorkInProgress 작업을 버리고 새로운 Render를 시작할 수도 있습니다.
이처럼 현재 화면과 계산 중인 다음 화면을 분리하기 때문에 React는 Render를 중단하거나 다시 시작하더라도 계산 중인 불완전한 결과를 사용자에게 노출하지 않을 수 있습니다.
이러한 구조는 흔히 더블 버퍼링(Double Buffering) 에 비유됩니다.
Current Tree
→ 현재 화면을 안정적으로 유지
WorkInProgress Tree
→ 다음 화면을 별도로 계산
계산 완료
↓
Commit
↓
WorkInProgress → 새로운 Current
즉, 현재 화면은 그대로 유지하면서 다음 화면을 별도로 준비하고, 준비가 완료되었을 때 화면에 반영하는 방식이라고 이해할 수 있습니다.
Fiber와 Virtual DOM은 같은 것일까?
Fiber와 Virtual DOM을 같은 개념으로 생각하기 쉽지만 역할은 다릅니다.
React Element는 컴포넌트가 반환한 UI의 모습을 설명하는 비교적 가벼운 객체입니다.
const element = <button>저장</button>;
이를 단순하게 표현하면 다음과 비슷합니다.
{
type: 'button',
props: {
children: '저장',
},
}
반면 Fiber Node는 React가 이 UI를 실제로 처리하고 업데이트하기 위해 사용하는 내부 작업 객체입니다.
- React Element: UI가 어떤 모습이어야 하는지 설명
- Fiber Node: 해당 UI의 상태, 관계, 작업 진행 상황 관리
React Element는 Render마다 새로 만들어질 수 있지만, Fiber Node는 이전 상태와 현재 작업 상태를 연결하면서 업데이트 과정을 관리합니다.
따라서 Fiber는 단순한 Virtual DOM 노드라기보다, React Element를 실제 화면 업데이트 작업으로 연결하는 실행 가능한 내부 데이터 구조에 가깝습니다.
React가 작업의 우선순위를 관리하는 이유
React의 목표는 가능한 한 빠르게 화면을 그리는 것이 아닙니다.
더 중요한 목표는 사용자가 항상 부드럽게 상호작용할 수 있는 UI를 만드는 것입니다.
예를 들어 리스트를 0.1초 만에 렌더링하는 것보다
- 버튼이 즉시 눌리고
- 입력이 끊기지 않으며
- 스크롤이 부드럽게 동작하는 것
이 사용자 경험에는 훨씬 중요합니다.
그래서 React는
모든 작업을 최대한 빨리 끝내는 것보다, 중요한 작업을 먼저 처리하는 방향을 선택했습니다.
Fiber는 이러한 철학을 가능하게 만든 핵심 아키텍처입니다.
정리
React 15까지 사용하던 기존 렌더링 구조에서는 Render가 시작되면 작업이 끝날 때까지 중단하기 어려웠습니다.
따라서 Render가 오래 걸리면 사용자 입력이나 화면 업데이트가 지연될 수 있었습니다.
React는 이러한 렌더링 작업을 더 유연하게 관리하기 위해 React 16에서 Fiber 아키텍처를 도입했습니다.
핵심 내용을 정리하면 다음과 같습니다.
- Fiber는 Render를 Fiber Node라는 작은 작업 단위(Unit of Work) 로 나누어 관리합니다.
- 각 Fiber Node는
child,sibling,return으로 연결되어 Fiber Tree를 구성하고, React는 이 연결을 따라 작업 순서를 직접 관리합니다. - Render는 필요에 따라 작업을 중단하거나 다시 시작하고, 우선순위가 높은 작업을 먼저 처리할 수 있습니다.
- Commit은 실제 DOM에 변경 사항을 반영하는 단계이므로 중간에 중단하지 않습니다.
- Fiber는 단순히 Render 속도를 높이기 위한 기술이 아닌, 렌더링 작업을 더 유연하게 제어하기 위한 아키텍처입니다.
- 동시성 렌더링, Transition, Suspense와 같은 기능도 이러한 Fiber 아키텍처를 기반으로 구현됩니다.
즉, Fiber의 핵심은 Render를 무조건 더 빠르게 만드는 것이 아니라, Render 작업을 React가 직접 나누고 관리할 수 있게 만든 것입니다.