teklog

Fluent React 7 - Reconcile

2024/03/18

n°54

category : React

Fluent React의 챕터 4는 조정Reconciliation에 관한 내용이다. 리액트 문서에서는 파이버나 조정에 관한 문서가 따로 없어 답답했는데, 이 장에서 꽤 깊이 있게 다룬다. 이 글을 통해 파이버와 조정이 무엇이고, 어떻게 리액트 내부에서 렌더링 최적화가 이루어지는지, 그리고 왜 필요했는지 살펴볼 수 있다. 궁금했던 내용이니 자세히 알아보자!


(italic은 인용입니다)


조정 단계의 내부에서 Inside Reconciliation


Reconciliation 이해하기


React.createElement -> 가상 DOM 구성 -> 조정Reconciliation -> 실제 DOM 구성


  • 리액트 vDOM = UI 상태의 청사진
  • 조정reconciliation을 통해 (브라우저, ios, 안드로이드 같은 네이티브 플랫폼 등) 다양한 환경에서 실제 DOM을 업데이트한다
import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <main>
      <div>
        <h1>Hello, world!</h1>
        <span>Count: {count}</span>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>
    </main>
  );
};

1. 리액트 트리로 변환하기 위해 JSX는 다음과 같이 트랜스파일된다
const App = () => {
  const [count, setCount] = useState(0);

  return React.createElement(
    "main",
    null,
    React.createElement(
      "div",
      null,
      React.createElement("h1", null, "Hello, world!"),
      React.createElement("span", null, "Count: ", count),
      React.createElement(
        "button",
        { onClick: () => setCount(count + 1) },
        "Increment"
      )
    )
  );
};

2. createElement는 다음과 같은 리액트 엘리먼트 트리를 반환한다
{
  type: "main",
  props: {
    children: {
      type: "div",
      props: {
        children: [
          {
            type: "h1",
            props: {
              children: "Hello, world!",
            },
          },
          {
            type: "span",
            props: {
              children: ["Count: ", count],
            },
          },
          {
            type: "button",
            props: {
              onClick: () => setCount(count + 1),
              children: "Increment",
            },
          },
        ],
      },
    },
  },
}

간단한 카운터 앱을 예시로, 리액트 내부에서 어떤 일이 일어나는지 이해해보자.


  1. JSX는 React.createElement 함수로 변환된다
  2. createElement 함수는 리액트 엘리먼트 트리를 반환한다 = vDOM과 같다
  • 이 때 최소한의 명령을 통해 브라우저에 "커밋"된다
  • 가능한 DOM을 적게 건드려 렌더링하기 위해 배칭Batching이 이루어진다



Batching


리액트는 조정 과정 중 실제 DOM에 대한 업데이트를 배치 처리한다


…React는 조정reconcile 과정 중에 실제 DOM에 대한 업데이트를 배치 처리하여, 여러 개의 vDOM 업데이트를 단일 DOM 업데이트로 결합합니다. 이는 실제 DOM이 업데이트되어야 하는 횟수를 줄이므로 웹 애플리케이션의 성능 향상에 도움이 됩니다.


조정 단계는 리액트의 핵심이라고 할 수 있는 가상 DOM의 목적을 실현해주는 과정으로 이해할 수 있다! 실제 DOM 변경을 여러번 일으키는 것이 아닌, 여러 상태 변화(가 일으키는 DOM의 변화, UI 업데이트)를 묶어 단일한 DOM 업데이트로 묶는다. 성능 향상에 도움이 되는 것도 자연스럽게 이해된다.


여러번 상태를 업데이트 하는 예시

function Example() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount((prevCount) => prevCount + 1);
    setCount((prevCount) => prevCount + 1);
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

이 예제에서 handleClick 함수는 빠르게 연속적으로 세 번 setCount를 호출한다. React의 배치 처리를 통해 count + 1을 각각 세 번 업데이트하는 대신 count + 3으로 DOM을 한 번만 업데이트할 것이다. 즉 Increment 버튼을 누를 때 1, 2, 3이 순차적이 보이는 것이 아닌 3이 바로 보이는 것이다.


