React 톺아보기 - 2 (주석을 담아서)

adultlee·2023년 12월 26일
4
post-thumbnail

해당 시리즈는 제 다른글과 비교했을때 특히나 "메모"의 성격이 짙습니다.
그럼에도 최대한 정제해서 작성해보려 노력하겠습니다. 잘 부탁드립니다

해당 시리즈는 원문의 버전인 v16.12.0 버전 함수형 컴포넌트와 브라우저 환경을 기준으로 진행합니다.
하지만 현 시점에서 추가되거나 변경된 정보들을 추가해서 작성되었습니다.

위 표는 클래스 컴포넌트의 라이프 사이클 실행 시점을 나타내는 표입니다.

해당 표에서의 각 과정에 대한 자세한 설명은 [React.js] 리액트 라이프사이클(life cycle) 순서, 역할, Hook 포스팅을 참고하시면 됩니다.

우선 지금은 React 생명주기의 Render phase와 Commit phase에 대해서 학습할것이다. 라는 점을 기억해주시면 되겠습니다.

Render phase

React Fiber의 Render Phase에서는 컴포넌트의 새로운 출력을 결정하는 작업이 수행됩니다. 이 단계에서는 컴포넌트 트리가 새로운 상태와 속성에 따라 재계산(Reconciliation)되며, 해당 과정은 reconciler 설계가 스택 기반에서 fiber 이는 Virtual DOM에 반영됩니다. Render Phase는 중단 가능한 작업으로 설계되어 있어, React가 다른 중요한 작업(예: 브라우저 이벤트 처리)을 우선적으로 처리할 수 있습니다. 이 단계에서의 계산은 실제 DOM에는 영향을 주지 않으며, 최적화와 성능 개선을 위한 내부적인 프로세스에 초점을 맞춥니다.

Fiber 아키텍처는 이 Render Phase에서 작업을 중단하고 재개하는 유연성을 제공합니다. 하지만 이는 concurrent mode 에서만 비동기와 함께 이루어지며 legacy mode(현재 우리가 일반적으로 사용하는 ReactDOM.render())에서는 위 기능 없이 동기적으로 Render phase가 동작합니다.

아래에서 Mode 들에 대한 추가적인 설명이 포함되어 있습니다.

Mode 에 대한 추가 설명

Legacy Mode

전통적인 React 동기 렌더링 방식입니다. ReactDOM.render()를 사용하여 컴포넌트를 렌더링하며, 업데이트 과정은 중단되거나 재시작되지 않습니다.

Concurrent Mode

비동기 렌더링을 지원하며, 더 복잡한 UI와 상호작용을 가능하게 합니다. Render phase를 중단, 중지, 재시작할 수 있으며, React의 Fiber 아키텍처를 활용합니다. 일반적으로 개발자들이 사용하는 createRoot를 기반으로 동작합니다.

+) 하지만 React 18에서는 이전 버전에서 사용되던 "모드" 개념이 변경되었습니다. 특히, Concurrent Mode는 이제 React 18의 기본 동작으로 통합되었으며, 별도의 "모드"로 구분하지 않습니다. 이것은 React가 더욱 효율적인 비동기 렌더링을 기본적으로 제공하게 되었음을 의미합니다. 따라서, 개발자들은 Concurrent Mode의 이점을 별도의 설정 없이도 사용할 수 있게 되었습니다. React 18에서의 이러한 변화는 애플리케이션의 성능과 사용자 경험을 향상시키는 데 중요한 역할을 합니다.
관련 react 공식 문서

concurrent mode 에 대한 글

Strict Mode

개발 모드에서만 활성화되며, 애플리케이션의 잠재적 문제를 발견하고 경고합니다.

동기 렌더링과 비동기 렌더링

  1. 동기 렌더링 : 이 방식에서는 React가 UI 업데이트를 순차적으로 실행합니다. 한 작업이 완료되어야 다음 작업으로 넘어갈 수 있습니다. 이는 코드가 간단하고 예측 가능하지만, 무거운 작업의 경우 UI가 멈추거나 느려질 수 있습니다.
    예시 : 사용자가 웹 페이지에서 "데이터 로드" 버튼을 클릭합니다. React는 fetchData 함수를 호출하여 서버에서 데이터를 가져옵니다. 이 함수가 데이터를 모두 가져올 때까지 UI는 멈춘 상태입니다. 데이터가 로드되면, UI가 업데이트되어 사용자에게 데이터를 표시합니다.

  2. 비동기 렌더링 : React의 Fiber 아키텍처를 통해 도입된 방식으로, 작업을 중단하고 재개할 수 있습니다. 이를 통해 React는 중요한 작업(예: 사용자 인터랙션)을 우선적으로 처리하고, 무거운 작업은 나중에 처리할 수 있어 애플리케이션의 반응성을 향상시킵니다.
    예시 : 동일한 "데이터 로드" 버튼을 클릭합니다. 이번에는 React가 fetchData를 비동기적으로 호출합니다. 데이터 로딩 동안 사용자는 페이지에서 다른 인터랙션(예: 스크롤, 다른 버튼 클릭)을 계속할 수 있습니다. 데이터가 로드되면, React는 이를 감지하고 UI를 업데이트합니다. 이 과정에서 기존의 사용자 인터랙션은 방해받지 않습니다.

