JavaScript/React

React Fiber... 어렵지만 친해지고 싶은 고마운 친구

남희정 2023. 12. 18. 20:10

리액트 스터디 중 "가상DOM과 리액트 파이버" 주제가 나왔다. 발표를 맡게된 리액트 렌더링 부분을 준비하고, 발표 당일 스터디원들과 얘기하며 조금씩 윤곽이 잡히기 시작했다. 하지만 여전히 어려운 건 매한가지... 그러다가 좋은 글을 발견했는데 포스팅 일자가 리액트 15,16 기준이라 렌더가 동기식으로 진행된다고 되어있어서 🤔 제대로 스터디 내용을 복기할 겸, 그리고 파이버와 좀 친해질 겸 포스팅 주제로 선정하게 되었다. 
조금 의문이었던 것은 재조정 과정은 똑같고, 여전히 유효할 것 같은데 리뉴얼된 React 사이트에는 파이버와 관련된 내용이 전부 생략되어있다. 왜일까????? 그래서 더 어렵게 느껴졌던 것 같다. 더이상 관련이 없는 내용인지? 레거시한 부분인 건지에 대한 고민.
 


 

리액트 파이버 아키텍처 React Fiber Architecture

가상 DOM과 렌더링 과정 최적화를 가능하게 해주는 가상 DOM을 위한 아키텍처

React Fiber is an ongoing reimplementation of React's core algorithm.
 It is the culmination of over two years of research by the React team.
The goal of React Fiber is to increase its suitability for areas like animation, layout, and gestures. Its headline feature is incremental rendering: the ability to split rendering work into chunks and spread it out over multiple frames.
Other key features include the ability to pause, abort, or reuse work as new updates come in; the ability to assign priority to different types of updates; and new concurrency primitives.

React Fiber는 현재 진행 중인 React core 알고리즘 재구성이다.
React 팀의 2년간의 연구 결과이다.
Fiber의 목적은 animation, layout, gesture (애니메이션, 레이아웃, 제스처)와 같은 영역들에 있어서 React의 반응성 문제를 해결하기 위함이다. 주요 주제는 점증적 렌더링 (incremental rendering)으로, 렌더링 작업을 chunk 단위로 나눈뒤 여러 프레임에 수행하는 것을 의미한다.
이와 더불어 새로운 업데이트가 들어올 때 기존의 작업을 멈추거나, 정지하거나, 재사용하는 기능들을 포함한다. 이외에도 다른 종류에 업데이트에 우선순위를 부여하거나, 새로운 동시성 모드를 위한 초기 작업들이 포함된다.

 
React는 JSX를 지원하며 Virtual DOM을 사용하여 변경된 부분만 DOM 업데이트하는 UI 라이브러리이다. 
리액트 파이버는 React 16버전부터 채택된 새로운 코어 아키텍처이다. 이것은 기존의 Call Stack 기반 알고리즘(재귀호출 방식)을 완전히 새롭게 작성한 것이다. 당시, 스택에 렌더링이 필요한 작업들이 쌓이면 이 스택이 빌 때까지 동기적으로 작업이 이루어졌다. 이 동기 작업은 중단될 수 없고, 결국 리액트의 비효율성으로 이어졌다.
 

재조정 Reconciliation

Virtual DOM은 특정 기술이라기보다 패턴에 가깝다. React의 세계에서 Virtual DOM이라는 용어는 보통 UI를 나타내는 객체라 React Elements와 연관된다. 그러나 React는 컴포넌트 트리에 대한 추가 정보를 포함하기 위해 Fibers라는 내부 객체를 사용한다. 

 
파이버파이버 재조정자(Fiber reconciler)가 관리하는데, 이는 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하며, 만약 이 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 한다.
파이버를 채택하고서 제일 큰 변화는 비동기로 아래 사항을 지원한다는 것! 
 

  • 작업을 작은 단위(chunk)로 분할하고 쪼갠 다음, 우선순위를 매긴다.
  • 작업을 일시 중지하고, 나중에 다시 시작할 수 있다.
  • 이전 작업을 다시 재사용하거나 필요하지 않은 경우 폐기 가능하다.

 

