React fiber 아키텍처

fiber 이전의 리액트 내부 메커니즘

리액트는 기존에 Stack Reconciler라는 방식의 메커니즘을 사용하였습니다. 이를 간단하게 말해보면 재귀를 통해 변경사항을 파악하고 이를 한번에 업데이트하는 방식입니다. 이러한 방식은 매우 직관적이지만 몇가지 문제가 있었습니다.

대규모 업데이트 시 발생하는 frame drop 현상

리액트는 어쨋든 자바스크립트 라이브러리이고, 이말은 즉 싱글쓰레드라는 뜻입니다. 이를 인지하고 위 방식을 다시 생각해보면 모든 변경사항을 한번에 업데이트하는 방식에는 분명 문제가 발생할것입니다. 왜냐하면 업데이트, 즉 UI 변경을 반영한다는 것은 콜 스택을 점유한다는 의미입니다. 근데 점유된 작업이 매우 큰 분량의 작업이라면 다른 작업이 그동안 차단된다는 뜻이기도 합니다.

이를 수치적으로 말하면, 브라우저는 기본적으로 하나의 업데이트를 60fps(16ms)에 처리해야 사용자 입장에서 끊김없는 부드러운 업데이트가 이루어진다고 알려져있습니다.

참고: dev.to

하지만 만약에 작업이 매우 커서 이 범위를 넘어간다면 사용자에게는 일종의 끊김 현상 ( janky )이 발생합니다.

또한 이후 설명할 동시성모드의 핵심인 우선순위 업데이트가 이 메커니즘에서는 존재하지 않기 때문에 상대적으로 중요한 업데이트가 우선 업데이트 되지 못하였습니다. 예를들어 유저의 입력같이 즉각적인 피드백이 나와야하는 중요한 업데이트가 차단되는 현상이 존재하였습니다.

사용자 입력 시작
|
v
대규모 DOM 업데이트 시작
|
v
대구모 DOM 업데이트 완료 전까지 사용자 입력이 차단됨
과장된 예시입니다,,,

fiber 도입

리액트 팀 또한 이러한 문제의 심각성을 인지하고 있었고, 이 문제를 해결하고 더 나은 리액트를 구현하기 위해서 v16부터 새로운 reconciler을 선보입니다. 이때 리액트 팀에서 잡은 핵심 목표는 아래와 같습니다

참고: React 맴버 깃허브

fiber의 핵심은 작업 단위를 여러개로 찢는 방식입니다. 이렇게 여러개로 찢은 후 각 작업 사이에 브라우저 제어를 넘겨주는 방식으로 위의 문제를 해결하려 접근하였습니다.

조금 더 자세히 서술해보면 fiber는 전체 트리를 한 번에 끝까지 내려가는 대신 작은 단위로 내려갔다가 다시 올라오는 과정을 반복하며 작업을 진행합니다. 저는 이 문장을 처음 읽었을때 두가지 의문이 들었습니다.

  1. 올라감, 내려감을 반복하면 오히려 한번에 재귀를 도는것보다 더 느린거 아닌지?
  2. 그렇다면 16ms를 넘어가는게 아닌지?

이 부분에 대해서 더 자세히 이해하려면 올라감, 내려감이 뭔지 정확히 알면 이해가 됩니다.

이를 실생활에 비유해보자면 마치 집안 청소를 할 때 전체 집을 한 번에 청소하는 대신, 방 하나씩 청소하면서 잠시 멈추고 집에 손님이 온 건 아닌지 확인하거나, 긴급한 전화가 오면 그 일을 먼저 처리하는 것과 비슷합니다.

정리하면 하나의 단위를 단순히 내려갔다가 올라가면서 읽는게 아니라, 내려가면서 읽고 그 작업이 끝나면 다음 작업을 실행해도 되는지 고려합니다 (남은 시간이 충분한지, 다른 우선순위 높은 작업이 있는지등,,) 이렇게 되면 하나의 작업을 읽은 다음 바로 다음 작업을 실행하면서 메인 쓰레드를 계속 차지하는 방식이 아니라, 중간에 브라우저에게 중간 제어권을 넘겨주기 때문에, 브라우저가 인터랙션을 처리하거나 우선순위가 높은 작업을 먼저 처리할 수 있게 해주므로 실제 사용자가 느끼는 체감 성능이 좋아집니다.

그래서 1번의 의문에 대해서는 아래처럼 답변이 가능할거같습니다.

  1. 전체적인 시간은 늘어날수있지만 (아주 약간..?) 사용자 체감 시간(반응성)은 더 개선됩니다.
  2. 16ms안에 작업을 완료 못할 수 있습니다. 다만 앞서 언급했듯이 하나의 작업 단위를 매우 잘게 쪼개놓아서 16ms내에 전체 작업이 완료되지 않더라도 끊김 현상이 아니라 부드러운 전환이 가능해집니다.

fiber 자세히 살펴보기

위 내용을 정리하면 fiber는 두가지 핵심 개념에 의해 동작합니다

  1. 하나의 작업 단위를 잘게 짜름
  2. 우선순위가 존재하여, 긴급한(중요한) 단위부터 처리됨

그렇다면 이 파트에서는 fiber가 어떻게 구성되어있고 어떻게 동작하는지 자세히 살펴보겠습니다.

fiber 노드(하나의 작업 단위)는 js 객체로 구현되며, 특정 컴포넌트(또는 DOM 요소)에 대한 정보를 담고 있습니다. fiber 노드들은 서로 연결 리스트 형태의 트리를 구성하는데, child, sibling, return과 같은 포인터를 통해 부모-자식 및 형제 관계를 표현합니다. 이러한 구조 덕분에 React는 재귀 호출 대신 반복이나 순회 알고리즘으로 트리를 탐색할 수 있고, 필요한 시점에 작업을 중단하거나 재개할 수 있습니다.

fiber 노드의 구성은 아래와 같습니다.

아래는 실제 리액트가 구현한 fiber 코드중 일부입니다.

function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;
  this.refCleanup = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;
}

아래는 fiber의 메커니즘을 매우 간단히 시각화한 것입니다.

Fiber 시각화

ImmediateApp
UserBlockingLayout
ImmediateNavbar
NormalSidebar
LowFooter
NormalContent
UserBlockingWidget
NormalReports

정리하면 React fiber 아키텍처는 리액트가 사용자 경험을 향상시키고 성능을 최적화하기 위해 내놓은 아주 중요한 개선사항입니다. 특히 fiber는 작업을 잘게 나누고 우선순위를 부여하여, 긴급한 작업부터 처리할 수 있게 합니다. 또한 긴 작업을 처리할 때도 프레임 드롭 현상을 최소화하여 더욱 부드러운 UI를 제공합니다.

React fiber | 김영훈 블로그