Commit phase

Commit Phase는 React의 렌더링 프로세스의 마지막 단계입니다. 이 단계에서 React는 Render Phase에서 계산된 변경사항을 실제 DOM에 적용합니다.(물론 이 과정이 브라우저가 dom을 반영해서 그리는 과정을 포함하는 것은 아닙니다. react의 컴포넌트 사이클에는 DOM에 반영까지가 마지막 단계이며, 그 이후의 책임은 브라우저가 가집니다.) Commit Phase는 동기적으로 수행되며, 일단 시작되면 중단되지 않습니다. 이 단계의 완료 후에 UI는 최신 상태로 사용자에게 보여지게 됩니다. Commit Phase의 목적은 최종적으로 계산된 UI 변경사항을 확정하고, 사용자에게 안정적이고 일관된 경험을 제공하는 것입니다.

왜 Commit phase 는 Render Phase와 다르게 동기적으로 실행되는걸까?

Render Phase에서 동기적으로 처리하지 않아도 되는 이유는 이 단계가 주로 Virtual DOM에 관한 계산을 수행하기 때문입니다. Render Phase에서는 실제 DOM에 직접적인 변경을 적용하지 않고, 컴포넌트의 렌더링 결과를 메모리 내에서 계산합니다. 이 과정은 동기적이거나 비동기적일 수 있으며, 주로 컴포넌트 트리의 변화를 계산하는 데 집중합니다. 반면, Commit Phase에서는 이러한 계산된 결과를 실제 DOM에 적용하므로, 일관된 UI 업데이트를 위해 동기적으로 실행되어야 합니다.

Virtual DOM

current 트리와 workInProgress 트리의 최상단 노드를 current, workInProgress로 명시하였지만 더 자세히는 Host root라는 이름의 노드입니다. (원문)

+) Host root에 대한 자세한 정보는 다음의 위치에서 확인하실 수 있습니다.
React 레포 ReactFiberRoot.js
React 파이버 아키텍처 분석

React의 virtual Dom은 두개의 tree 구조(일반적으로 tree 구조는 아니며 특수하게 fiber architecture 로 구성되어 있습니다. )로 이루어져 있습니다.

Dom에 이미 적용이 되어 마운트 되어 있는 정보를 그대로 받아온 current tree 와 Render phase에서 Reconciliation 작업을 진행중인 workInProgress tree 로 이루어져 있습니다.

이런 구조를 더블 버퍼링 구조라고 칭합니다.
더블 버퍼링 구조는 싱글 버퍼링 구조를 사용함에 따라 발생하게 되는 깜빡임, 찢어짐 등을 방지하기 위해서 원본을 유지하며, 유연하게 수정도리 수 있는 수정본을 동시에 가지고 있는 형태를 의미합니다.

// (원문)
function commitRootImpl(...) {
  /*...*/
  root.current = finishedWork // Commit phase가 종료된경우
  /*...*/
}

위의 코드에서 확인할 수 있든, finisheWork가 마무리가 된 경우, 현재의 current tree 를 수정합니다. 이것뿐 만이 아니라, 유연하게 작업을 중지하고 재개하는 등 원본을 유지하기 때문에 유연하게 대처가 가능합니다.

VDOM 에서의 노드 Fiber는 자식을 child로 참조하는데 first child만 참조합니다. 나머지 자식들은 이전 형제가 sibling으로 참조하고 있습니다.
모든 자식은 부모를 return으로 참조합니다. 실제 Fiber에 대한 상세한 설명은 하단에서 이어서 진행합니다.

Dom에 개입하는 commit phase가 비동기적으로 이루어진다면 어떨까?

비동기적인 DOM 조작은 예측 불가능한 결과와 레이스 컨디션을 초래할 수 있어 문제가 됩니다. 이는 여러 비동기 작업이 동시에 같은 DOM 요소를 변경하려고 할 때 발생합니다. 또한, DOM은 무거운 작업이므로, 빈번한 비동기 조작은 성능 저하를 일으킬 수 있습니다. 이러한 이유로, 일관된 UI 업데이트와 정확도를 위해 DOM 조작은 일반적으로 동기적으로 처리됩니다.

