TIL 100 - React V-DOM과 Fiber 아키텍처

김영현·2024년 6월 4일
0

TIL

목록 보기
111/129

100번째 TIL🎉

9월말 프로그래머스 데브코스를 계기로 프로그래밍 공부를 시작한지 어언 9개월쯤...
100번째 TIL이라니 감격스럽기도 하고 아직 취준생이라는 사실에 살짝 눈물이 삐져나온다.😥
물론 꼴랑 몇개월 하고 취업하려는 마인드로부터 굉장히 글러먹은 인간이라는 것도 알수있다....😭
아무튼 다음 200, 300번째 TIL을 향해 꾸준히 달려보자!


Virtual DOM

출처 : https://github.com/acdlite/react-fiber-architecture?tab=readme-ov-file
Reconciliation is the algorithm behind what is popularly understood as the "virtual DOM."
=> Reconciliation은 일반적으로 Virtual DOM으로 이해되는 알고리즘입니다.

이게 무슨소리지? Virtual DOM이 사실 Reconciliation이란건가?

=> Virtual DOM은 사실 잘못된 명칭이다.😮 또한 Virtual DOM은 하나의 패턴이다.
Reconciliation이 사람들이 이해하고 있는 Virtual DOM 패턴이다.
예를들어 안드로이드, IOS라면 DOM환경이 아니다. 따라서 React DOMReact Naitive(모바일)이 리액트코어의 동일한 Reconciliation을 공유하며 자체렌더러를 사용할수 있음을 의미한다.

아하 결국 Virtual DOM은 실제 DOM을 뜻하는게 아니라 Reconciliation을 활용한 트리 변경 알고리즘이구나!


React Fiber Architecture

FiberReact에서 몇가지 프로퍼티를 가진 단순한 JS객체다. (다음 목차에 소개)
이전 TIL에서 배운 Reconilation을 해주는 라이브러리에 Fiber라는 이름이 붙어 Fiber reoconciler라는 이름이 되었다.
그러므로 React Fiber란 사실 reconciliation해주는 리액트 내장 라이브러리를 뜻한다.
이를 알기위해선 기존의 Reconciler을 조금 알아두면 좋다.

Old reconciler: Stack

말 그대로 자료구조스택을 활용하여 Reconciliation을 집행하던 아키텍처다. 동시에 동기식 구조다.
Reconciliation은 한 트리를 다른 트리로 바꾸는 일종의 휴리스틱 알고리즘이다.

const ParentA = () => {
	return (
      <ParentB>
      	<ChildA/>
      </ParentB>
    )
}

위와같은 컴포넌트 구조가 있다고 가정해보자. 이 컴포넌트를 stack reconciler를 이용해 렌더한다면 아래와 같은 구조를 띌 것이다.

[] //비어있던 스택
 
[ParentA] //ParentA가 들어옴. 남아있는 자식이 존재함.

[ParentA, ParentB] //ParentB가 들어왔지만, 남아있는 자식이 존재...

[ParentA, ParentB, ChildA] //ChildA가 끝이므로 실행!

[ParentA, ParentB] //ParentB도 실행!

[ParentA] //ParentA도 실행!

[] //비어있다.

이 동작원리는 단순명료하지만, 큰 문제점을 안고있다.

  • 한 컴포넌트의 작업 양이 방대할 경우
  • 많은 컴포넌트를 동시에 업데이트 할 경우

둘 다 사용자 관점에서 화면이 멈춰 보이게 될 수 있다. 예를들면 인풋컴포넌트를 입력하는 도중, 연산이 방대한 다른 컴포넌트stack reconciler에 의해 렌더된다. 그렇게되면 사용자가 입력하는 문자열이 상태를 업데이트하는 데 꽤 오랜시간이 걸릴 것이다.

물론 개발자는 requestAnimationFrames를 이용하여 다음 애니메이션 프레임에서 우선순위가 높은 함수를 호출하도록 하고, requestIdleCallback을 이용하여 유휴 시간동안 우선순위가 낮은 함수가 호출되도록 하게 작업을 조정할 수도 있다.
문제는 이러한 API를 사용하려면 렌더링 작업을 증분(점진)단위로 나누는 방법이 필요하다. 특히 스택에 의존하게 된다면 스택이 빌 때까지 작업을 계속 수행한다.

UI 렌더링을 최적화하기위해 호출 스택의 동작을 커스텀할 수 있다면 좋지 않을까? 호출 스택을 마음대로 중단하고, 스택 프레임을 수동으로 조작하면 훨씬 낫지 않을까? 가상의 스택을 만들어 볼까?

이를 해결하기 위해 나온 게 지금의 Fiber Reconciler다.

New Reconciler: Fiber

Stack reconciler의 취약점을 Fiber Reconciler는 어떻게 보완했을까?

  1. 특정 작업에 우선순위를 메길 수 있음
  2. 그로 인하여 concurrent하게 작업을 진행(일시중지, 재개)할 수 있게 됨
  3. 또한 Singly linked list자료구조를 활용하여 chlid, sibling, return...(next fiber)순으로 next fiber로 이동하게 만듬.

3번에서 칭하는 next fiberFiber 객체를 의미한다. 지금부터는 헷갈리니 FiberFiber conciler로 나눠서 부르기로 하겠다.

일단

What is Fiber?

Fiber는 객체라 하였다. 이는 Fiber reconciler에서 사용되는 하나의 작업 단위다. 동시에 컴포넌트의 인스턴스이기도하다.
React.createElement()로 생성 된 것이 컴포넌트의 인스턴스가 아니었나?

const Foo = ({ children }) => {
  return children;
};

console.log(<Foo>하이</Foo>);
console.log(React.createElement(Foo, {}, "하이"));

위 코드의 출력결과를 한번 살펴보자