React는 DOM에 가장 효율적인 배치 업데이트를 계산한다. 이를 위해 vDOM 트리의 포크인 새로운 vDOM 트리를 생성하고, 여기서 count를 3일 것이다. 이 트리는 브라우저에 현재 있는 것과 조정reconcile하여 0을 3으로 바꾼다.


조정 과정 중 배치에 대해 간략히 살펴보았으니, 구체적으로 조정 내부에선 어떤 일이 일어나는지 살펴보자. 이해를 위해, 이전 방식인 Stack Reconciler와 Fiber reconciler를 순차적으로 살핀다.



이전 방식 (스택 조정자 Stack reconciler)


React는 16버전 이전까지 렌더링을 위해 스택 데이터 구조를 사용했다. 스택Stack은 후입선출(LIFO, Last In, First Out) 원칙을 따르는 선형 데이터 구조이다. 이는 스택에 마지막으로 추가된 요소가 가장 먼저 제거된다. 자바스크립트의 실행 컨텍스트가 대표적이다. (이전 글 참조)


스택의 기본적인 방법으로 push와 pop을 통해 아이템을 스택 상단에 추가하고 제거한다. 자바스크립트 배열 메서드를 떠올리면 쉽다.


React의 원래 reconciler는 오래된 가상 트리와 새로운 가상 트리를 비교하고 DOM을 업데이트하는 데 사용되는 스택 기반 알고리즘이었습니다. stack reconciler는 간단한 경우에는 잘 작동했지만, 애플리케이션이 크기와 복잡성에서 성장함에 따라 여러 도전적인 상황을 보여줬습니다


도전적인 상황의 예시


  • 중요하지 않지만 연산적으로 비용이 많이 드는 컴포넌트(ExpensiveComponent)가 CPU를 소비하며 렌더링
  • 사용자가 입력 요소에 타이핑
  • 입력이 유효하면 버튼이 활성화
  • Form 컴포넌트가 상태를 가지므로, 리렌더링
import React, { useReducer } from "react";

const initialState = { text: "", isValid: false };

function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleChange = (e) => {
    dispatch({ type: "handleInput", payload: e.target.value });
  };

  return (
    <div>
      <ExpensiveComponent />
      <input value={state.text} onChange={handleChange} />
      <Button disabled={!state.isValid}>Submit</Button>
    </div>
  );
}

function reducer(state, action) {
  switch (action.type) {
    case "handleInput":
      return {
        text: action.payload,
        isValid: action.payload.length > 0,
      };
    default:
      throw new Error();
  }
}

스택 조정자reconciler는 작업을 일시 중지하거나 연기할 수 없이 스택의 상단부터 순차적으로 업데이트를 렌더링할 것이다. 여기서 다음과 같은 문제가 생긴다.


  • 연산적으로 비용이 많이 드는 컴포넌트가 텍스트 입력 렌더링을 차단
  • 사용자 입력이 화면에 지연되어 렌더링
  • 텍스트 필드가 반응하지 않는 것처럼 보여 사용성 저하


직관적인 해결법은 렌더링의 순서를 중요도에 따라 바꾸는 것이다. 중요하지 않지만 비용이 많이 드는 컴포넌트 렌더링은 나중에, 사용자 입력을 더 먼저 우선순위를 두고 먼저 렌더링한다. 즉 스택의 순서대로가 아닌 우선순위 기반으로 가상 DOM을 업데이트하여 해결할 수 있을 것이다. 그러기 위해서 리액트는 사용자 입력 같은 특정 유형의 렌더링 작업을 다른 렌더링보다 우선순위를 매길 수 있어야한다.


이같은 문제 인식과 해결법을 통해 기존 스택 조정자reconciler의 한계와 렌더링 우선순위 부여의 필요성을 이해할 수 있다.


스택 조정자의 한계

  • 우선순위에 따라 가상 DOM을 업데이트 하지 않음
  • 업데이트를 중단하거나 취소할 수 없음
  • 느린 업데이트, 불필요한 렌더링이 성능과 사용성 저하로 이어짐


스택 조정자reconciler는 업데이트를 우선순위에 따라 처리하지 않았다. 렌더링에 우선순위가 없기 때문에, 이는 중요하지 않은 업데이트가 더 중요한 업데이트를 차단할 수 있었다.


