React Fiber 얕게 알아보기

jangws·2022년 5월 15일
2

React

목록 보기
7/8
post-thumbnail

리액트 화이바....?

어디선가 계속 언급되던 React Fiber를 살펴봐야겠다고 마음을 먹었다.

react-fiber-architecture 문서 번역

React Fiber

Fiber는 React v16에서 리액트의 핵심 알고리즘을 재구성한 새 재조정(Reconciliation) 엔진이다. React Fiber의 목표는 애니메이션, 레이아웃, 제스처, 중단 또는 재사용 기능과 같은 영역에 대한 적합성을 높이고 다양한 유형의 업데이트에 우선 순위를 지정하는 것이다.

핵심 기능은 렌더링을 증분하는 것이다. 이는 렌더링 작업을 여러 덩어리로 나누어 여러 프레임에 분산하는 기능이다.

문서에 의하면, 이 기능의 주요 목표는 다음과 같다.

i. 중단 가능한 작업을 덩어리로 나누기
ii. 진행 중인 작업의 우선순위를 지정하고, 리베이스하고 재사용
iii. 리액트의 레이아웃을 지원하기 위해 부모와 자식 간에 yield back and forth
iv. render()로부터 다수 엘리먼트들을 반환
v. 에러 바운더리에 대한 더 나은 지원

Virtual DOM

Virtual DOM은 Real DOM의 in-memory 표현이다. UI 표현은 메모리에 저장되며 Real DOM과 동기화된다. 이는 호출되는 렌더 함수에서 화면에 표시되는 요소가 되는 사이에 발생한 단계이다. 이러한 전체 과정을 재조정(reconciliation)이라 한다.

Virtual DOM은 특정 기술이라기보다는 패턴에 가깝다. React에서는 Virtual DOM이란 UI를 나타내는 객체로 통용되며, React elements와 연관된다.

React는 컴포넌트 트리에 대한 추가 정보를 포함하기 위해 fibers라는 내부 객체를 사용한다.

Virtual DOM은 브라우저 API 위에 있는 JavaScript 라이브러리에서 구현되는 개념이다.

Reconciliation

React가 변경해야 할 부분을 결정하기 위해 한 트리를 다른 트리와 비교하는 데 사용하는 알고리즘이다.

React API의 핵심 아이디어는 업데이트를 통해 전체 앱을 다시 렌더링하도록 생각하는 것이다. 이를 통해 개발자는 앱을 특정 상태에서 다른 상태(A에서 B로, B에서 C로, C에서 A로 등)로 효율적으로 전환하는 방법에 대해서는 걱정하지 않고, 선언적으로 개발할 수 있다.

하지만 각 변경사항마다 전체 앱을 다시 렌더링하면 성능이 좋지 않다.

This demo shows the differences between Stack and Fiber by rendering a Sierpinski triangle that constantly shrinks and grows, and whose nodes have a value that increments by one every second.
Fiber Example
Stack Example

리액트는 이를 해결하기 위해 뛰어난 성능을 유지하면서 전체 앱을 리렌더링할 수 있는 최적화 방법을 고안했다. 이러한 최적화 방법들은 재조정(Reconciliation)이라고 불리는 과정의 일부이다.

재조정은 Virtual DOM으로 널리 이해되고 있는 것의 뒤에 있는 알고리즘이다.

리액트 애플리케이션을 렌더링하면 앱을 묘사하는 노드의 트리가 생성되어 메모리에 저장된다. 그다음 이 트리는 렌더링 환경으로 전달된다(브라우저 환경이라면 이 트리는 일련의 DOM 조작으로 변환된다). 앱이 setState 등을 통해 업데이트되면, 새 트리가 생성된다. 새 트리는 렌더링된 앱을 업데이트하는 데 필요한 작업을 계산하기 위해 이전의 트리와 구분된다(diff).

알고리즘의 요점은 다음과 같다.

  • 컴포넌트 type이 다르면, 실질적으로 다른 트리를 생성한다고 가정한다. 따라서 리액트는 이를 구분(diff)하려고 하지 않고, 대신에 이전의 트리를 완전히 교체한다.
  • 목록은 key를 통해 구분(diff)된다. 따라서 key는 "안정적이고, 예측 가능하고, 유니크해야 한다."

