React 동시성 렌더링: Fiber가 가능하게 한 혁신

들어가며

대규모 리스트를 렌더링하다가 입력 필드가 먹통이 되던 경험, 애니메이션이 뚝뚝 끊기던 순간들이 있지 않으신가요? 이 문제의 근본 원인은 JavaScript의 단일 스레드 특성React의 동기적 렌더링이었습니다.

문제의 본질 - 모든 작업이 평등했던 시대

기존 Stack Reconciler의 한계

React의 초기 아키텍처는 콜 스택을 기반으로 작동했습니다. 이를 비유하자면, 엘리베이터를 타고 건물의 모든 층을 순서대로 방문해야 하는 것과 같았죠. 중간에 멈출 수도, 긴급한 용무로 다른 층으로 먼저 갈 수도 없었습니다.

Stack Reconciler와 Fiber 아키텍처를 나란히 놓고 비교한 그림. 왼쪽은 render(App)부터 render(Footer)까지 세로로 쌓인 중단 불가능한 재귀 호출이고, 오른쪽은 App·Header·Content·Footer가 화살표로 이어진 중단 가능한 연결 리스트다

왜 문제였나?

브라우저는 60fps를 유지하려면 한 프레임당 16.67ms 안에 모든 작업을 마쳐야 합니다:

브라우저 한 프레임의 16.67ms 예산을 가로 막대 하나로 나눈 그림. JavaScript, 스타일 계산, 레이아웃, 페인트가 차례로 자리를 차지하고 아래에 한 프레임은 16.67ms이며 60fps를 유지한다고 적혀 있다

그런데 React가 50ms짜리 업데이트를 실행하면? 3개의 프레임이 날아가고, 사용자는 “버벅거림”을 체감하게 됩니다.

실제 시나리오로 이해하기

검색 자동완성을 생각해보세요:

기존 React의 검색 처리 문제를 하나의 타임라인에 찍은 그림. 0ms에 R 입력, 100ms에 Re 입력, 300ms에 React 입력이 이어지는데 정작 R의 결과가 완료되는 시점은 500ms다

  • 사용자가 “R”을 입력 → 1000개 결과 렌더링 시작
  • 사용자가 “Re”를 입력 → 하지만 이전 렌더링이 끝날 때까지 대기
  • 사용자가 “React”까지 입력 → 여전히 “R”의 결과를 그리는 중…

결과: 타이핑은 멈춰있고, 이미 필요 없는 “R”의 결과를 열심히 그리고 있는 React

Fiber - 작업을 “중단 가능”하게 만들다

핵심 아이디어: 재귀에서 반복으로

Fiber의 혁신은 재귀적 처리를 반복적 처리로 바꾼 것입니다.

기존 방식 (Stack): 함수 호출 스택

text
render(App)
  └─ render(Header)
      └─ render(Logo)
  └─ render(Content)  // Logo가 끝날 때까지 대기

Fiber 방식: 연결 리스트

text
App → Header → Logo
 ↓                ↓
Content ← ← ← ← ←

이제 Logo를 그리다가도 멈추고, 더 급한 일을 처리한 후 돌아올 수 있습니다!

Fiber 노드의 구조: 작업의 최소 단위

각 Fiber 노드는 하나의 “작업 패킷”입니다:

Fiber 노드 구조를 어두운 카드 한 장에 항목별로 나열한 그림. type은 button, props는 onClick 핸들러, effectTag는 UPDATE, expirationTime은 500ms 뒤이고 return·child·sibling이 각각 부모·첫째 자식·다음 형제 Fiber를 가리킨다

javascript
{
  // 무엇을?
  type: 'button',
  props: { onClick: handleClick },

  // 언제까지?
  expirationTime: 성능.now() + 500,  // 우선순위

  // 어떤 작업을?
  effectTag: 'UPDATE',  // 또는 'PLACEMENT', 'DELETION'

  // 다음은 어디로?
  return: 부모Fiber,
  child: 첫째자식Fiber,
  sibling: 다음형제Fiber
}

우선순위 시스템 - 모든 업데이트가 평등하지 않다

우선순위의 계층 구조

React 우선순위 시스템을 위에서 아래로 갈수록 길어지는 다섯 개의 막대로 그린 그림. 즉시(타이핑·클릭), 사용자 차단(호버·드래그), 일반(데이터 페칭), 낮음(애널리틱스), 유휴(오프스크린) 순으로 우선순위가 낮아진다

React는 이제 작업을 분류합니다:

  1. 즉시 (Immediate): 사용자 입력 - 타이핑, 클릭
  2. 사용자 차단 (User-blocking): 호버 효과, 드래그
  3. 일반 (Normal): 데이터 페칭 결과
  4. 낮음 (Low): 애널리틱스 로깅
  5. 유휴 (Idle): 화면 밖 콘텐츠 프리렌더링

실제 작동 방식

타임라인으로 보는 동시성:

text
시간 →
[사용자 타이핑] [무거운 리스트 렌더링 시작]
      ↓                    ↓
[입력 즉시 반영] [리스트 렌더링 일시정지]

                  [다음 프레임에서 계속]

Time Slicing - 작업을 잘게 나누기