1. Triggering a render (delivering the guest’s order to the kitchen)

Step 1: Trigger a render 

Triggering a render (delivering the guest’s order to the kitchen)
리액트 공식에서 표현하는 렌더링의 1단계는 "손님의 주문을 주방으로 전달하다."이다.
 
렌더링이 일어나는 이유

  1. 컴포넌트의 최초 렌더링
  2. 컴포넌트의 상태가 업데이트 되는 경우

최초 렌더링의 경우 target DOM 노드로 createRoot를 호출하고 render 메서드를 호출한다.

React Element 

import { createRoot } from 'react-dom/client';

const domNode = document.getElementById('root');
const root = createRoot(domNode);

root.render(<App />);
import { createElement } from 'react';

function Greeting({ name }) {
  return createElement(
    'h1',
    { className: 'greeting' },
    'Hello ',
    createElement('i', null, name),
    '. Welcome!'
  );
}

 
리액트가 지원하는 JSX는 JavaScript 엔진이 이해할 수 있는 순수 JavaScript 코드로 변환될 것이다.
native DOM에 React Element를 렌더링하기 위해서는 루트root를 먼저 생성해야 한다.
createRoot()는 몇 가지 플래그를 설정한 후 고유한 FiberRootNode를 생성한다.
 

export function createHostRootFiber(
  tag: RootTag,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
): Fiber {
  let mode;
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode;
    if (isStrictMode === true || createRootStrictEffectsByDefault) {
      mode |= StrictLegacyMode | StrictEffectsMode;
    }
    if (
      // We only use this flag for our repo tests to check both behaviors.
      forceConcurrentByDefaultForTesting
    ) {
      mode |= ConcurrentUpdatesByDefaultMode;
    } else if (
      // Only for internal experiments.
      allowConcurrentByDefault &&
      concurrentUpdatesByDefaultOverride
    ) {
      mode |= ConcurrentUpdatesByDefaultMode;
    }
  } else {
    mode = NoMode;
  }

  if (enableProfilerTimer && isDevToolsPresent) {
    // Always collect profile timings when DevTools are present.
    // This enables DevTools to start capturing timing at any point–
    // Without some nodes in the tree having empty base times.
    mode |= ProfileMode;
  }

  return createFiber(HostRoot, null, null, mode);
}

 

브라우저 DOM Element와 달리 React Element는 일반 객체이며(plain object) 쉽게 생성할 수 있다. React DOM은 React Element와 일치하도록 DOM을 업데이트한다.

 
Element는 컴포넌트 인스턴스나 DOM 노드 및 그에 대해 원하는 속성을 설명하는 일반 객체를 말한다. 
 

  •  JSX로 선언된 표현식은 React Element라는 객체로 치환된다.
  • React Element는 일반 객체이다.
  • React Element는 native DOM에 렌더링하기 위해선 루트root를 거쳐야 한다.

 

상태 업데이트시 Re-rendering

컴포넌트의 최초 렌더링 이후 set function으로 상태를 업데이트 하여 리렌더링을 트리거할 수 있다. 컴포넌트의 상태를 업데이트하면 자동으로 렌더링이 대기열에 추가된다.
 

Re-rendering

 

렌더링 프로세스가 시작되면 리액트는 컴포넌트의 루트root부터 아래쪽으로 내려가면서 업데이트가 필요하다고 지정되어 있는 모든 컴포넌트를 찾는다. 클래스형 컴포넌트의 경우에 내부의 render() 함수, 함수형 컴포넌트의 경우엔 FunctionComponent() 그 자체를 호출하고 결과물을 저장한다.
렌더링 결과물은 JSX 문법으로 구성되어 있고, JS로 컴파일되면서 React.createElement()를 호출하는 구문으로 변환된다. createElement는 브라우저의 UI 구조를 설명할 수 있는 일반적인 자바스크립트 객체를 반환한다.

 
 
코드 링크

const createFiber = function(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  return new FiberNode(tag, pendingProps, key, mode);
};

 
파이버와 리액트 엘리먼트의 한 가지 중요한 차이점은 엘리먼트는 렌더링 발생시 새롭게 생성되지만 파이버는 컴포넌트가 최초로 마운트되는 시점에 생성되어 이후엔 가급적이면 재사용된다.
 

