[번역] A deep dive into React Fiber internals

원문 : https://blog.logrocket.com/deep-dive-into-react-fiber-internals/

ReactDOM.render(<App />, document.getElementById('root')) 를 호출하면 어떻게 될까요?

ReactDOM이 내부적으로 DOM 트리를 구축하고 화면에 애플리케이션을 렌더링한다는 사실은 알고는 있지만 실제로 DOM 트리를 어떻게 구축할까요? 또한 app의 상태가 변경될 때 트리를 어떻게 변경할까요?

이번 포스트에서는 React 15.0.0까지 DOM 트리를 구축한 방법과 문제점, 그리고 React 16.0.0이 이 문제들을 어떻게 해결했는지를 설명합니다. 순전히 세부적인 내부 구현 사항에 대한 내용이며 실제로 React를 사용하는 프론트엔드 개발에서는 꼭 필요하지 않지만 광범위한 개념들에 대해 다룹니다.

Stack reconciler

우리에게 익숙한 ReactDOM.render(<App />, document.getElementById('root')) 부터 시작하겠습니다.

ReactDOM 모듈은 <App /> 을 reconciler에게 전달합니다. 여기서 두 가지 의문이 발생합니다.

  1. <App /> 이 의미하는 건 무엇인가요?

  2. reconciler는 무엇인가요?

위 두 가지 질문에 대해 이야기해봅시다.

<App /> React 엘리멘트이고 "엘리멘트는 트리를 나타냅니다"

An element is a plain object describing a component instance or DOM node and its desired properties. - React Blog

즉 element는 실제 DOM 노드 또는 컴포넌트 인스턴스가 아닙니다. 어떤 종류의 element이고 어떤 속성을 가지고 있으며 어떤 children을 가지고 있는지 나타내는 React에게 알려주는 방식입니다.

여기가 바로 React의 강력함이 있는 부분입니다. React는 실제 DOM 트리의 수명 주기를 자체적으로 구축, 렌더링 및 관리하는 복잡한 모든 부분을 추상화하여 개발자에게 효율적으로 제공합니다. 좀 더 자세하게 알기 위해 객체 지향 관점에서 일반적인 방식으로 접근해보겠습니다.

일반적인 객체 지향 프로그래밍 세계에서 모든 DOM 요소의 수명 주기를 인스턴스화하고 관리해야 합니다. 예를 들어 간단한 폼과 submit 버튼을 만들때도 상태 관리에 개발자의 노력이 들어가야 합니다.

Button 컴포넌트가 isSubmitted 이라는 상태 변수를 가지고 있다고 가정합니다. Button 컴포넌의 수명 주기는 앱이 각 상태를 관리해야하는 아래 순서도와 유사합니다.

순서도의 크기와 코드 수는 상태 변수의 증가에 따라 기하급수적으로 증가합니다.

React는 이 문제를 해결하기 위한 두 종류의 element가 있습니다.

  • DOM element : element의 타입이 string일 때, 예를 들면 <button class="okButton"> OK </button>

  • Component element : 타입이 class이거나 function일 때, 예를 들면 <Button className="okButton"> OK </Button> 에서 <Button> 이 일반적으로 React 컴포넌트인 클래스 이거나 함수형 컴포넌트 일 때.

두 타입 모두 단순한 객체라는 것을 인지하는게 중요합니다. 화면에 렌더링해야 하는 내용에 대한 설명일 뿐이며, 실제로 만들고 인스턴스화 할 때 렌더링이 발생하지 않습니다. 이런 방식은 React가 DOM 트리를 구축하기 위해 파싱하고 순회하는 것이 더욱 편리해집니다. 실제 렌더링은 이런 순회가 전부 끝난 후에 발생합니다.

React는 클래스 및 함수형 컴포넌트를 만나면 element의 props에 기반해서 렌더링할 element를 요청합니다. 예를 들어 <App /> 컴포넌트는 다음과 같이 구성되어 있다

<Form>
  <Button>
    Submit
  </Button>
</Form>

React는 <Form><Button> 컴포넌트에게 해당 element에 대응하는 props를 기반으로 무엇을 렌더링할지 요청합니다. 예를 들어 Form 컴포넌트가 다음과 같이 함수형 컴포넌트라면

const Form = (props) => {
  return (
    <div className="form">
      {props.form}
    </div>
  )
} 