스택 조정자reconciler의 또 다른 한계는 업데이트를 중단하거나 취소할 수 없다는 것이다. 이는 스택 reconciler가 업데이트 우선순위를 인식하더라도, 고 우선순위 업데이트가 예약되었을 때 낮은 우선순위의 작업을 취소하거나 중단할 수 없었다. 이는 곧 불필요한 업데이트가 이루어진다는 뜻이며, 가상 트리와 DOM에서 불필요한 작업이 수행되어 애플리케이션의 성능에 부정적인 영향을 줄 수 있다.


업데이트 우선 순위의 예시들

  • 텍스트 입력은 마우스 호버 시에만 렌더링되는 툴팁 보다 높은 우선 순위를 갖는다
  • 버튼 클릭은 notification 알림의 표시 보다 높은 우선순위를 갖는다.
  • input 입력이나 버튼 클릭은 즉각적인 반응을 요구하는 의도적인 행동이기 때문에 우선순위가 높다.


결론

이전의 조정 방식은 스택 순서로 일관되게 업데이트를 처리하여 성능 저하, 사용성 저하로 이어졌다. 효율적인 순서로 가상 DOM을 업데이트할 필요성이 생겼고, 파이버 조정자Fiber reconciler가 개발되었다.



파이버 조정자 Fiber Reconciler




파이버 Fiber란?

  • 조정자 reconciler의 작업 단위
  • 파이버 조정자를 통해 업데이트 우선순위를 결정함
  • React 엘리먼트로부터 생성됨
  • React 엘리먼트와 달리 상태를 보존하고 장기간 존재함
  • React 엘리먼트는 일시적이고 상태가 없음
  • 리액트는 vDOM 트리처럼 조정 단계에서 파이버 트리를 사용함



파이버 데이터 구조


파이버의 데이터 구조를 자세히 살펴보자


기본적으로, 파이버 데이터 구조는 React 애플리케이션에서 컴포넌트 인스턴스와 그 상태의 표현입니다. 논의된 바와 같이, 파이버 데이터 구조는 가변 인스턴스로 설계되어 조정reconcile 과정 중에 필요에 따라 업데이트되고 재배열될 수 있습니다.


파이버는 객체는 이렇게 생겼다

{
  "tag": 3, // 3 = ClassComponent
  "type": "App",
  "key": null,
  "ref": null,
  "props": {
    "name": "Tejas",
    "age": 30
  },
  "stateNode": "AppInstance",
  "return": "FiberParent",
  "child": "FiberChild",
  "sibling": "FiberSibling",
  "index": 0
  // ...
}

이는 App이라고 불리는 클래스형 컴포넌트의 파이버 노드다. 파이버 노드는 컴포넌트의 태그, 타입, props, stateNode, 그리고 컴포넌트 트리 내의 위치 등의 정보를 포함한다.


tag:

tag는 각 컴포넌트 유형을 나타낸다. 클래스 컴포넌트, 함수 컴포넌트, Suspense 및 에러 경계, 프래그먼트 등은 자신만의 숫자 ID를 Fiber로 갖는다. 예시의 3은 클래스 컴포넌트이다.


type:

이 Fiber에 대응하는 함수 또는 클래스 컴포넌트를 가리킨다.예시에서 type은 App이라는 컴포넌트다.


props:

({name: "Tejas", age: 30})은 컴포넌트에 대한 props나 함수의 인자를 뜻한다


stateNode:

이 Fiber가 대응하는 App 컴포넌트의 인스턴스.


컴포넌트 트리에서의 위치(returnchildsiblingindex ):

returnchildsiblingindex 각각은 Fiber 조정자가 “트리를 걷는” 방법을 제공하며, 부모, 자식, 형제 및 Fiber의 인덱스를 식별한다.

파이버 조정자reconciler는 현재 파이버 트리와 다음 파이버 트리를 비교하여 업데이트되거나 추가되거나 제거되어야 할 노드를 파악하는 과정을 포함한다. 즉 앞서 살핀 우선순위를 기반으로 업데이트 순서를 조정하기 위한 것.