2. Rendering the component (preparing the order in the kitchen)

Step 2: React renders your components (Render Phase)

리액트 공식에서 표현하는 렌더링의 2단계는 "주방에서 주문을 준비하다"이다.
 
render를 트리거한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악한다. Rendering은 React가 컴포넌트를 호출하는 것이다. 이 단계를 렌더 단계라고 하는데, 이 단계에서 리액트는 사용자에게 노출되지 않는 모든 비동기 작업을 수행한다. 앞서 언급한 파이버의 작업, 우선순위 지정, 중지 등의 작업이 일어난다.

  1. 초기 렌더링에서 React는 루트root 컴포넌트를 호출한다.
  2. 이후 렌더링 부터는 상태가 업데이트되어 렌더링을 트리거한 컴포넌트를 호출한다.

React Fiber 내부 코드 들여다보기 👀

function FiberNode(
  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.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

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

  this.alternate = null;
  
  // .. 이하 프로파일러, __DEV__ 코드 생략
  }

 

tag

파이버를 만드는 함수 이름인 createFiberFromElement를 보면 유추할 수 있는데, 파이버는 하나의 element에 하나가 생성되는 1:1 관계를 갖고 있다. 여기서 1:1로 매칭된 정보를 갖고 있는 게 tag다. 1:1로 연결되는 것은 컴포넌트, 혹은 DOM 노드 등 다른 어떤 것일 수 있다.
 

코드 링크

export type WorkTag =
  | 0
  | 1
  ...
  | 25
  | 26
  | 27;

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;
export const HostSingleton = 27;

 

  • stateNode : 파이버 자체에 대한 참조reference 정보를 갖고 있다. 리액트는 이 참조를 바탕으로 파이버와 관련된 상태에 접근한다.
  • child, sibling, return : 파이버 간 관계 개념을 나타내는 속성
Child, Sibling, Return

 
파이버의 자식은 항상 첫 번째 자식의 참조로 구성되므로, <ul /> 파이버의 자식은 첫 번째 <li/> 파이버가 된다. 그리고 나머지 두 개의 <li /> 파이버는 형제, Sibling으로 구성된다. 마지막으로 Return은 부모 파이버를 의미하며 모든 <li/> 파이버는 <ul/> 파이버를 return으로 갖게 되는 것이다!
 

  • index : 여러 형제들(sibling) 사이에서 자신의 위치가 몇 번째인지 숫자로 표현한다.
  • pendingProps: 아직 작업을 미러 처리하지 못한 props
  • memoizedProps: pendingProps를 기준으로 렌더링이 완료된 이후에 pendingProps를 memoizedProps로 저장해 관리한다.
  • updateQueue: 상태 업데이트, 콜백 함수, DOM 업데이트 등 필요한 작업을 담아두는 큐
  • memoizedState: 함수형 컴포넌트 훅 목록이 저장됨. useState 뿐 아니라 모든 훅 리스트가 저장됨.
  • alternate: 반대편 트리 파이버를 가리킨다.

 
슬슬 파이버의 내부가 이해가 되기 시작함🥹
 

리액트 파이버 트리 React Fiber Tree

파이버 트리는 리액트 내부에서 2 개가 존재한다. 하나는 현재의 모습을 담은 파이버 트리이고, 다른 하나는 작업 중인 상태를 나타내는 workInProgress 트리다. 리액트 파이버의 작업이 끝나면 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버린다. 이 기술을 이중 버퍼링 Double Buffering이라 한다. 이를 통해 화면이 갑자기 변경되는 것을 방지하고 매끄러운 화면 전환을 제공한다.
 