React는 어떤 element가 렌더링 되는지 알기위해 render() 를 호출하고, 최종적으로 자식 요소와 함께 <div> 가 렌더링 되는 걸 확인할 수 있습니다. React는 페이지의 모든 컴포넌트에 대해 기본적인 DOM tag element를 알 수 있을 때까지 이 과정을 반복합니다.

React 앱의 컴포넌트 트리를 재귀적으로 순회하면서 기본적인 DOM tag element를 알아내는 과정을 reconciliation(조정) 이라고 합니다. reconciliation이 끝날 때 쯤, React는 DOM 트리의 결과를 알 수 있고, react-dom이나 react-native와 같은 렌더러에게 DOM 노드를 변경해야할 최소한의 변화를 적용합니다.

ReactDOM.render()setState() 를 호출할 때, React는 reconciliation을 수행합니다. setState 의 경우, 순회를 수행하고 렌더링된 트리와 새 트리를 비교해서 변경된 사항을 파악합니다. 그 다음 이 변경사항을 현재 트리에 적용하여 setState() 호출에 해당하는 상태를 업데이트합니다.

reconciliation이 무엇인지 알았으니, 문제점을 알아볼까요?

아! 그런데 왜 이걸 "스" reconciler라고 부를까요?

이 이름은 LIFO 메커니즘인 "stack" 데이터 아키텍에서 유래되었습니다. 그런데 스택은 방금 본 것과 무슨 관계가 있을까요? 결과적으로 효율적으로 재귀를 수행하고 있기 때문에 이 모든게 스택과 관련이 있습니다.

Recursion

좀 더 이해하기 위해 간단한 예를 어 call stack에서 어떤 일이 발생하는지 보겠습니다.

function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2)
}

fib(10)

위에서 보이는 것처럼 콜스택은 fib() 의 모든 호출을 반환할 첫 번째 함수인 fib(1) 을 팝업할 때까지 스택으로 push를 계속 합니다. 재귀적인 호출을 계속 푸시한 다음, return 구문에 도달하면 pop을 합니다. 이런식으로 fib(3) 이 반환할 때까지 효율적으로 콜스택을 사용한다음, 스택에서 마지막을 pop 합니다.

위에서 본 reconciliation 알고리즘은 순전히 재귀 알고리즘입니다. 업데이트를 하게 되면 그 아래로 전체 하위 트리가 즉시 다시 렌더링됩니다. 이 알고리즘은 잘 작동하지만 약간의 제한사항이 있습니다. Andrew Clark은 다음과 같이 언급했습니다.

  • UI에서는 모든 업데이트를 바로 반영할 필요가 없습니다. 실제로 변경사항이 발생할 때마다 업데이트를 반영하는건 리소스를 낭비하며 프레임이 떨어지고, 사용자 경험이 저하될 수 있습니다.

  • 또한 업데이트 종류마다 우선 순위가 다릅니다. 애니메이션 업데이트는 데이터 저장소 업데이트보다 빨리 완료되야 합니다.

프레임 저하가 의미하는 바가 무엇이고 재귀적인 접근 방식이 문제가 되는 이유는 뭘까요? 이를 이해하기 위해서는 프레임 속도와 사용자 경험 관점에서 왜 중요한지 간략하게 알아보겠습니다.

프레임 속도는 연속적인 이미지가 화면에 나타나는 빈도입니다. 컴퓨터 화면에서 보이는 모든 것은 눈에 즉시 나타나는 속도로 화면에서 재생되는 이미지 또는 프레임으로 구성됩니다.

더 자세히 이해하기 위해서, 컴퓨터 화면을 플립 북이라고 가정하고 플립 북의 페이지를 넘길 때 일정한 속도로 재생되는 프레임을 상상해보세요. 즉 컴퓨터 화면은 화면 상황이 변할 때 항상 재생되는 자동 플립북입니다. 이해가 안된다면 아래 영상을 참고하세요.

일반적으로 영상이 사람 눈에 부드럽고 즉각적으로 느껴질려면 약 30 FPS(초당 프레임 수)의 속도로 재생되야 합니다. 이보다 높다면 더 좋은 사용자 경험을 제공합니다. 그래서 정밀도가 중요한 1인칭 슈팅 게임에서 게이머가 더 높은 프레임을 선호하는 이유이기도 합니다.

하지만 요즘 대부분의 장치는 60FPS로 화면을 재생합니다. 즉 1/60 = 16.67ms로 새 프레임이 16ms마다 나타납니다. React 렌더러가 화면에 렌더링할 때 16ms 이상 걸리면 브라우저가 해당 프레임을 제거하기 때문에 이 숫자는 매우 중요합니다.