파이버 조정 과정 개요


  1. 조정reconcile 과정 동안, 파이버 조정자reconciler는 가상 DOM의 각 React 엘리먼트의 파이버 노드를 생성한다. createFiberFromTypeAndProps라는 함수가 이 작업을 수행한다. 이 함수는 리액트 엘리먼트로부터 파생된 파이버를 반환한다.
  2. 파이버 노드가 생성되면, 파이버 조정자는 UI를 업데이트하기 위한 작업 루프를 사용한다.
  3. 작업 루프는 루트의 파이버 노드에서 시작하여 컴포넌트 트리를 아래로 진행하면서, 업데이트가 필요한 각 파이버 노드를 "dirty"로 표시한다.
  4. 트리의 최하단에 도달하면, 브라우저로부터 분리된 새로운 DOM 트리를 메모리 내에서 거슬러 올라가며 생성한다
  5. 마지막으로 화면에 커밋(플러시)한다


이 과정은 두 함수로 실현된다:

beginWork: “업데이트가 필요한” 컴포넌트를 표시하며 아래로 진행. 3번 과정

completeWork: 브라우저로부터 분리된 실제 DOM 요소들의 트리를 구성하며 위로 거슬러 올라감. 4번 과정

이러한 렌더링(조정) 과정은 사용자가 보기 이전(오프스크린)에 실행되어 언제든 중단되고 버려질 수 있다.



더블 버퍼링


파이버 아키텍처는 게임 세계에서 "더블 버퍼링"이라는 개념에서 영감을 받았으며, 여기서 다음 화면은 오프스크린에서 준비되고 현재 화면으로 "플러시"됩니다.


더블 버퍼링은 컴퓨터 그래픽스와 비디오 처리에서 깜빡임을 줄이고 인지된 성능을 개선하기 위해 사용되는 기술이다. 이 기술은 이미지나 프레임을 저장하기 위한 두 개의 버퍼(또는 메모리 공간)를 생성하고, 최종 이미지나 비디오를 표시하기 위해 정기적인 간격으로 이들 사이를 전환한다.


실제로 더블 버퍼링이 작동하는 방식:

  1. 첫 번째 버퍼는 초기 이미지 또는 프레임으로 채워짐
  2. 첫 번째 버퍼가 화면에 표시되는 동안, 두 번째 버퍼는 새로운 데이터 또는 이미지로 업데이트
  3. 두 번째 버퍼가 준비되면, 첫 번째 버퍼와 교체되어 화면에 표시
  4. 이 과정은 계속되며, 첫 번째와 두 번째 버퍼는 최종 이미지나 비디오를 표시하기 위해 정기적인 간격으로 교체됨


이를 통해 최종 이미지나 비디오가 중단이나 지연 없이 표시되므로 깜박임이나 기타 시각적인 잡음을 줄일 수 있다. (화면 깜빡임의 줄임은 서버사이드 렌더링을 떠올리게 한다)


파이버 조정은 더블 버퍼링과 유사한 방식으로 작동한다.

  • 업데이트가 발생하면 현재 Fiber 트리가 포크되어 주어진 사용자 인터페이스의 새 상태를 반영하도록 업데이트. 이 과정을 렌더링이라고 한다.
  • 그런 다음, 대체할 DOM 트리가 준비되고, 사용자가 보기를 기대하는 상태를 정확하게 반영하면, 더블 버퍼링에서 비디오 버퍼가 교체되는 방식과 유사하게 현재 트리와 교체됩니다.
  • 이를 조정reconciliation의 커밋(단계)라고 한다

커밋 단계 이전에 파이버를 통해 미리 생성된 DOM 트리는 더블 버퍼링의 두번째 버퍼에 해당할 수 있다.


이점:

  • 실제 DOM에 불필요한 업데이트를 하지 않아 성능을 향상시키고 화면 깜빡임을 줄일 수 있다.
  • UI의 새로운 상태를 오프스크린에서 계산하고, 더 새롭고 높은 우선순위의 업데이트가 필요한 경우 이를 버릴 수 있다.
  • 조정reconcile이 오프스크린에서 발생하기 때문에, 사용자가 현재 보고 있는 화면을 손상하지 않고 중단하고 재개할 수 있다


앞서 파이버 조정 과정에서 두 개의 트리가 생성되는데, (더블 버퍼링의 첫번째 버퍼에 해당하는) 현재 파이버 트리와 (두번째 버퍼인) work-in-progress 파이버 트리이다. 새로운 내용은 아니고, 앞선 내용을 이해하면 자연스럽다.