Reconciliation vs Rendering

DOM은 리액트가 렌더할 수 있는 렌더링 환경 중 하나일 뿐이다. 이외에 다른 주요한 렌더링 환경으로는 React Native를 통한 native iOS와 Android views 등이 있다. 이러한 이유로 Virtual DOM은 약간 잘못된 명칭이라고 할 수 있다.

리액트는 재조정(Reconciliation)렌더링을 별개의 단계가 되도록 설계됐다. Reconciler은 트리의 어떤 부분이 변경됐는지 계산한다. 이후Renderer은 계산된 정보를 앱을 실제로 업데이트하는 데 사용한다.

이러한 분리는 React DOMReact Native가 React Core가 제공하는 동일한 Reconciler를 공유하면서 각자 자체적인 Renderer의 사용을 가능하게 한다.

FiberReconciler를 재구현한다.

  • UI에서 모든 업데이트를 즉시 적용할 필요는 없다. 모든 업데이트를 즉시 적용하는 것은 낭비일뿐만 아니라 프레임을 떨어트리고 사용자 경험을 감소시킬 수 있다.
  • 다른 유형의 업데이트들은 우선순위가 다르다. 예를 들어, 애니메이션 업데이트는 데이터 저장소의 업데이트보다 빠르게 완료돼야 한다.
  • push 기반 접근법은 앱(개발자 당신)에게 스케줄 일정을 어떻게 결정해야 할지 요구한다. 반면, pull 기반 접근법은 프레임워크(리액트)가 똑똑하게 당신을 대신하여 그러한 결정을 내린다.

리액트는 현재 스케줄링의 이점을 크게 활용하지 않으며, 업데이트 결과 전체 하위 트리가 즉시 리렌더링된다. 스케줄링의 이점을 활용하기 위해 리액트의 핵심 알고리즘을 정비하는 것이 Fiber의 원동력이다.

Fiber

위에서 살펴봤듯이, Fiber의 목표는 리액트가 스케줄링의 이점을 활용할 수 있도록 하는 것이다. 구체적으로는 다음과 같은 일을 할 수 있어야 한다.

  • 일을 잠시 멈추고 나중에 다시 돌아오기
  • 다른 유형의 일에 우선권 부여하기
  • 이전에 완성한 작업을 재상요하기
  • 더 이상 필요하지 않은 작업을 중단하기

이들을 하기 위해선, 먼저 단위별로 세분화해야 하며, 이 과정 자체가 Fiber라고 할 수 있다. Fiber는 이처럼 작업의 한 단위(unit of work)를 나타낸다.

데이터의 함수로서 리액트 컴포넌트라는 개념은, 보통 다음과 같이 표현된다.

v = f(d)
// view는 data에 대한 function의 결과

리액트 앱을 렌더링하는 것은 다른 함수들을 호출하는 body를 가진 함수를 호출하는 것과 동일하다고 볼 수 있다. 이러한 비유는 fiber에 대해 생각할 때 유용하다.

컴퓨터가 프로그램 실행을 추적할 때 일반적으로 Call Stack을 사용한다. 함수가 실행되면 스택에 새 stack frame이 추가된다(stack frame은 해당 함수에 의해 수행되는 작업을 의미한다.).

UI 렌더링에 최적화되도록 Call Stack의 동작을 사용자 지정할 수 있다면 좋지 않을까? Call Stack과 stack 프레임을 조작할 수 있다면 좋지 않을까?

그것이 Fiber의 목적이다. Fiber는 리액트 컴포넌트에 특화된 stack의 재구성이다. 하나의 Fiber를 virtaul stack frame으로 간주할 수 있다.

stack을 재구성함으로써 얻는 이점은 stack frame을 메모리에 보관하고 원하는 경우 언제든지 실행할 수 있다는 것이다. 이는 스케줄링 목표를 달성하는 데 매우 중요하다.

stack frame을 수동으로 처리하면 스케줄링뿐만 아니라 concurrency 및 error boundary와 같은 기능들에 대한 잠재력을 확보할 수 있다.

Fiber의 구조