실제로 브라우저는 housekeeping 작업을 진행하므로 모든 작업은 10ms 이내에 완료되어야 합니다. 만약 작업이 이를 초과한다면 프레임 속도가 떨어지고 화면의 내용이 흔들리게 됩니다. 이를 버벅거림이라고 하며 사용자 경험에 부정적인 영향을 미칩니다.

물론 내용이 정적이고 텍스트 위주라면 큰 우려가 되지 않습니다. 그러나 애니메이션을 나타낸다면 이 숫자는 중요해집니다. 따라서 React reconciliation 알고리즘이 업데이트가 있을 때마다 전체 App 트리를 순회하고 다시 렌더링 할 때 순회가 16ms 이상 걸린다면 프레임이 삭제되어 불량이 발생하게 됩니다.

이러한 이유 때문에 업데이트를 우선 순위별로 분류하고 reconciler에게 모든 업데이트를 무조건 전달하지 않는 것이 좋습니다. 또한 다음 프레임에서 작업을 일시 중단하고 다시 시작할 수 있는 기능도 React에 있습니다. 이런 기능들을 통해 React는 렌더링시 16ms에 대한 제한사항을 만족할 수 있습니다.

React팀은 이런 요소들을 고려해서 Fiber라는 새 reconciliation 알고리즘을 다시 만들었습니다. 이제 Fiber가 어떻게, 왜 존재하는지 그리고 의미하는 바를 이해했을거라 생각합니다. 그럼 Fiber 가 위 문제들을 어떻게 해결하는지 알아보겠습니다.

How Fiber works

Fiber의 개발 동기를 알게 되었으니, 이를 달성하기 위해 필요한 기능을 요약해보겠습니다.

다시 한 번 Andrew Clark의 메모를 참조하겠습니다.

  • 작업 별 우선 순위 지정

  • 작업을 일시 중지하고 나중에 다시 시작

  • 더 이상 필요하지 않은 경우 작업 중단

  • 이전에 완료된 작업 재사용

이 구현에 어려운 점중 하나는 JavaScript 엔진이 동작하는 방식과 언어 자체에 스레드가 부족다는 점입니다. 이를 이해하기 위해 JavaScript 엔진이 execution context를 처리하는 방법을 간략히 살펴보겠습니다.

JavaScript execution stack

JavaScript 언어로 함수를 작성할 때마다 JS 엔진은 function execution context를 호출합니다. 또한 JS 엔진이 실행 될 때마다 global object를 점유하는 global execution context 또한 생성됩니다 - 예를 들면 브라우저에서는 window 객체이고 Node.js에서는 global 객체입니다. 이 두 context는 실행 스택이라고 하는 스택 데이터 구조를 사용해서 JS에서 처리됩니다.

즉 다음처럼 작성된다면 :

function a() {
    console.log("i am a");
    b();
}

function b() {
    console.log("i am b");
}

a();

JavaScript 엔진은 먼저 global execution context를 생성하고 이를 실행 스로 push 합니다. 그 다음 a() 에 대한 function execution context를 생성합니다. b()a() 내부에서 호출되기 때문에 b() 또한 또 다른 function execution context를 생성하고 실행 스택에 push 합니다.

함수 b() 가 반환하면 엔진은 b() 의 context를 제거하고, 함수 a() 가 종료되면 a() 의 context가 제거됩니다. 실행중에 스택은 다음과 같습니다.

이때 브라우저가 HTTP request와 같은 비동기 이벤트를 만들면 어떻게 될까요? JS엔진은 실행 스택을 저장하고 비동기 이벤트를 처리할까요? 아니면 이벤트가 완료될때 까지 기다릴까요?

JS엔진은 다르게 동작합니다. JS엔진은 실행 스택 말고도 이벤트 큐라고 하는 큐 데이터 구조가 있습니다. 이벤트 큐는 브라우저로 들어오는 HTTP 또는 네트워크 이벤트와 같은 비동기 호출을 처리합니다.

JS엔진이 실행 스택이 비어있으면 큐의 작업을 처리합니다. 즉 실행 스택이 비워질때마다 JS엔진은 큐를 확인하고 큐에서 작업을 가져와 해당 이벤트를 처리합니다. JS엔진은 실행 스택이 비어있거나 실행 스택에 global execution context만 있을 때 이벤트 큐를 확인한다는 점에 유의하세요.