파이버가 엘리먼트와 달리 상태를 보존하고 장기간 유지되기에 현재 파이버를 갖고, 작업 루프의 completeWork에서 트리를 거슬러가며 새로운 트리를 생성한다고 하였다. 전자를 첫번째 버퍼, 후자를 두번째 버퍼로 이해하면 되겠다.


여기까지 파이버의 근간이 되는 개념, 기본 요소들, 작동 과정의 기초 개념을 살폈다. 이제 파이버 조정에서 어떤 일들이 일어나는지 깊이 있게 살펴보자.



파이버 조정 Fiber Reconciliation


img


파이버 조정의 두 단계


  1. 렌더링 단계
  2. 커밋 단계


바로 리액트 공식문서의 설명이 떠오른다. 앞서 파이버 조정자는 UI를 업데이트하기 위한 작업 루프를 사용한다고 하였다. 이 두 단계 접근법은 React가 DOM에 커밋하고 사용자에게 새로운 상태를 보여주기 전에 언제든지 폐기할 수 있는 렌더링 작업을 할 수 있게 한다 = 렌더링을 중단 가능하게 만든다. 앞선 스택 조정자의 문제를 해결하는 구체적인 방식이다.


조금 더 자세히 설명하자면, 렌더링을 중단 가능하게 느끼게 하는 것은 React 스케줄러가 메인 스레드로 실행을 다시 양보하는 휴리스틱을 사용하는데, 이는 120fps 장치에서조차 단일 프레임보다 작은 5ms 마다 발생합니다.



렌더 단계 Render phase


  • 렌더링 단계는 현재 트리에서 상태 변경 이벤트가 발생할 때 시작


beginWork


React는 대체 트리에서 화면 밖에서 변경 작업을 수행하여 각 Fiber를 재귀적으로 거치면서 업데이트가 보류 중임을 신호하는 플래그를 설정한다.


  • beginWork 함수에서 Fiber 노드에 업데이트 여부를 결정하는 플래그를 설정
  • 재귀적으로 다음 Fiber 노드로 이동하여 같은 작업을 수행하고, 트리 바닥에 도달할 때까지 계속함
  • 완료 시, Fiber 노드에 대해 completeWork를 호출하기 시작하고 다시 위로 실행
  • 앞서 루트의 파이버 노드에서 시작하여 컴포넌트 트리를 아래로 진행하면서, 업데이트가 필요한 각 파이버 노드를 "dirty"로 표시한다. -에 해당


beginWork의 시그니쳐는 다음과 같다:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null;

이 함수의 매개변수를 알아보자.


current

  • 현재 트리에 있는 Fiber 노드에 대한 참조
  • 업데이트 여부를 결정하는 플래그 비교의 원대상
  • 업데이트되고 있는 두번째 매개변수 workInProgress 노드와 같은 엘리먼트에서 생성된 파이버
  • 이전 버전과 새 버전의 트리 사이에 무엇이 변경되었는지, 그리고 무엇을 업데이트해야 하는지를 결정하는 데 사용됨
  • 절대 변형되지 않으며 비교를 위해서만 사용


workInProgress

  • 작업 진행 중인 트리에서 업데이트되는 Fiber 노드
  • beginWork함수에 의해 업데이트가 필요한 플래그"dirty"으로 표시되고 반환된다


renderLanes

  • renderLanes는 React의 Fiber 조정자에서 이전의 renderExpirationTime을 대체하는 새로운 개념
  • React가 업데이트를 더 잘 우선순위 지정하고 업데이트 과정을 더 효율적으로 만들게 하기 위해 필요
  • renderLanes는 업데이트가 처리되고 있는 "레인"을 나타내는 비트마스크(bitmask)
  • renderLanes은 업데이트를 우선순위와 기타 요인을 기반으로 분류하는 방법
  • React 컴포넌트에 변경이 이루어지면, 그 우선순위와 기타 특성을 기반으로 레인이 할당됨
  • 우선순위가 높을수록, 할당된 renderLanes도 높음 (높은 숫자를 갖음)


renderLanes 깊게 살펴보기