Current and workInProgress trees

 

  • 현재 렌더링을 위해 존재하는 트리 Current를 기준으로 모든 작업이 시작된다.
  • 업데이트 발생시 새로 받은 데이터로 workInProgress 트리를 빌드하기 시작한다. 다음 렌더링에 이 트리가 사용된다.
    UI에 최종적으로 렌더링되어 반영되면 current가 이 workInProgress로 변경된다.

    🌟 YK님께서 피드백해주셔서 내용을 수정한다.
  • 재조정 단계(render phase)는 두 가지 케이스로 나뉜다.
    1. Render Phase Update : 컴포넌트가 렌더링 되고 있는 상태에서 추가로 업데이트가 발생할 경우 => UI 영향이 있는 상태
    2. Idle Update : 상태가 변경되었지만 UI에 영향을 미치지 않는 경우 (유휴 상태라고도 함)

    VDOM은 하나의 노드를 current와 workInProgress로 관리한다고 위의 그림을 보면 알 수 있다. 하지만 문제는 current와 workInProgress는 고정이 아니라 Commit Phase를 지나면 교체되는 것이므로 Render 단계에서는 현재 작업 중인 currentlyRenderingFiber가 둘 중 어느 것에 해당하는지 알 수 없다. 따라서 fiber와 alternate를 모두 비교하고 두 케이스 중 어떤 케이스에 해당하는지 파악할 수 있는 것이다.

    즉, render phase 로직을 업데이트 발생시 새로 받은 데이터로 workIn 트리를 빌드한다 설명하였는데 두 케이스의 처리 및 최적화 방식이 조금씩 다르고 render phase에서 workIn트리를 바로 빌드하는 것처럼 표현하게 되어서 이 단계의 코드가 어떤 상황에 위치해 있는지 정확하게 짚고 넘어갈 필요가 있었다.

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) 

...

  const alternate = fiber.alternate;
  if (
  // currentlyRenderingFiber는 workInProgressFiber이다.
  // 현재 파이버가 workInProgressFiber라면 render Phase인 것.
  // 1. Render Phase 
    fiber === currentlyRenderingFiber ||
    // alternate = 다른 트리의 참조값
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    didScheduleRenderPhaseUpdate = true;
	// Render Phase의 업데이트
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime, // work가 진행될 때 work가 여기에 값을 할당.
      suspenseConfig: null,
      action, // setState를 호출할 때 들어온 parameter
      eagerReducer: null, // 불필요한 렌더링 최적화에 필요
      eagerState: null, // 불필요한 렌더링 최적화에 필요
      next: null, // 다음 노드의 주소값
    };
    ...
    
    if (renderPhaseUpdates === null) {
      // render phase에서 발생한 업데이트들을 임시로 저장하는 공간
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      // 아직 render phase update가 저장된 게 없다면
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      // 마지막 노드에 update를 저장
      lastRenderPhaseUpdate.next = update;
    }
  } else {
  	// 2. 아무런 업데이트가 없는 idle 상태
    const currentTime = requestCurrentTimeForUpdate(); 
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update: Update<S, A> = {
      expirationTime, // work가 진행될 때 work가 여기에 값을 할당한다.
      suspenseConfig,
      action, // setState를 호출할 때 들어온 parameter
      eagerReducer: null, // 불필요한 렌더링 최적화에 필요
      eagerState: null, // 불필요한 렌더링 최적화에 필요
      next: null, // 다음 노드의 주소값
    };
	...
    
    // update: 새로운 업데이트, update 객체는 circular linked list이다.
    const last = queue.last; // queue의 마지막 값을 갖고온다.
    if (last === null) { // 큐가 null이라면 최초로 업데이트
      // This is the first update. Create a circular list.
      // 최초 업데이트이므로 next에 연결 (새로운 업데이트가 업데이트 큐의 첫 번째 업데이트가 되도록 함)
      // 최초 업데이트일 경우에만 유효한 부분, 다음 업데이트시 이곳으로 오지 않음.
      update.next = update;
    } else {
      const first = last.next; // queue.last에 연결된 next. 큐의 head
      if (first !== null) {
        // Still circular. => update의 next를 head로 연결하여 circular 구조 유지.
        update.next = first;
      }
      last.next = update; // last의 next에 새로운 update 할당
    }
    queue.last = update; // 큐의 last에 새로운 Update 할당

    if ( // fiber가 update를 수행하고 있지 않는가? 처리할 작업이 없다면 scheduleWork()
         // idle 업데이트인지의 여부를 확인
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      // => 주석에서 컴포넌트가 처음 렌더링되거나 큐가 비어있는 경우 다음 상태를 계산하고 
	  // 현재 상태와 동일하다면 렌더링을 수행하지 않고 함수를 빠져나갈 수 있다고 설명. (성능 최적화)
	  // 즉, action의 결과값이 현재 상태 값과 같다면 함수 실행 중지
      // 마지막으로 렌더링된 리듀서를 가져옴. mountState()에서 lastRenderedReducer에 basicStateReducer 할당했음.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        ...
        try {
          // 마지막 상태값 가져오기
          const currentState: S = (queue.lastRenderedState: any);
          // action: setState에서 받은 인자값
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          // => update에 계산된 상태를 저장한다. 
		  // 렌더 단계에 변경되지 않음이 확인되면 추후에 호출하지 않고도 재사용할 수 있다는 내용
          
          update.eagerReducer = lastRenderedReducer; // 바뀌어야 할 state 계산
          update.eagerState = eagerState; // 바뀌어야 할 state 값
          if (is(eagerState, currentState)) { // 업데이트될 값이 같다면?
            return; // dispatch 종료
          }
          ...
        }
      }
    }
    ...
    scheduleWork(fiber, expirationTime); // 스케쥴러에 work를 업데이트
  }
}

 
- Summary - 

  • Render Phase Update 의 경우
    Work는 이미 진행 중이므로 Work를 스케줄링하거나 성능 최적화를 위한 처리가 필요없다. 단지 Render phase update가 더 이상 발생하지 않을 때까지 계속해서 컴포넌트를 재호출하여 action을 소비한다.
    ➡️ action을 소비하기 위해선 update를 담아둘 임시 저장소가 필요하다. 그래야 다음 컴포넌트 호출 때 꺼내어 소비할 수 있다. 
    🌟 didScheduleRenderPhaseUpdate 플래그를 통해 Render phase update 발생을 판단한다.

  • Idle Update 의 경우
    1. 사용자의 업데이트 정보를 담은 update 객체를 만든다.
    2. update를 queue에 저장한다.
    3. 불필요한 렌더링이 발생하지 않도록 최적화를 한다.
    4. 업데이트를 적용하기 위해 Work를 스케줄링한다.

    🌟 expirationTime : 업데이트가 발생하여 Work가 스케줄링 될 경우 fiber에 발생 시간을 기록한다. 