비동기 이벤트라고 부르지만 여기엔 미묘한 차이가 있습니다. 이벤트가 대기열에 도착하는 시점은 비동기이지만 실제로 처리되는 시점에서는 비동기적이진 않습니다.

다시 stack reconciler로 돌아와서, React가 트리를 순회할 때마다 실행 스택은 이렇게 동작합니다. 따라서 업데이트가 도착하면 이벤트 대기열로 추가됩니다. 그리고 실행 스택이 비워질때만 업데이트가 처리됩니다. 이게 바로 Fiber가 intelligent 기능 (일시 중지 및 재개, 중단)으로 스택을 거의 다시 구현함으로써 해결하는 문제입니다.

다시 한 번 Andrew Clark의 메모를 참조하겠습니다.

"Fiber는 React 컴포넌트에 특화된 stack의 재구현입니다. 단일 fiber를 가상의 stack 프레임으로 생각할 수 있습니다.

stack을 다시 구현할 때 장점은 stack 프레임을 메모리에 유지하고 언제든지 원하는대로 실행할 수 있다는 점입니다. 이건 우리의 목표를 달성하는데 중요합니다.

스케줄링 외에도 stack 프레임을 수동으로 처리하면 동시성 및 오류 경계와 같은 잠재적인 문제가 발생할 수 있습니다. 향후 섹션에선 이것에 대해 다룹니다.

간단히 말해서 fiber는 그 자체로 작업 단위의 가상의 스택을 나타냅니다. 이전 reconciliation 알고리즘에서 React는 불변한 React element를 재귀적으로 순회하는 트리를 만들었습니다.

fiber의 구현에서 React는 변경할 수 있는 fiber 노드 트리를 생성합니다. fiber 노드는 컴포넌트의 state, props 그리고 기본적인 나타낼 DOM 요소를 효율적으로 가지고 있습니다.

fiber 노드는 변경될 수 있기 때문에 React는 업데이트를 위해 모든 노드를 다시 만들 필요가 없습니다. 업데이트가 있다면 간단히 노드를 복사하고 업데이트할 수 있습니다. 또한 React는 fiber 트리를 재귀적으로 순회하지 않습니다. 대신 single linked list를 만들고 부모 우선 깊이 순회(parent-first, depth-first)를 진행합니다.

Singly linked list of fiber nodes

fiber 노드는 스택 프레임 뿐만 아니라 React 컴포넌트의 인스턴스를 나타냅니다. fiber 노드는 다음과 같은 속성들로 구성됩니다.

Type

<div> , <span>, 등 host 컴포넌트(문자열) 이거나 합쳐진 컴포넌트인 클래스나 함수

Key

React에서 전달하는 key와 동일

Child

컴포넌트에서 render() 를 호출할 때 반환되는 요소를 나타냅니다. 예를 들면:

const Name = (props) => {
    return (
        <div className="name">
            {props.name}
        </div>
    )
}

<Name><div> 를 반환하므로 여기서 child는 <div> 입니다.

Sibling

render 가 반환하는 element 목록을 나타냅니다.

const Name = (props) => {
    return ([<Customdiv1 />, <Customdiv2 />])
}

위의 경우, <Customdiv1><Customdiv2> 는 부모인 <Name> 의 자 요소입니다. 두 자식 요소는 단순 연결 리스트로 이루어져 있습니다.

Return

논리적으로 부모 fiber 노드인 스택 프레임에 반환을 의미합니다. 즉 부모 요소를 나타냅니다.

pendingProps and memoizedProps

메모화(Memoization)는 중복 계산을 피하기 위해 함수의 실행 결과를 저장합니다. pendingProps 는 컴포넌트로 전달 된 props이고, memoizedProps 는 노드의 props를 저장하고 실행 스택의 끝에서 초기화됩니다.

전달받은 pendingPropsmemoizedProps 가 같으면 fiber의 이전 결과를 재사용할 수 있다는 신호를 보내 불필요한 작업을 방지합니다.

pendingWorkPriority

fiber에서 숫자로 작업의 우선 순위를 나타냅니다. ReactPriorityLevel 모듈은 다양한 우선 순위 레벨과 해당 숫자를 나열합니다. 값이 0인 NoWork 을 제외하고, 숫자가 클수록 우선 순위가 낮습니다.