`renderLanes` 값은 `beginWork` 함수에 전달되어 업데이트가 올바른 순서로 처리되도록 합니다. 더 높은 우선순위 레인에 할당된 업데이트는 더 낮은 우선순위 레인에 할당된 업데이트보다 먼저 처리됩니다. 이는 사용자 상호작용이나 접근성에 영향을 미치는 고우선순위 업데이트가 가능한 한 빨리 처리되도록 합니다.

업데이트를 우선순위 지정할 뿐만 아니라, `renderLanes`는 React가 동시성을 더 잘 관리할 수 있게 도와줍니다. React는 "타임 슬라이싱"이라고 불리는 기법을 사용하여 장기 실행 업데이트를 더 작고 관리하기 쉬운 조각으로 나눕니다. `renderLanes`는 이 과정에서 핵심 역할을 합니다. React가 어떤 업데이트를 우선 처리해야 하는지, 어떤 업데이트를 나중으로 미룰 수 있는지 결정하는 데 도움을 줍니다.

렌더링 단계가 완료된 후, `getLanesToRetrySynchronouslyOnError` 함수가 호출되어 렌더링 단계 동안 생성된 지연된 업데이트가 있는지 결정합니다. 지연된 업데이트가 있다면, `updateComponent` 함수가 `beginWork``getNextLanes`를 사용하여 업데이트를 처리하고 그들의 레인을 기반으로 우선순위를 지정하는 새로운 작업 루프를 시작합니다.
completeWork


completeWork 함수는 새로운 실제 DOM 트리를 구성한다. 이 트리는 브라우저 밖에서 DOM과 분리되어 구성된다. 브라우저인 경우, 이는 document.createElement 또는 newElement.appendChild와 같은 작업을 의미한다. 하지만 이 DOM 트리는 아직 브라우저 내 document에 연결되지 않았다는 점이 중요하다. React는 화면 밖에서 UI의 다음 버전을 생성한다. (더블 버퍼링의 두번째 버퍼 개념을 떠올려보자.)


DOM 트리를 미리 화면 밖에서 수행하는 것은 렌더링을 중단 가능하게 만든다이것이 Fiber 조정자의 중요 포인트이다. React가 계산하고 있는 다음 상태는 아직 화면에 그려지지 않았으므로, 더 높은 우선순위의 업데이트가 예정되어 있는 경우 폐기될 수 있다.


completeWork의 시그니쳐는 다음과 같다:

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null;

매개변수는 beginWork와 동일하다.


completeWork 함수는 beginWork 함수와 밀접한 관련이 있다.


  • beginWork: 파이버에 “업데이트 해야 할” 상태에 대한 플래그를 설정하는 책임이 있다
  • completeWork: 호스트 환경에 커밋될 새로운 DOM 트리를 구성하는 책임이 있다.


completeWork가 맨 위에 도달하고 새로운 DOM 트리를 구성했을 때, "렌더링 단계가 완료되었다"고 한다.


그후, React는 커밋 단계로 이동한다.



커밋 단계 Commit phase


커밋 단계에서 렌더링 단계 동안 가상 DOM에 이루어진 변경 사항을 실제 DOM에 업데이트한다


커밋 단계 동안, 새로운 가상 DOM 트리는 호스트 환경에 커밋되고, work-in-progress 트리는 현재 트리로 교체된다. 이 단계에서 모든 (부수) 효과도 실행된다. 커밋 단계는 두 부분으로 나뉜다.


  • 변형 단계
  • 레이아웃 단계

변형 단계 Mutation phase
  • 변형단계는 가상 DOM에 이루어진 변경 사항을 실제 DOM에 업데이트
  • 이 단계에서 React는 업데이트가 필요한 것을 식별하고 commitMutationEffects라는 특별한 함수를 호출한다.
  • 이 함수는 렌더링 단계 동안 vDOM 트리의 Fiber 노드에 이루어진 업데이트를 실제 DOM에 적용
function commitMutationEffects(Fiber) {
  switch (Fiber.tag) {
    case HostComponent: {
      //  DOM 노드를 새로운 props나 children으로 업데이트
      break;
    }
    case HostText: {
      // DOM 노드를의 text content를 업데이트
      break;
    }
    case ClassComponent: {
      // componentDidMount and componentDidUpdate 같은 컴포넌트 생명주기 메서드를 호출
      break;
    }
    // ... 다른 종류의 노드를 위한 분기 처리 로직이 이어진다
  }
}

