React, 어디까지 알고있는지..

·2024년 10월 4일
26

프론트엔드

목록 보기
12/12

시작하며

React는 현대 웹 개발의 핵심 기술 중 하나로 많은 개발자들이 애용하는 라이브러리입니다. "백전백승" 이라는 말처럼 React의 내부 작동 방식을 이해하면 그 활용도를 극대화할 수 있지 않을까? 하는 생각이 들었습니다. 기술을 더 잘 쓰기 위해....오늘도 GO!

React란

React is a JavaScript library for building user interfaces.
React는 사용자 인터페이스를 구축하기 위한 자바스크립트 라이브러리입니다.

React는 모노레포로 되어있습니다.

모노레포는 여러 프로젝트(패키지)를 단일 저장소에서 관리하는 접근 방식입니다. React 패키지는 Yarn Workspaces를 사용하여 모노레포를 구성하고 있네요.

package.json

  "workspaces": [
    "packages/*"
  ],

Packages

패키지들을 쭉 살펴보고 중요한 패키지들을 추려 설명해보겠습니다.

react

React의 코어 라이브러리입니다. useState, useEffect 같은 주요 훅과 컴포넌트 시스템, 이벤트 시스템이 포함되어 있어 React의 본질적인 동작을 담당합니다. 이 패키지가 없으면 React 자체가 동작할 수 없기 때문에 가장 중요한 레포입니다.

react-dom

이 패키지는 React 컴포넌트를 브라우저 DOM에 렌더링할 수 있게 해주는 패키지입니다. 서버 사이드 렌더링이나, CSR (Client-Side Rendering) 같은 기능도 이 패키지가 담당합니다. 프론트엔드에서 실제로 React를 사용할 때 필요한 필수 패키지입니다.

react-reconciler

React의 핵심 알고리즘인 "Fiber Reconciler"가 구현된 패키지입니다. React의 가상 DOM을 관리하고, 변경된 부분을 효율적으로 DOM에 업데이트하는 중요한 역할을 합니다. 성능 최적화의 핵심인 "동시성 모드(Concurrent Mode)"도 이곳에서 처리됩니다.

react-server 및 관련 패키지 (react-server-dom-*)

서버 사이드 렌더링을 지원하는 패키지들입니다. React 서버 측에서 실행할 때 필요한 기능을 제공하며, 특히 React 18 이후로 서버 컴포넌트(Server Components)를 도입하며 이 부분이 더욱 중요해졌습니다.

react-test-renderer

테스트를 위해 사용하는 패키지로, React 컴포넌트가 어떻게 렌더링되는지 시뮬레이션할 수 있습니다. 단위 테스트 작성 시 유용하게 쓰입니다.

scheduler

React는 다양한 이유로 작업을 비동기로 처리해야 할 때가 있는데 이러한 작업들은 Task라는 이름으로 관리되며 우선순위에 따라 실행 순서가 정해집니다.

이때 가장 적절한 시점에 Task를 실행할 수 있는 타이밍을 결정하는 것이 바로 scheduler의 역할입니다.
*즉 React는 비동기 작업을 직접 관리하지 않고 이를 더 잘 처리할 수 있는 전문가인 " scheduler" 에게 맡기는 것입니다.

이 패키지는 운영 환경에 따라 다르게 동작하며 각 환경에 맞춰 최적의 Task 실행 타이밍을 찾아냅니다.

event(legacy-events)

SyntheticEvent라는 명칭으로 내부적으로 개발된 이벤트 시스템.

react의 SyntheticEvent 시스템은 브라우저의 네이티브 이벤트(native event)를 감싸서(wrapping) 추상화하고, 추가적인 기능을 더한 이벤트 시스템입니다. 브라우저에서 발생하는 기본 이벤트를 React가 자체적으로 처리할 수 있도록 감싸는 방식입니다. 이를 통해 React는 다양한 브라우저에서 이벤트가 다르게 동작하는 문제를 통일된 방식으로 처리할 수 있습니다.

[ * SyntheticEvent ]

SyntheticEvent는 브라우저에서 발생하는 원래의 네이티브 이벤트를 감싸서 React가 추가적인 제어를 가능하게 합니다. 예를 들어 브라우저의 기본 동작을 방지하거나(event.preventDefault), 이벤트가 버블링되지 않도록 하는(event.stopPropagation) 등의 작업을 더 일관적이고 효율적으로 관리할 수 있습니다. React는 모든 이벤트를 직접 네이티브 브라우저 이벤트로 처리하는 것이 아니라 이 SyntheticEvent를 사용해 모든 이벤트를 표준화합니다. 이는 각 브라우저마다 이벤트 처리 방식이 다를 수 있기 때문에 React가 자체적으로 이벤트 처리를 제어할 수 있게 한다는 장점이 있습니다.

reconciler