Fiber

React element를 VDOM에 올려놓아야 합니다. 그 확장을 fiber가 해줍니다.
Fiber Node는 React의 Fiber 아키텍처에서 사용되는 기본 구조 단위입니다. 이는 컴포넌트의 렌더링 상태, 출력, 다음 작업을 관리하기 위한 정보를 담고 있습니다. 각 Fiber Node는 컴포넌트의 인스턴스를 나타내며, React의 렌더링 엔진이 UI를 효율적으로 업데이트하고 관리하는 데 사용합니다. Fiber 아키텍처는 비동기 렌더링을 가능하게 하며, UI 작업을 중단하고 재개하는 기능을 제공합니다. 이를 통해 React는 더 복잡한 UI를 더 빠르고 효율적으로 처리할 수 있습니다.

react-reconciler > ReactFiber.js

18 버전에서는 일부 변경된 점이 있습니다. 원문의 버전과는 다르며 새롭게 추가합니다.

function FiberNode( // 원문과는 다르게 인자에서 type 추론을 진행합니다. 
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // Fiber Node의 유형을 나타냅니다. 함수형 컴포넌트, 클래스 컴포넌트, 호스트 컴포넌트 등을 의미합니다. 
  this.key = key; // React에서 key 속성을 의미합니다. 리스트에서 요소들을 구별할때 사용됩니다. 재조정 과정에서 중요한 역할을 수행합니다. 
  this.elementType = null; // +) 기존과는 다르게 새롭게 추가되었습니다. React 요소의 타입입니다. 예를 들어, <MyComponent />에서 elementType은 MyComponent가 됩니다.
  this.type = null; // 컴포넌트의 구체적인 타입 정보를 담습니다. elementType과 유사하지만, 고차 컴포넌트나 다른 추상화를 다룰 때 차이가 나타날 수 있습니다.
  this.stateNode = null; // Fiber Node와 연관된 실제 DOM 노드나 React 컴포넌트 인스턴스를 가리킵니다. 클래스 컴포넌트의 경우 컴포넌트 인스턴스가 여기에 해당합니다.

  // Fiber
  this.return = null; // 부모 FiberNode
  this.child = null; // 부모입장에서 첫번째 자식노드
  this.sibling = null; // 자신의 바로 "다음" 형제 노드를 칭함
  this.index = 0; // 자신의 형제들 중에서 몇번째 형제인지

  this.ref = null; // +) Dom Node나 컴포넌트 인스턴스의 상태와 업데이트를 관리합니다. 
  this.refCleanup = null; // 참조 정리를 위해서 사용되는 속성입니다

  this.pendingProps = pendingProps; // 컴포넌트에 전달될 예정인 속성 workInProgress 작업이 종료되지 않았음, 아직 pending으로 관리
  this.memoizedProps = null; // Render phase가 종료된 이후 사용되었던 Props
  this.updateQueue = null; // 상태 업데이트나 이펙트(effect)를 위한 업데이트 큐입니다.
  this.memoizedState = null; // 컴포넌트의 메모된(캐시된) 상태입니다.
  this.dependencies = null; // 컴포넌트와 관련된 다양한 의존성들(예: Context)을 관리합니다.

  this.mode = mode; // 컴포넌트의 렌더링 모드(예: Concurrent Mode)를 나타냅니다.

  // Effects
  this.flags = NoFlags; // Fiber Node의 현재 또는 예정된 작업을 나타내는 플래그(예: 업데이트, 마운트)입니다.

  this.subtreeFlags = NoFlags; // 하위 트리에 대한 플래그입니다.
  this.deletions = null; //삭제되어야 할 자식 노드들을 나타냅니다.

  this.lanes = NoLanes; // 우선 순위 레벨을 표시하는 데 사용되는 레인(lanes). 여기서 레인은 작업의 우선 순위와 관련된 개념입니다.
  this.childLanes = NoLanes; // 앞서 언급한 Concurrent Mode에서 비동기 렌더링에서 진행되는 작업의 우선순위에 따라 변경됩니다.

  this.alternate = null; // 더블 버퍼링 구조의 각 tree 를 관리합니다
}

맺음말

지금까지 원문인 React 톺아보기 - 02. Intro의 글을 바탕으로 해당 글에 대해 주석을 담아 학습을 진행해 보았습니다.

다음 글은 React 18 버전으로 넘어가며 변경점들에 대해서 조금 더 학습을 진행해보겠습니다

0개의 댓글