위 코드 & 질문 출처
Q. 상태 변화가 여러 번 호출 될 때마다 컴포넌트가 리렌더링 되는가?
Sync Work와 Async Work의 종류에 따라 다르다. (React 18 이후부터 이벤트 종류에 따라 Async Work 지원)
Q. 클릭을 통해 컴포넌트 상태를 업데이트 하고, 변경된 상태를 기준으로 setState()를 추가로 호풀하면 두 번 리렌더링 되는가?
클릭을 통한 컴포넌트 상태 업데이트시 재호출, 재호출 시점에 추가로 발생하는 여러 번의 setState()는 한 번으로 묶여서 실행한다. 추가적인 업데이트가 발생하지 않을 때까지 계속해서 함수를 재실행하는데 최소 2번 최대 25(RENDERLIMIT = 25)컴포넌트가 재실행될 수 있다.
 
=> 전부 Render phase에서 동작되며, VDOM에 변경 점을 적용하는 것! 화면에 반영되는 단계가 아님. 브라우저 렌더링과 관련된 리소스는 낭비되지 않는다.
 
재조정자는 FiberNode를 하나의 작업단위(unitOfWork)로 취급한다. 즉, 파이버는 자체로 렌더링에 필요한 정보를 담고 있는 객체이자 하나의 재조정 작업 단위라고 볼 수 있다. 렌더가 필요한 Fiber부터 순차적으로 beginwork() 함수가 실행되며, 새로운 Virtual DOM Fiber를 만드는 작업을 완료하면서 finishedWork()라는 작업으로 마무리한다. 
 

코드 링크

function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

 

  • beginWork() : 해당 fiber의 input 값이 current 트리에 있는 fiber의 prop과 비교했을 때 변경되었나를 확인하고, workInProgress fiber의 tag값에 따라 해당 fiber를 업데이트해주고 다음 작업 fiber를 반환하는 함수를 호출한다.