예를 들면, 다음 함수로 fiber의 우선 순위가 주어진 수준보다 높은지 확인할 수 있습니다. 스케줄러는 우선 순위 필드를 사용해 다음에 수행할 작업을 검색합니다.

function matchesPriority(fiber, priority) {
    return fiber.pendingWorkPriority !== 0 && 
           fiber.pendingWorkPriority <== priority
}

Alternate

컴포넌트 인스턴스는 최대 두 개의 fiber를 가질 수 있습니다 : 현재 fiber 와 진행중인 fiber. 현재 fiber와 진행중인 fiber는 서로 대체될 수 있습니다. 현재 fiber는 이미 렌더링 된 것이며, 진행중인 fiber는 개념적으로 아직 반환되지 않은 스택 프레임입니다.

Output

React 애플리케이션의 leaf 노드입니다. 렌더링 환경에 따라 노드는 달라질 수 있습니다(브라우저에서는 div, span 등). JSX에서는 소문자 태그 이름을 사용합니다.

개념적으로 fiber의 출력물은 함수의 반환 값입니다. 모든 fiber는 결국 출력물을 가지긴 하지만, 실제 출력물은 호스트 컴포넌트의 leaf 노드에서만 생성됩니다. 그 다음 출력물은 트리의 위로 전달됩니다.

최종적으로 출력은 렌더러가 렌더링 환경에서 변경 사항을 flush할 수 있도록 렌더러에게 전달됩니다. 예를 들면 fiber 트리가 다음과 같은 코드 앱에서 어떻게 동작하는지 살펴보겠습니다.

const Parent1 = (props) => {
    return ([<Child11 />, <Child12 />])
}

const Parent2 = (props) => {
    return (<Child21 />)
}

class App extends Component {
    constructor(props) {
        super(props)
    }
    render() {
        <div>
            <Parent1 />
            <Parent2 />
        </div>
    }
}

ReactDOM.render(<App />, document.getElementById('root'));

fiber 트리는 자식 요소들 간의 단순 연결 리스트(sibling 관계)와 부모-자식 관계의 연결 리스트로 이루어져 있습니다. 트리는 깊이-우선 탐색 통해 순회합니다.

Render phase

React가 이 트리를 빌드하고 reconciliation 알고리즘을 어떻게 적용하는지 이해하기 위해, React 소스 코드와 유닛테스트에 디버깅을 붙여 과정을 살펴보겠습니다.

이 과정이 궁금하다면 리액트 소스 코드를 clone 하고 이 디렉터리로 이동하세요. Jest로 테스트케이스를 추가하고 디버거를 붙입니다. 여기서 테스트는 기본적으로 텍스트가 있는 버튼을 렌더링하는 간단한 동작입니다. 버튼을 클릭하면 앱이 버튼을 제거하고 다른 div 로 렌더링하기 때문에 여기서 text는 상 변수 입니다.

'use strict'

let React;
let ReactDom;

describe('ReactUnderStanding', () => {
    beforeEach(() => {
        React = require('react');
        ReactDOM = require('react-dom');
    });
    
    it('works', () => {
        let instance;
        
        class App extends React.Component {
            constructor(props) {
                super(props);
                this.state = {
                    text: 'hello'
                }
            }
            
            handleClick = () => {
                this.props.logger('before-setState', this.state.text);
                this.setState({text: 'hi'});
                this.props.logger('after-setState', this.state.text);                
            }
            
            render() {
                instance = this;
                this.props.logger('render', this.state.text);
                if (this.state.text === 'hello') {
                    return (
                        <div>
                            <div>
                                <button onClick={this.handleClick.bind(this)} >
                                    {this.state.text}
                                </button>
                            </div>
                        </div>
                    )
                } else {
                    return (
                        <div>
                            hello
                        </div>
                    )
                }
            }
        }
        
        const container = document.createElement('div');
        const logger = jest.fn();
        ReactDOM.render(<App logger={logger}/>, container);
        console.log('clicking');
        instance.handleClick();
        console.log('clicked');
        
        expect(container.innerHTML).toBe(
            '<div>Hello</div>'
        )
        
        expect(logger.mock.calls).toEqual(
            [
                ["render", "hello"],
                ["before-setState", "hello"],
                ["render", "hi"],
                ["after-setState", "hi"],
            ]
        )
    })
})

React는 먼저 초기 렌더링 되는 현재 트리를 생성합니다.