React의 가상 DOM(Virtual DOM)을 실제 DOM과 동기화하는 과정을 관리하는 중요한 패키지입니다. 이 패키지에 구현된 알고리즘이 바로 "Reconciliation"이며, 이 과정 덕분에 React가 효율적으로 UI를 업데이트할 수 있습니다.

Virtual DOM ?

current 트리와 workInProgress 트리의 최상단 노드를 current, workInProgress로 명시하였지만 더 자세히는 "Host root" 라는 이름의 노드라고 합니다. 출처

Virtual DOM은 React가 사용하는 메모리 내에서의 DOM 표현입니다. 실제 DOM의 가벼운 복사본으로 JavaScript 객체로 구현되어 있습니다.

Virtual DOM 을 구성하는 요소들

ReactElement

ReactElement는 React 컴포넌트의 정보를 담고 있는 객체입니다. 참고로 React Element는 React의 메모리 내 표현이며 UI 구성의 기본 단위입니다. 실제 DOM 요소가 아닙니다!

사용자가 JSX를 작성할 때, 실제로 반환되는 것은 JSX가 아니라 이 ReactElement입니다. 이 객체는 컴포넌트의 타입, props, children 등의 정보를 포함하고 있습니다.

예를 들어,

<MyComponent prop1="value" />

와 같은 JSX는 ReactElement로 변환되어 React가 이 요소를 어떻게 렌더링할지를 알 수 있게 합니다.

Fiber

React v16에서 리액트의 핵심 알고리즘을 재구성한 새 재조정(Reconciliation) 엔진입니다. Fiber는 > 컴포넌트의 상태, 훅, 라이프 사이클, 그리고 업데이트 정보 < 를 포함합니다. 이를 통해 React는 각 컴포넌트의 변경 사항을 추적하고 관리할 수 있습니다. Fiber 구조는 더블 버퍼링 형태를 사용하여 이전 상태와 현재 상태를 동시에 유지하고 변경 사항을 효율적으로 처리할 수 있도록 합니다.

current

현재 렌더링이 완료된 상태의 트리를 나타냅니다. 즉,사용자에게 보여지는 실제 DOM을 업데이트하기 위해 사용되는 트리입니다.

workInProgress

현재 업데이트 중인 상태의 트리를 나타냅니다. 새로운 변경 사항이 적용되고 있는 트리로, React는 이 트리를 사용하여 새로운 렌더링 결과를 준비합니다.

더블 버퍼링의 작동 방식

두 개의 버퍼를 사용하여 부드럽고 효율적인 렌더링을 가능하게

렌더링 과정

  1. 여기서 하나는 current 트리로, 이미 DOM에 마운트된 상태입니다.
  2. 다른 하나는 workInProgress 트리로, 현재 렌더링 과정에서 업데이트 작업이 진행 중인 상태입니다.

이 두 트리는 각각의 역할을 수행하며, workInProgress 트리는 나중에 Commit 단계에서 current 트리로 업데이트됩니다. 이 구조를 통해 리액트는 사용자 경험을 최우선으로 고려하며 필요한 경우 작업을 중단하고 다시 시작할 수 있는 유연성을 갖추고 있습니다. 예를 들어 사용자가 인터페이스와 상호작용할 때 리액트는 필요에 따라 작업 우선순위에 따라 신속하게 작업을 조정할 수 있습니다.

workInProgress 트리는 current 트리의 복사본으로 만들어집니다. 리액트는 현재 상태의 DOM 구조를 복사하여 새로운 작업을 위한 workInProgress를 생성하는데, 이 복사본은 서로 다른 상태에서 작업이 진행되도록 도와줍니다. 이 두 트리는 서로를 alternate로 참조하여, 업데이트가 필요할 때 적절히 교체할 수 있습니다.

이제 VDOM에서의 fiber 노드 구조에 대해 이야기해보면 fiber는 컴포넌트와 그 자식들을 나타내는 객체입니다. 각 fiber 노드는 다음과 같은 관계를 가지고 있습니다.

First Child: 각 fiber 노드는 오직 첫 번째 자식 노드만 직접 참조합니다. 한 부모 노드가 여러 자식을 가질 경우 가장 첫 번째 자식만을 직접적으로 연결합니다.
Sibling: 나머지 자식 노드는 형제(sibling) 관계로 연결됩니다. 두 번째 자식부터는 첫 번째 자식의 sibling로 연결되어 있어 서로를 참조할 수 있습니다.
Parent Reference: 모든 자식 노드는 자신을 포함하는 부모 노드를 참조합니다. 이를 통해 자식 노드는 자신의 부모가 누구인지 알 수 있으며, 이를 return을 통해 참조합니다.

*파이버의 자식은 항상 첫 번째 자식의 참조로 구성됩니다..리액트 컴포넌트의 root 요소가 무조건 하나여야 하는 이유도 이 때문입니다.