A1 fiber의 child 값에 있는 B1 fiber 가 다음 wordInProgress 가 된다.

 
beginWork()와 completedWork() 사이에 대한 정보는 이곳에서 확인할 수 있다.
 
요약하자면, 더이상의 child가 없는 fiber에 다다를 경우, sibling으로 넘어가기 위해 다시 performUnitOfWork로 돌아가고 더이상의 child와 sibling이 없게 되면 return fiber(위에서 말했듯 부모 파이버)로 돌아간다. completeWork가 없을 때까지, (= return할 수 없을 때까지) 반복한다.

 
 

  • completeWork() : 작업을 완료하고, 개별 fiber의 이펙트를 표시하며 돌아간다(return)

마지막으로 루트에 완료 표시까지 되었다면 render phase의 목적인 "이펙트 정보를 포함한 새로운 fiber 트리"가 완성된 것. 다음 단계인 Commit Phase가 실행된다.
 
 

3. Committing to the DOM (placing the order on the table)

Step 3: React commits changes to the DOM (Commit Phase)

 
리액트 공식에서 표현하는 렌더링의 3단계는 "테이블에 주문된 음식을 배치하다"이다.
 
커밋 단계는 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정을 말한다. 이 단계가 끝나야 비로소 브라우저의 렌더링이 발생한다.
 

  1. 초기 렌더링의 경우 React는 appendChild() DOM API를 사용하여 모든 DOM 노드를 화면에 배치한다.
  2. 리렌더링의 경우 React는 DOM이 업데이트를 반영하기 위해 필요한 최소한의 연산을 적용한다.

React 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경한다. 즉, 렌더링을 수행했으나 커밋 단계까지 갈 필요가 없다면 커밋 단계는 생략될 수 있고 그에 따라 DOM 업데이트도 생략된다. 
 

function commitRootImpl(  
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
) {
    ...
    // The work-in-progress tree is now the current tree. This must come after
  // the mutation phase, so that the previous tree is still current during
  // componentWillUnmount, but before the layout phase, so that the finished
  // work is current during componentDidMount/Update.
  root.current = finishedWork;
}

 

  • 모든 커밋이 끝나면 root.current를 finishedWork로 변경한다. 즉, current는 마지막으로 커밋이 끝난 HostRoot FiberNode!!! 

 
finishedWork와 completedWork 사이에 혼란이 있었는데 위 코드 링크를 보니, root에 최종 반영되는 건 `const finishedWork: Fiber = (root.current.alternate: any);` , FinishedWork였고, `let completedWork = unitOfWork;`처럼 작업단위의 마감을 나타낼 때 completedWork를 사용했다. 
 
 
 


 
저번 달까지 업데이트된 파이버 코드를 보며 최근까지도 업데이트 되고 있는 부분을 확인하니 조금씩 파이버의 구조를 깨닫게 된 것 같다. 실질적으로 코드를 쳐보고 테스트를 해볼 수 없었던 것과, 도식화를 스스로 못한 점은 아쉽지만 이전에 React deep dive 책을 처음 접했을 때 어지럽던 것에 비해 많이 알게 되어 뿌듯하다. 
 
*코드 출처는 코드 상단에 남겨놓았습니다.
 
[React Fiber Architecture]

[React Fiber Architecture]
[React 18 톺아보기 - 04. Concurrent Render]
[A deep dive into React Fiber]
[React 파이버 아키텍처 분석]
[Preserving and Resetting State]
[Understanding Your UI as a Tree]
[ReactFiber.new.js]
[An Introduction to React Fiber - The Algorithm Behind React]
[리액트의 조화(Reconciliation) 과정 정리]
[[번역] Didact 파이버: 점진적 재조정]
[Fiber]
[An Introduction to React Fiber - The Algorithm Behind React]
[A Closer Look at React Fiber]
[React 톺아보기 - 05. Reconciler_4]
[React Deep Dive — Fiber]
[React Fiber]
[React 톺아보기 - 03. Hooks_1]
📚 모던 React Deep Dive