createFiberFromTypeAndProps() 는 React element로부터 data를 받아와 React fiber를 생성합니다. 이 함수에 breakpoint를 두고 테스트를 진행하면 다음과 같은 콜스택을 확인할 수 있습니다.

콜스택은 다시 render() 를 호출하며 createFiberFromTypeAndProps() 로 들어갑니다. 여기엔 주의 깊게 봐야할 함수들이 있습니다 : workLoopSync(), performUnitOfWork(), 그리고 beginWork() 입니다.

function workLoopSync() {
    // 이미 시간 초과되었다면, yield 확인 없이 바로 실행합니다.
    while (workInProgress !== null) {
        workInProgress = performUnitOfWork(workInProgress);
    }
}

workLoopSync() 는 React가 <App> 노드부터 시작해서 자식인 <div> , <button> 으로 재귀적으로 이동하면서 트리 구축을 시작하는 부분입니다. workInProgress 는 수행할 다음 노드가 있다면 다음 fiber 노드를 참조합니다.

performUnitOfWork() 은 fiber 노드를 인수로 받으며 노드의 대체를 가져온 후 beginWork() 을 호출합니다. execution 스택에서 execution context를 실행하는 것과 같습니다.

React가 트리를 빌드할 때, beginWork()createFiberFromTypeAndProps() 로 이어지고 fiber 노드를 생성합니다. React는 재귀적으로 이 작업들을 수행한 후 performUnitOfWork() 은 null을 반환하여 트리의 끝에 도달했음을 전달합니다.

이제 버튼을 클릭하고 상태를 업데이트하는 instance.handleClick() 을 수행하면 어떻게 될까요? 이때, React는 fiber 트리를 순회하면서 각 노드를 복제하고 노드에 수행할 작업이 있는 지 확인합니다. 이 시나리오의 콜스택은 다음과 같습니다.

첫 번째 콜스택에서 보지 못했던 completeWork()completeUnitOfWork() 은 여기서 확인할 수 있습니다. performUnitOfWork() 이나 beginWork() 처럼 현재 실행의 완료 부분을 수행하며 효율적으로 스택에 다시 돌아가는 것을 의미합니다.

다음의 네 가지 함수는 실행하는 작업을 수행하고 현재 수행중인 작업을 작업 단위로 제어할 수 있습니다. 이는 stack reconciler에서는 누락됬으며, fiber에서는 해당 기본 작업을 수행하는데 4단계가 있습니다.

여기서 각 노드는 completeWork() 이 자식 요소를 전부 반환할 때까지 completeUnitOfWork() 로 이동하지 않는 점을 주목해야 합니다. 예를 들어 <App>performUnitOfWork()beginWork() 으로 시작한 다음 Parent1의 performUnitOfWork()beginWork() 로 이동합니다. <App> 의 모든 자식 컴포넌트를 실행 후 완료합니다.

이게 바로 React가 렌더링 단계를 완료하는 순간입니다. click() 업데이트를 기반으로 새로 구축된 트리를 workingInProgress 트리라고 합니다. 기본적으로 렌더링 대기중인 초안 트리입니다.

Commit Phase

렌더링 단계가 완료되면, React는 커밋 단계로 이동하여 현재 트리와 workingInProgress 트리의 루트 포인터를 교체하여 효과적으로 업데이트가 반영된 트리로 교체합니다.

뿐만 아니라 React는 포인터를 Root에서 workingInProgress 트리로 변경 한 이후 이전 트리의 노드들을 재사용합니다. 이 최적화된 프로세스의 가장 큰 효과는 앱의 이전 상태와 다음 상태, 또 다음 상태의 전환이 원활하게 전환되는 것 입니다.

16ms 프레임의 시간은 어떤가요? React는 각 수행중인 작업 단위에 대해 내부 타이머를 두고 시간 제한을 모니터링 합니다. 시간이 다되면 React는 현재 진행중인 작업 단위를 일시 중지하고 컨트롤을 다시 메인 스레드에게 다시 넘기며 그 시점에 완료된 트리를 렌더링 합니다.

이후에 다음 프레임에서 React는 중단된 부분을 선택 후 트리를 계속 구축합니다. 시간내에 완료되면 workInProgress 트리를 커밋하고 렌더링을 완료합니다.

Conclusion

좀 더 자세히 알기 위해선 Lin Clark의 비디오를 시청하는걸 추천합니다. 멋진 애니메이션을 통해 이 알고리즘을 설명합니다.

작업중...

Last updated