레이아웃 단계 Layout phase
  • DOM의 업데이트된 노드의 새 레이아웃을 계산한다
  • 이 단계에서 React는 commitLayoutEffects라는 특별한 함수를 호출
  • 이 함수는 DOM의 업데이트된 노드의 새 레이아웃을 계산한다


레이아웃 단계가 완료되면, React는 렌더링 단계 동안 가상 DOM에 이루어진 변경 사항을 반영하여 실제 DOM을 성공적으로 업데이트할 수 있다.


두 단계로 나누는 이유


커밋 단계를 두 부분(변형과 레이아웃)으로 나눔으로써, React는 효율적인 방식으로 DOM에 업데이트를 적용할 수 있습니다. 조정자의 다른 핵심 함수들과 협력하여 작업함으로써, 커밋 단계는 React 애플리케이션이 더 복잡해지고 더 많은 데이터를 처리하면서도 빠르고 반응적이며 신뢰할 수 있게 유지될 수 있도록 돕습니다.



Effects


React의 조정 과정의 커밋 단계 동안, 커밋 단계의 (부수) 효과가 발생한다. 이 효과들은 유형에 따라 특정 순서로수행된다. 커밋 단계 동안 발생할 수 있는 여러 유형은 다음과 같다:


배치 Placement 효과


이 효과는 새로운 컴포넌트가 DOM에 추가될 때 발생한다. 예를 들어, form에 새 버튼이 추가되면, 그 버튼을 DOM에 추가하기 위한 배치 효과가 발생한다.


업데이트 효과


컴포넌트가 새로운 props나 상태로 업데이트될 때 발생한다. 예를 들어, 버튼의 텍스트가 변경되면, DOM 내의 텍스트를 업데이트하기 위한 업데이트 효과가 발생


삭제 효과


컴포넌트가 DOM에서 제거될 때 발생. 예를 들어, 폼에서 버튼이 제거되면, 그 버튼을 DOM에서 제거하기 위한 삭제 효과가 발생한다.


레이아웃 효과


이 효과는 브라우저가 페이지를 그리기 전에 발생하며, 페이지의 레이아웃을 업데이트하는 데 사용된다. 레이아웃 효과는 함수 컴포넌트에서 useLayoutEffect 훅을 사용하여 관리되고, 클래스 컴포넌트에서는 componentDidUpdate 생명주기 메서드를 사용하여 관리됩니다.


수동 효과 useEffect


커밋 단계의 효과와 달리, 수동 효과는 브라우저가 페인트한 후에 실행되도록 예약된 사용자 정의 효과이다. 수동 효과는 페이지의 초기 렌더링에 중요하지 않은 작업을 수행하는 데 유용하다. 예를 들어, API에서 데이터를 가져오거나 분석 추적을 수행하는 등의 작업입니다. 수동 효과는 렌더링 단계 동안 수행되지 않기 때문에, 컴포넌트를 업데이트하는 시간에 영향을 주지 않는다.



화면에 모든 것을 표시하기


React는 현재 트리, work-in-progress 트리 중 하나를 가리키는 FiberRootNode를 두 트리 상단에 유지한다. FiberRootNode는 조정 과정의 커밋 단계를 관리하는 핵심 데이터 구조다.


  • 렌더링 과정: 가상 DOM에 업데이트가 이루어지면, React는 work-in-progress 트리를 업데이트하면서 현재 트리는 변경하지 않는다. 이를 통해 React는 가상 DOM의 렌더링과 업데이트를 계속하면서도 애플리케이션의 현재 상태를 보존할 수 있다.
  • 렌더링 과정 완료 후: React는 commitRoot라고 하는 함수를 호출한다. 이는 work-in-progress 트리에 이루어진 변경 사항을 실제 DOM에 커밋하는 책임이 있다. commitRoot는 FiberRootNode의 포인터를 현재 트리에서 작업 진행 중인 트리로 전환하여, 작업 진행 중인 트리를 새로운 현재 트리로 만든다.
  • 업데이트 이후: 이때부터, 모든 업데이트는 새로운 현재 트리를 기반으로 한다. 이 과정은 애플리케이션이 일관된 상태를 유지하고, 업데이트가 올바르고 효율적으로 적용되도록 보장한다.


이것이 조정Reconcile이 작동하는 방법이다.