Time Slicing의 작업 분할을 두 줄로 비교한 그림. 위쪽 일반 렌더링은 React 작업 50ms 뒤에 프레임 드롭이 오고, 아래쪽 동시성 렌더링은 React 5ms와 브라우저 차례가 번갈아 놓인다

브라우저와의 협업

Fiber는 requestIdleCallback의 개념을 차용했습니다:

text
한 프레임 (16ms)의 구성:
┌─────┬──────┬──────┬──────┬────┐
│입력 │React │스타일│레이아웃│페인트│
│처리 │ 5ms  │계산  │      │    │
└─────┴──────┴──────┴──────┴────┘

    "5ms만 쓰고 양보"

중단과 재개의 메커니즘

  1. 작업 시작: 우선순위 큐에서 작업 선택
  2. 시간 체크: 5ms마다 “더 급한 일 있나?” 확인
  3. 양보 결정: 급한 일 있으면 현재 진행상황 저장
  4. 재개: 나중에 저장된 지점부터 계속

실무에서의 동시성 기능들

useTransition - 상태 업데이트의 우선순위 조절

언제 쓰나요?

  • 무거운 계산이 UI 반응성을 해치는 경우
  • 사용자 입력과 결과 표시를 분리하고 싶을 때

작동 원리:

text
startTransition(() => {
  // 이 안의 setState는 "낮은 우선순위"로 표시됨
  setExpensiveState(newValue);
});

useDeferredValue - 값의 지연된 버전

언제 쓰나요?

  • props로 받은 값에 대한 무거운 계산
  • 디바운싱과 유사하지만 더 스마트한 처리

작동 원리:

입력값이 빠르게 변할 때, UI는 최신값을 표시하지만 무거운 계산은 이전 값으로 수행될 수 있습니다.

javascript
// useDeferredValue: 값을 지연시킬 때
const SearchResults = ({ query }) => {
  const deferredQuery = useDeferredValue(query);
  // deferredQuery를 사용한 무거운 계산
};

// useTransition: 상태 업데이트를 지연시킬 때
const SearchInput = () => {
  const [isPending, startTransition] = useTransition();

  const handleSearch = (value) => {
    startTransition(() => {
      setSearchResults(computeResults(value));
    });
  };
};

Suspense - 비동기의 선언적 처리

혁신적인 점:

  • 로딩 상태를 컴포넌트 경계에서 관리
  • 여러 비동기 작업의 조율이 자동화

Suspense가 가능한 이유:

Fiber는 컴포넌트 렌더링을 “일시정지”하고 나중에 “재개”할 수 있기 때문!

동시성이 가져온 패러다임 변화

동시성 렌더링 작동 방식을 세 트랙으로 그린 그림. 사용자 입력 트랙의 타이핑에 맞춰 무거운 작업 트랙이 일시정지와 재개를 반복하고, 화면 업데이트 트랙은 즉시 반영을 거쳐 마지막에 최종 업데이트를 낸다

Before: 동기적 사고

“모든 업데이트를 즉시, 완전히 처리해야 한다”

After: 동시적 사고

“급한 것부터, 나머지는 시간 날 때”

실무에서의 영향

  1. UX 설계의 변화
    • 즉각적 피드백과 최종 결과를 분리
    • 로딩 상태의 계층적 관리
  2. 성능 최적화 전략
    • 메모이제이션 의존도 감소
    • 작업 우선순위로 최적화
  3. 아키텍처 패턴
    • Suspense 경계 설계
    • 점진적 향상 전략

실전 가이드라인

동시성 기능을 써야 할 때

필수적인 경우:

  • 검색 자동완성
  • 실시간 필터링
  • 대용량 데이터 시각화
  • 복잡한 폼 유효성 검사

과도한 사용 주의:

  • 단순한 상태 업데이트
  • 이미 빠른 작업
  • 소규모 리스트

성능 측정과 검증

javascript
// React DevTools Profiler로 확인할 점
1. Commit 단계가 여러 개로 나뉘는지
2. 사용자 인터랙션 중 프레임 드롭이 없는지
3. 우선순위가 의도대로 작동하는지

미래를 향해

React Server Components와의 시너지

동시성 렌더링은 RSC와 만나 더욱 강력해집니다:

  • 서버에서 스트리밍되는 컴포넌트
  • 클라이언트 렌더링과의 매끄러운 통합

사용자 경험 중심의 렌더링

Fiber와 동시성 렌더링은 단순한 성능 개선이 아닙니다. 사용자 경험을 최우선으로 하는 렌더링 패러다임의 전환입니다.

핵심 메시지:

  1. 모든 작업이 똑같이 중요하지 않다
  2. 중단 가능한 아키텍처가 유연성을 만든다
  3. 선언적 API로 복잡성을 숨긴다

이제 우리는 “빠른 앱”이 아닌 “반응적인 앱”을 만들 수 있습니다. 사용자가 무언가를 입력할 때, 클릭할 때, 그 순간의 반응이 전체 성능보다 중요하다는 것을 React가 이해하게 된 것입니다.

여러분의 앱에서 사용자가 “버벅거림”을 느끼는 순간이 있다면, 이제 그 해답을 알고 계실 겁니다. 동시성 렌더링으로 사용자에게 마법 같은 경험을 선사하세요! 🚀