Algorithm History

리액트의 Diffing 알고리즘은 사용자 인터페이스(UI)를 업데이트하는 데 있어 매우 중요한 역할을 합니다. 이 알고리즘의 핵심은 가상 DOM과 실제 DOM을 비교하여 변경된 부분만 업데이트하는 것인데요, 이를 통해 리액트는 빠르고 효율적으로 UI를 관리할 수 있습니다.

Diffing

Dirty 체크: 상태 변경 감지

리액트에서 컴포넌트의 상태(state)가 변경되면 해당 컴포넌트에 dirty 표시를 하고 이를 배치(batch)에 추가합니다. 이렇게 되면 리액트는 나중에 이 컴포넌트를 업데이트할 준비를 하게 됩니다. 특히, zustand 와 같은 상태 관리 도구를 사용하게 되면 개별 컴포넌트가 아닌 루트 노드에 dirty 마크가 찍히게 되죠.

O(n)에 이러한 돔 트리를 순회하면서 이런 일을 할 수 있는 이유는 heuristics알고리즘을 이용한 순회덕입니다. 비용이 너무 높으니 중요하지 않은 정보들은 고려하지 말고 중요한 것들만 고려해서 최선의 값을 찾아내자는 대충 때려맞추는 방법입니다.

그렇다면 리액트는 순회과정에서 어떠한 값들을 중요하다고 판단하고 있을까요?

처음은 같은 계층에서의 비교입니다. 예를 들어 여러 개의 거래 기록이 있다고 가정해보겠습니다. 이 거래 기록들은 보통 하나의 컴포넌트로 만들어지고 map이나 배열 메서드를 사용해 반복적으로 렌더링됩니다.

이때, 리액트는 거래 기록이 동일한 계층에 위치할 것이라는 전제를 가지고 있습니다. 즉, 한 거래 기록이 다른 거래 기록보다 더 위나 아래에 배치될 일은 거의 없다는 것이죠. 따라서 리액트는 같은 계층에 있는 컴포넌트끼리만 비교하여 변경사항을 확인합니다. 이렇게 하면 불필요한 비교를 줄이고 성능을 향상시킬 수 있습니다.

뭔가 생각나지 않나요? (.......Key)

결국, 리액트는 같은 계층에 있는 컴포넌트들 간의 변화를 더 효과적으로 관리하기 위해 이러한 접근 방식을 채택하고 있습니다.

결국 새로운 구조의 도입

리액트 16버전부터는 이러한 Diffing(Reconciliation) 알고리즘을 총체적으로 관리하기 위해 Fiber라는 새로운 구조를 도입했습니다. Fiber는 복잡한 UI 업데이트를 더욱 효율적으로 처리할 수 있도록 개선된 구조입니다. 이를 통해 리액트는 사용자에게 매끄러운 경험을 제공하게 되죠.

Fiber Instance에 대해서 더 자세히 알아보겠습니다.

We're about to discuss the heart of React Fiber's architecture. Fibers are a much lower-level abstraction than application developers typically think about. If you find yourself frustrated in your attempts to understand it, don't feel discouraged. Keep trying and it will eventually make sense. (When you do finally get it, please suggest how to improve this section.)

파이버는 애플리케이션 개발자가 일반적으로 생각하는 것보다 훨씬 낮은 수준의 추상화입니다. 이를 이해하려고 시도하다가 좌절감을 느끼더라도 낙심하지 마세요. (라고 저를 위로하네요)

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

.
.
.

tag

  • 리액트 파이버는 1:1관계를 가집니다.
    DOM이든 컴포넌트든 무조건 1:1의 관계를 지니며 이 유형은 태그 속성 내의 this.tag에 저장되고 이 type에서는 fuctional component, class component 등 우리가 아는 여러 유형들의 노드들에 대해서 번호로 관리합니다.

1:1 관계란?

React 컴포넌트 트리는 실제로 사용자가 작성하는 컴포넌트 계층 구조입니다. 각 컴포넌트는 화면에 나타나는 UI의 한 부분을 담당하죠. React의 Fiber 구조에서는 이 컴포넌트 트리의 각 컴포넌트에 대응하는 Fiber 객체가 생성되며, 이를 통해 React는 컴포넌트의 상태 및 업데이트 정보를 관리합니다. 즉, 각 컴포넌트는 하나의 Fiber와 연결되며, 컴포넌트 트리와 Fiber 트리가 1:1 대응 관계를 이룹니다.

예를 들어, 아래와 같은 컴포넌트 트리가 있다고 가정해봅시다.

<App>
  <Header />
  <Content />
</App>

여기에서

<App>, <Header>, <Content>

는 각각 자신만의 Fiber 객체를 가집니다.