Summary


마지막으로 살펴본 내용들을 요약해보자.


조정Reconciliation이란?

  • vDOM의 렌더링을 최적화하고 실제 DOM을 업데이트하는 과정
  • 이 과정에서 배칭Batching을 통해 여러 상태의 업데이트를 단일한 상태로 바꾸어 실제 DOM을 업데이트한다.


조정자

  • 리액트 내부에서 일어나는 조정을
  • 스택 조정자(Stack Reconciler)
  • 리액트 16전까지 사용되었다.
  • 렌더링 순서의 중요도와 우선순위에 상관없이 스택 순서대로 업데이트가 일어났다
  • 스택 구조를 유지하기에 비용이 높지만 중요도가 낮은 업데이트를 중단하거나 취소할 수 없었다
  • 결과적으로 느린 업데이트, 불필요한 렌더링이 성능과 사용성 저하로 이어짐

파이버

  • 파이버 조정자가 사용하는 조정의 단위.
  • 파이버 (노드)를 통해 현재 파이버 트리와 다음 파이버 트리를 비교하여 업데이트되거나 추가되거나 제거를 파악한다
  • 리액트 엘리먼트는 파이버를 생성한다.
  • 컴포넌트 인스턴스와 그 상태의 표현을 나타낸다.
  • 속성으로 컴포넌트의 종류(tag 클래스, 함수, 서스팬스, 에러바운더리 등), 대응하는 컴포넌트(type), 컴포넌트의 위치, props 등을 포함한다


파이버 조정자의 과정 개요

  1. 파이버 조정자는 가상 DOM의 각 React 엘리먼트의 파이버 노드를 생성한다. createFiberFromTypeAndProps라는 함수가 이 작업을 수행한다. 이 함수는 리액트 엘리먼트로부터 파생된 파이버를 반환한다.
  2. 파이버 노드가 생성되면, 파이버 조정자는 UI를 업데이트하기 위한 작업 루프workLoop를 사용한다. (렌더링 단계)
  3. 작업 루프는 루트의 파이버 노드에서 시작하여 컴포넌트 트리를 아래로 진행하면서, 업데이트가 필요한 각 파이버 노드를 "dirty"로 표시한다. (beginWork)
  4. 트리의 최하단에 도달하면, 브라우저로부터 분리된 새로운 DOM 트리를 메모리 내에서 거슬러 올라가며 생성한다(completeWork)
  5. 마지막으로 화면에 커밋(플러시)한다 (커밋 단계)


파이버 조정자(Fiber Roconciler)를 통한 렌더링 과정

  • 오늘날 리액트는 조정 단계에서 파이버 조정자를 사용한다.
  • 렌더 단계와 커밋 단계로 나뉜다


  • 렌더 단계: beginWork, completeWork 두 단계로 다시 나뉜다
  • beginWork: 트리 아래 방향으로 나아가며 현재 파이버와 작업 중 (work-in-progress) 파이버 노드를 비교하고, 업데이트가 필요한 파이버에 플래그를 설정한다.
  • completeWork: 트리 최하단에서 상위로 올라가며 작업 중파이버 트리를 업데이트하고 실제 DOM을 구성. 커밋 단계까지 전까지 생성된 DOM은 화면에 실제로 나타나지 않는다. 이때 생성된 DOM은 폐기될 수 있다.


  • 커밋 단계: 실제로 화면에 DOM을 페인트하는 과정. 변형, 레이아웃 단계로 구분된다.
  • 변형단계: 렌더링 단계에서 업데이트된 파이버 트리를 실제 DOM에 업데이트 한다
  • 레이아웃 단계: DOM의 업데이트된 노드의 새 레이아웃을 계산한다. commitLayoutEffects라는 함수를 통해 DOM의 업데이트된 노드의 새 레이아웃을 계산한다. 레이아웃 단계가 완료되면, React는 렌더링 단계 동안 가상 DOM에 이루어진 변경 사항을 반영하여 실제 DOM을 성공적으로 업데이트할 수 있다.
  • 커밋 단계 동안 DOM이 추가되면서 일어날 수 있는 부수적인 효과들이 순차적으로 실행된다


  • useEffect와 같은 수동적인 효과는 DOM의 페인팅이 끝난 이후 실행된다