Fiber는 컴포넌트 및 컴포넌트의 입력과 출력에 대한 정보를 포함한 자바스크립트 객체이다.

Fiber는 stack frame이기도 하지만, 컴포넌트의 인스턴스이기도 하다.

Fiber에 속하는 중요한 field들은 다음과 같다.

'type'과 'key'

Fiber의 typekey는 리액트 요소에 그것들과 비슷하다.

type 및 key는 재조정 시에 fiber가 재사용될 수 있는지 판단할 때 사용된다.

'child'와 'sibling'

이 필드들은 다른 fiber들을 가리키는데, fiber의 재귀적 트리 구조를 묘사한다고 할 수 있다.

child fiber는 컴포넌트의 렌더 메서드에 의해 반환된 값들이다.

function Parent() {   return <Child /> }

위 예시에서, Parent의 child fiber는 Child이다.

sibling 필드는 render가 여러 자식들을 반환하는 케이스를 설명한다. child fiber들은 첫째 child를 head로 두는 단일 연결 리스트(singly linked list)를 형성한다.

function Parent() {   return [<Child1 />, <Child2 />] }

위 예시에서, Child1과 Child2는 sibling이다.

'return'

return fiber는 프로그램이 현재의 fiber를 처리한 후 반환해야 하는 fiber이다. 개념적으로는 stack frame의 return address이다. 또한, parent fiber라고 생각할 수 있다.

fiber에 여러 child fiber들이 있으면, 각 child fiber의 return fiber는 parent이다. 따라서 위에서 살펴본 예시에서, Child1Child2return fiber는 Parent이다.

'pendingProps'와 'memoizedProps'

개념적으로, props는 함수의 arguments이다. fiber의 pendingProps는 그것의 실행 시작 시 설정되고, memoizedProps는 그것의 실행 끝에 설정된다.

새로 들어오는 pendingProps는 memoizedProps와 같을 때, fiber의 이전 결과가 재사용될 수 있으며, 불필요한 작업을 방지할 수 있다.

'pendingWorkPriority'

fiber가 나타내는 작업들의 우선순위.

'alternate'

  • flush : fiber를 flush하는 것은 output을 화면에 렌더링하는 것을 의미한다.
  • work-in-progress : 아직 완료되지 않은 fiber는 아직 반환되지 않은 stack frame과 개념적으로 같다.

'output'

  • 호스트 컴포넌트 : 리액트 앱에서 leaf nodes이다. 이들은 렌더링 환경에 한정된다. JSX에서는 이들은 소문자 태그 이름을 사용하여 표시된다.

개념적으로, fiber의 output은 함수의 반환 값이다.

모든 fiber는 결국 output을 가지지만, output은 호스트 컴포넌트에 의해 리프 노드에서만 생성된다. output은 트리 위로 전달된다.

렌더링 환경에 대한 변경사항을 flush할 수 있도록, output은 최종적으로 렌더러에게 전달된다. output이 생성되고 업데이트되는 방법을 정의하는 것은 렌더러가 할 일이다.

{
    
    type: any, // For a class component, it points to constructors; for a DOM element, it specifies HTML tags
    key: null | string, // The unique identifier
    stateNode: any, // Save the references to class instances of components, DOM nodes, or other React element types associated with the fiber node
    child: Fiber | null, // The first child node
    sibling: Fiber | null, // The next child node
    return: Fiber | null, // The parent node
    tag: WorkTag, // Define the type of fiber action. For more information,see https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
    nextEffect: Fiber | null, // The pointer to next node
    updateQueue: mixed, // The queue for status update, callback function, and DOM update
    memoizedState: any, // The fiber state for output creation
    pendingProps: any, // The props that are updated from the new data of React elements and need to be applied to child components or DOM elements
    memoizedProps: any, // The props used to create the output during the previous rendering
    // ……     
}

작동 원리

  • requestAnimationFrame()
    - 높은 우선 순위 함수 스케줄링
  • requestIdleCallback()
    - 낮은 우선 순위 함수 스케줄링

참고
https://github.com/acdlite/react-fiber-architecture
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js
https://www.alibabacloud.com/blog/a-closer-look-at-react-fiber_598138
https://blog.kiprosh.com/react-fiber/

0개의 댓글