각 Fiber 객체는 해당 컴포넌트의 업데이트 시기를 추적하고, 어떤 자식 컴포넌트가 있는지, 어떤 상태나 props가 있는지 등의 정보를 담고 있습니다. 이를 통해 React는 필요한 부분만 효율적으로 업데이트할 수 있죠.

[ Key ]
key는 리스트나 배열을 렌더링할 때, 각 컴포넌트를 고유하게 식별하기 위해 사용됩니다. 이를 통해 React는 컴포넌트가 변경되었는지, 추가되었는지, 혹은 삭제되었는지를 더 효율적으로 감지할 수 있습니다.

[ elementType ]
이 속성은 렌더링할 JSX 요소의 타입을 나타냅니다. 예를 들어, JSX에서

<div>

로 나타나는 경우 elementType은 "div"가 될 것입니다. 클래스나 함수 컴포넌트의 경우 해당 클래스나 함수가 여기에 할당됩니다.

[ type ]
elementType과 유사하지만 조금 다르게 동작하는데, elementType은 JSX에서 정의된 타입을 나타내고 type은 실제로 그 요소가 무엇으로 처리되는지를 나타냅니다. 예를 들어 forwardRef 같은 고차 컴포넌트에서는 elementType은 고차 컴포넌트일 수 있지만 type은 내부의 실제 컴포넌트일 수 있습니다.

[ stateNode ]
이 속성은 현재 Fiber 노드에 연결된 실제 DOM 노드 또는 클래스 컴포넌트 인스턴스를 참조합니다. 이를 통해 React는 Fiber 트리와 실제 DOM 트리 사이의 연결을 유지하고 DOM을 조작할 때 적절한 위치에 접근할 수 있습니다.

Fiber 주요 기능

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

Fiber Algorithm 파이버 알고리즘

Fiber 는 reconciliation 작업을 2단계로 나눠서 실행합니다.

[ Phase 1 Render ] — 실제로 두 fiber 트리를 비교하고 변경된 이펙트들을 수집하는 작업을 합니다. 이 단계는 concurrent 하게 일시 정지되고 재가동될 수 있습니다. 리액트 scheduler로 인해 허용되는 시간 동안 작업하고 수시로 멈춰서 메인 스레드에 user input, animation 같은 더 급한 작업이 있는 확인 해가며 실행되기 때문에 아무리 트리가 커도 비교 작업이 메인 스레드를 막을 걱정이 없습니다. Phase 1의 목적은 이펙트 정보를 포함한 새로운 fiber 트리를 만들어내는 것입니다.

[ Phase 2 Commit ] — Phase 1에서 만든 트리에 표시된 이펙트들을 모아 실제 DOM에 반영하는 작업을 합니다. 이 단계는 synchronous하게 한 타에 이루어지기 때문에 일시 정지하거나 취소할 수 없습니다.

Reconciliation

React가 변경해야 할 부분을 결정하기 위해 한 트리를 다른 트리와 비교하는 데 사용하는 알고리즘입니다. React API의 핵심 아이디어는 업데이트를 통해 전체 앱을 다시 렌더링하도록 생각하는 것입니다. 이를 통해 개발자는 앱을 특정 상태에서 다른 상태(A에서 B로, B에서 C로, C에서 A로 등)로 효율적으로 전환하는 방법에 대해서는 걱정하지 않고, 선언적으로 개발할 수 있습니다.

Commit

변경 사항을 결정한 후, 리액트는 커밋 과정을 거칩니다. 이 과정에서는 이전에 결정된 변경 사항이 실제 DOM에 적용됩니다. 여기서 중요한 점은 리액트가 실제 DOM을 직접 조작하는 것이 아니라, 먼저 Virtual DOM에서 모든 변경을 처리하고 마지막으로 필요한 변경만을 실제 DOM에 반영한다는 것입니다. 이로 인해 불필요한 렌더링을 최소화하고 성능을 극대화할 수 있습니다.

커밋 과정이 끝나면 리액트는 변경된 트리를 기반으로 새로운 Virtual DOM을 생성하고 이를 브라우저의 DOM과 동기화합니다.

레퍼런스

https://goidle.github.io/react/in-depth-react-preview/#eventlegacy-events
https://velog.io/@jangws/React-Fiber
https://github.com/acdlite/react-fiber-architecture
https://d2.naver.com/helloworld/2690975
https://blog.mathpresso.com/react-deep-dive-fiber-88860f6edbd0
https://velog.io/@naamoonoo/%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%ED%95%A0%EA%B9%8C-Diffing-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0
https://velog.io/@alsgud8311/React-Fiber-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C

+) 내용을 천천히 보충해 나갈 예정입니다.

profile
My Island

2개의 댓글

comment-user-thumbnail
2024년 10월 15일

정말 좋은 글입니다 추천합니다

1개의 답글