JSX로 작성된 코드는 Babel로 트랜스파일링되어 React.createElement()에 인자로 넘겨진다.
따라서 컴포넌트의 인스턴스는 React.createElement()로 반환된 객체라고 볼 수 있다.

그런데 Fiber가 왜 컴포넌트의 인스턴스라고 하는걸까?

//react-dom/packages/src/client/ReactDOMRoot.js
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {

  const root = createContainer(
    ...
  );
  markContainerAsRoot(root.current, container);

  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;
  listenToAllSupportedEvents(rootContainerElement);

  return new ReactDOMRoot(root);
}

처음 index.js에서 호출하는 createRoot함수는 내부적으로 createContainer를 호출한다. 이 함수는...

// react-reconciler/packages/src/ReactFiberReconciler.new.js
export function createContainer(
	...
): OpaqueRoot {
  const hydrate = false;
  const initialChildren = null;
  return createFiberRoot(
	...
  );
}

이렇게 내부적으로 createFiberRoot라는 함수를 호출한다. 즉 컴포넌트의 인스턴스사실 2개가 생기는 셈이다.(실제 인스턴스와 Fiber)

What is Work?

Fiber conciler는 DOM처럼 Tree를 가진다. 이때 트리의 노드(Fiber)는 보통 컴포넌트 인스턴스(React Element)를 나타내지만, 다른 것도 나타낼 수 있다. (createFiberFromText()등의 메서드가 존재한다)

또한 Fiber는 하나의 작업 단위라 하였다. React는 이를 두 단계로 나눠 처리한다.

  • Render Phase : 작업단위를 처리한다. 비동기식으로 진행된다.
  • Commit Phase : 처리한 작업 단위를 실제 화면에 반영한다. 동기식으로 진행된다.

이전 TIL에서 봤던 렌더링 단계가 바로 Fiber에서 처리하는단계였다.

그래서 작업 단위란 무엇일까? 어떤 연산을 작업이라 부를까?

  • state changed
  • lifecycle function
  • changes in the DOM

위와 같은 연산들을 Fiber내부에서 작업(work)이라 부른다.

Fiber tree

Fiber conciler는 두개의 트리를 이용하여 작업을 처리한다.

Current트리는 변경할 수 없다. 실제 유저가 보고있는 화면이기 때문이다. 대신 WorkInProgress트리 내부를 바꾼 뒤, 참조를 변경한다. 이후 Current트리와 WorkInProgress트리를 교체한다.
이를 보통 더블 버퍼링이라 칭한다.


요약

  1. Virtual DOM은 가상돔이 아니다. 일종의 React가 만든 휴리스틱 트리 교체알고리즘이자 패턴이다.
  2. Fiber ArchitectureFiber라는 일종의 컴포넌트 인스턴스를 이용하여 만든 Reconciler이다.
  3. Fiber는 두번째 컴포넌트 인스턴스이다.

번외) Fiber와 double buffering

Fiber라는 개념은 사실 React에서 먼저 제시한 게 아니다.

Fiber란, 경량 스레드를 의미한다. 기존의 스레드와 무슨 차이가 있을까?

Fiber(Computer Science)

스레드는 메모리중 고유한 스택영역을 차지하고 있다. 그렇기에 많은 스레드를 생성하면 메모리가 부족할 우려가 있다.
그리고 I/O작업이 빈번하면 CPU의 유휴시간이 길어지게된다. (인터럽트시 대기하게됨)

이를 처리하기위해 나온 다양한 패턴 중 하나가 Fiber다.

  1. FiberOS의 스레드에 task(작업)으로서 마운트 되고 해당 스레드를 LOCK걸어 점유한다.
  2. I/O작업을 하게되면, unlock하여 스레드에서 언마운트되고 당시의 state를 저장한다 (스레드가 선점형으로 우선순위에 의해 CPU에서 interrupt시키는 반면, Fiber는 본인 스스로 unlock한다. 이를 협력적 멀티태스킹이라 칭함)
  3. 만약 unmount한스레드가 생기면 제어권이 Fiber 스케쥴러로 넘어가고, 이 스케쥴러는 다른 ready stateFiber를 해당 스레드에 할당, I/O작업이 끝난 Fiberunmount된 시점의 State를 가지고 다른 스레드에 재할당되어 나머지 작업을 이어간다.(concurrent)

React에서 사용했던 개념과 굉장히 유사하지 않은가? 결국 React도 무에서 유를 창조한 게 아닌 탄탄한 기본기를 바탕으로 기능을 쌓아올린다.

Double Buffer

Fiber Tree는 트리를 2개로 나누어 관리한다. 이를 이중 버퍼(double buffer)라 부르는데, 이 역시 본래 컴퓨터공학에 존재하는 기술이다.
정확히 말하자면, 컴퓨터 그래픽스쪽에서 자주 사용되는 용어다.(DB쪽에서도 사용한다는데...이부분은 잘 모르겠다.)


출처 : https://luckyresistor.me/2019/12/07/how-to-write-custom-snowflake-patterns-1/

사용자A버퍼에 저장된 데이터를 보고있으면, 개발자는 B버퍼를 업데이트한다. 업데이트가 완료되어 B버퍼를 보게된다면, 반대로 개발자는 A버퍼를 업데이트한다. 이를 스위칭이라 한다.

당연하게도 버퍼 하나를 사용하는 데 비해 두배의 메모리가 소모된다.


느낀점

Virtul DOMReconciler, 그리고 Fiber에 대해서 설명할 수 있을 정도로는 알게되었다.
리액트 코어 깃허브에서 내부 동작을 전부 볼 수 있으나 미약한 본인의 실력으로는 아직인듯하여...다음에 기회가 된다면 알아보겠습니다!

profile
모르는 것을 모른다고 하기

0개의 댓글