# 2024. 07.
# 07. 01.
# React Reconcile
번역하면 재 조정이라는 뜻인데,
React Render 트리 (가상 돔)를 초기화 또는 업데이트 하여 실제 돔이랑 동기화 해주는 과정이다.
# 07. 04.
# NextJS에서 서버 컴포넌트를 렌더링하는 방법
# 기존 렌더링
- 접속시 유저는 빈 화면의 index.html을 받는다.
- index.html 내에 링크 되어있는 번들 파일을 요청한다.
- react가 실행된다.
- reconcile (재조정) 과정이 진행된다.
- DOM에 렌더링 한다.
# NextJS 렌더링
NextJS에서의 렌더링은 청크 단위로 이루어진다.
이 때, 청크를 나누는 기준은 Route Segment와 Suspense Boundaries이다.
분리된 청크는 NextJS에서 React API를 사용하여 서버에서 RSC(React Server Component) Payload로 생성해준다.
RSC Payload란?
React Server Component를 그리기 위한 바이너리 형태의 트리. 이 객체는 클라이언트에서 브라우저에 DOM을 업데이트하기 위해서 사용된다.
RSC Payload의 구성요소는 다음과 같다.
- Server Component의 렌더링된 결과
- Client Component의 Javascript 파일의 위치 (Placeholder라고도 부르며, 렌더링 된 결과는 포함되지 않는다.)
- Server Component에서 Client Component로 전달하는 Props 정보
그 후, NextJS는 RSC Payload와 Client Component Javascript Instructions를 이용해 미리보기 HTML을 렌더링 한다.
Client Component Javascript란?
useState나 onClick와 같이 Client Component에서 사용되는 정보를 포함하는 객체이다.
RSC Payload 정보는 인라인 JS 태그 내에 위치하며, Client Component Javascript Instructions는 useState의 기본값 등 초기 렌더링에 필요한 값들만 일부 참조한다.
이때 까지가 서버에서의 작업이었고,
생성된 HTML 파일을 클라이언트가 전달받았을 때,
브라우저에서 HTML 내용을 즉시 렌더링하여 사용자에게 보여준다.
그 후, 인라인 JS 태그 내에 있는 RSC Payload를 통해 재조정 과정을 거친다.
Reconcile (재조정) 이란?
Server Component와 Client Component의 정보를 결합하여 Render Tree를 생성하고, 이 Render Tree를 이용해 VDOM을 구성한 후 DOM과 Sync를 맞추는 과정이다.
마지막으로 서버에서 생성된 Client Component Javascript Instructions를 요청하여 hydrate 과정으로 마무리된다.
# 좀 더 공부가 필요한 부분
Reconcile 과정과 Hydrate 과정 상세화
# 07. 06.
# Javascript Runtime 구성 요소 및 관리 주체
Call Stack
: JavaScript 엔진(V8 등)에서 관리.Task Queue
: 브라우저나 Node.js와 같은 JavaScript 런타임 환경에서 관리.Event Loop
: 브라우저나 Node.js와 같은 JavaScript 런타임 환경에서 관리.
# 07. 08.
# React의 Fiber 아키텍처
React.Element가 확장되는 형태로 Hook 관련 정보가 붙는다.
# 07. 13.
# React에서 Work란?
재조정을 하기 위해 Schedule에서 처리하는 단위.
# React에서 dispatchAction 이란?
재조정을 하기 위해 Schedule에 Work를 넣어주는 함수
# 07. 14.
# React에서 Idle 상태와 Render Phase 상태의 기준
Render Phase 진입점이 setState의 마지막 단계
# React에서 expirationTime
Scheduler가 Work에게 처리 우선순위를 알려주는 기준이 expirationTime. 여기에 할당되는 값은 이벤트 발생 시점 시간
expirationTime이 클수록 우선순위가 높아짐 (expirationTime은 MAGIC_NUMBER에 performance.now()를 뺀값이어서 시간이 흐를 수록 작아짐. 즉, 나중에 발생한 이벤트일수록 작어지고, 먼저 발생한 이벤트일수록 커짐)
# ReactFiberHooks의 역할
React의 상태 관리는 react-reconcile 패키지에서 담당한다. React는 VDOM에서 노드를 관리하기 위해 Fiber 형태로 관리하고 있으며 이는 React Element에서 확장된 정보(상태 등)를 가진 객체이다.
ReactFiberHooks는 Fiber 객체에 Hook에 관련된 정보를 마운트 시켜주는 역할을 한다. react-reconcile의 ReactFiber Github 링크 (opens new window)
Fiber 객체에는 memoizedState 프로퍼티가 존재하는데 renderWithHooks 함수를 통해 momoizedState에 Hook 객체를 할당해준다.
Hook 객체는 LinkedList 형태로 존재하며 하나의 Hook 객체는 next로 다른 Hook 객체를 참조할 수 있다.
그래서 실제 memoizedState에 할당되는 값은 첫번째 Hook 객체이며 이는 해당 Fiber에서 사용하는 모든 Hooks와 LinkedList 형태로 연결되어있다.
ReactCurrentDispatcher.current -> HookDispatcherOnMount -> mountState -> mountWorkInProgressHook -> currentlyRenderingFiber.memoizedState에 Hook 객체를 LinkedList로 mount
mountWorkInProgressHook은 Fiber에 Hook을 LinkedList 형태로 연결시켜주는 역할
# 07. 16.
# BuildTime Css와 RunTime Css에 대한 고찰
기술 스택을 정하는 도중 Build Time Css와 Run Time Css에 대한 의문점이 들었다.
나는 이 둘의 차이점이 CSS가 적용된 상태로 오느냐 아니면 브라우저에서 JS 런타임에서 CSS를 만들어주냐의 차이점인줄 알았다. 반은 맞고 반은 틀렸다.
초기 HTML 파일을 응답받은 상태에서 브라우저에서 Preview를 보면 뼈대만 있는데 (CSS 적용 X) 여기서 부터 이상함을 감지했다.
어? 왜 스타일이 적용 안되어있지?
라고 생각했지만 이후 Critical Rendering Path를 생각해보면 당연한 결과였다.
브라우저는 HTML을 전부 해석하기 전까진 Paint룰 안해준다.
주요 차이점은 다음과 같다.
BuildTime CSS
- link 태그에 있는 CSS 요청
- CSS 파일 다운로드 및 파싱
- CSSOM 생성
- DOM + CSSOM을 결합하여 Render Tree 생성
- Paint
Runtime CSS
- script 태그 해석
- Javascript에 의한 동적 스타일 변경
- CSSOM 업데이트
- Render Tree 재계산 및 업데이트
- Paint
- 이후 업데이트가 있을때마다 2 ~ 6 과정 반복
추가로 JS에서 Runtime으로 CSS를 생성할 때 렌더링 엔진이 JS 엔진에게 제어권을 위임하기 때문에 렌더링 과정이 수행되지 않기에 더 오래 걸린다는 점도 있다.
정리하자면 BuildTime과 RunTime 모두 초기 CSS가 Paint 전에 반영되지만 속도면에서 차이가 있다.
브라우저 렌더링 방식의 중요성을 깨달았다.
# 07. 19.
# React Fiber Architecture
Reconcilation 단계에서 React 16 이전 Stack Reconcilation 구조에서 Fiber Reconcilation으로 바뀐 아키텍처이다.
# Stack Reconcilation의 단점
React는 변화를 탐지할때 상태가 변한 컴포넌트들을 모두 CallStack에서 render 메소드를 호출하여 반영한다. 이는 브라우저의 프레임 드랍을 유발할 수 있다.
브라우저는 60fps(초당 60프레임)으로 화면을 갱신하려고 한다. 이는 각 프레임 당 약 16.67ms(1000ms / 60)가 걸린다.
만약 16.67ms동안 콜스택을 비우지 못하면 브라우저가 화면을 갱신하지 못하기 때문에 프레임 드랍이 발생하는 원리이다.
즉, 대규모 렌더링이 발생할 때 React가 16.67ms내로 렌더링하지 못하면 프레임 드랍이 발생하는 문제가 있었다.
# Fiber Architecture
Fiber는 React가 각각의 렌더링 작업에 대해서 스케쥴링을 가능하게 하는 아키텍쳐이고 다음과 같은 목표를 가지고 있다.
- 작업을 중단하고 나중에 다시 진행
- 작업별 우선순위 지정
- 이전에 완료된 작업 재사용
- 더 이상 필요하지 않은 작업 중단
출처 - React Fiber Architecture (opens new window)
즉, Fiber는 각각의 작업을 최대한 효율적으로 처리하고자 설계된 구조다.
그러면 각각의 스택 프레임에 우선순위를 부여한다는 의미인데 JS 환경을 이해했을때 "어떻게 가능하지?" 라는 의문이 생긴다. 이를 해결하기 위해 React는 workLoop (opens new window)라는 가상의 스케줄러를 만들어서 렌더링 작업을 스케줄링한다.
// react/packages/scheduler/src/forks/Scheduler.js
function workLoop(...){
while(...){
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
}
}
_workLoop는 while문으로 구성되어있는데 할당된 시간 동안만 수행된다. _
workLoop는 하나의 스택 프레임으로 동작하며 호스트(브라우저)의 렌더링 주기에 맞춰서 생명주기가 정해지며, 대게 매우 짧은 생명주기 시간을 갖고있다. ex) 16.67ms
이전에 Stack 구조에서는 콜스택에 일괄로 렌더링 작업을 진행했다면, Fiber 구조는 workLoop을 콜스택에 넣고 생명주기 시간동안 while문을 반복하여 렌더링 작업을 처리하고 생명주기 시간이 끝나거나, 더이상 처리할 작업이 없을 때 제어권을 브라우저에게 반납한다.
어? 그러면 생명주기 시간동안 렌더링 작업을 끝내지 못하면요?
이는 브라우저에서 제공하는 requestIdleCallback
함수를 활용한다. 이 함수는 브라우저가 중요한 작업(렌더링, 이벤트 처리 등)을 모두 마치고 여유가 있을 때 호출해주는 콜백이다.
React는 이를 활용하여 브라우저가 여유로울때마다 workLoop를 호출하여 렌더링 작업을 수행한다.
네? 브라우저가 여유로울때만 렌더링 작업을 할 수 있나요? 우선순위가 높은 작업은요?
좀 더 알아봐야됨
Fiber는 우선순위에 따라 처리하는 작업의 방식도 다르다. 보통은 requestIdleCallback
가 호출되는 시점에 처리하는데 우선순위가 높은 작업은 setTimeout
으로 처리된다.
# 정리
즉, Fiber 구조는 React의 재조정(reconciliation) 과정을 작은 단위의 작업으로 나누어 중단과 재개가 가능하게 만들고, 작업 우선순위를 지정할 수 있게 하여 UI의 반응성과 성능을 향상시키는 내부 구현 아키텍처이다.
# 07. 24.
# React 비즈니스 로직 분리 자료
프론트엔드에서 비즈니스 로직과 뷰 로직 분리하기 (feat. MVI 아키텍쳐) (opens new window)
프론트엔드 아키텍처: 비지니스 로직과 사례 (opens new window)
프론트엔드 아키텍처: API 요청 관리 (opens new window)
비지니스로직 분리 질문드려요! (opens new window)
# 07. 26.
# React 실행 흐름
# 초기화
진입점 (예: index.js)
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
packages/react/index.js
- React 코어 API를 내보냅니다.
packages/react-dom/index.js
- ReactDOM API를 내보냅니다.
packages/react-dom/src/client/ReactDOM.js
createRoot()
: 루트 컨테이너를 생성합니다.
packages/react-dom/src/client/ReactDOMRoot.js
root.render()
: 애플리케이션 렌더링을 시작합니다.
packages/react-reconciler/src/ReactFiberReconciler.js
updateContainer()
: 컨테이너 업데이트를 시작합니다.
packages/react-reconciler/src/ReactFiberWorkLoop.js
scheduleUpdateOnFiber()
: Fiber 업데이트를 스케줄링합니다.performSyncWorkOnRoot()
또는performConcurrentWorkOnRoot()
packages/react-reconciler/src/ReactFiberBeginWork.js
beginWork()
: 각 Fiber 노드 작업을 시작합니다.
packages/react-reconciler/src/ReactFiberHooks.js
renderWithHooks()
: 함수 컴포넌트 렌더링 및 훅 처리
packages/react-reconciler/src/ReactFiberClassComponent.js
- 클래스 컴포넌트 처리 (해당하는 경우)
packages/react-reconciler/src/ReactChildFiber.js
reconcileChildFibers()
: 자식 요소 재조정
packages/react-reconciler/src/ReactFiberCompleteWork.js
completeWork()
: 각 Fiber 노드 작업 완료
packages/react-reconciler/src/ReactFiberCommitWork.js
commitRoot()
: 변경사항 DOM에 적용
packages/react-dom/src/client/ReactDOMHostConfig.js
- DOM 조작 함수들 (예:
appendChild()
,insertBefore()
)
- DOM 조작 함수들 (예:
# 이벤트 처리
packages/react-dom/src/events/DOMPluginEventSystem.js
- 이벤트 리스너 설정 및 처리
# 상태 업데이트 시
packages/react-reconciler/src/ReactUpdateQueue.js
enqueueUpdate()
: 업데이트 큐에 추가
# 렌더링 완료 후
packages/react-reconciler/src/ReactFiberWorkLoop.js
finishConcurrentRender()
또는commitRootImpl()
# 클린업 및 부수 효과
packages/react-reconciler/src/ReactFiberCommitWork.js
commitPassiveUnmountEffects()
,commitPassiveMountEffects()
# 07. 28.
# React Reconcile 과정 beginWork, completeWork
beginwork -> updateFunctionComponent 여기서 workInProgress.child를 반환해줌 이 후 completeWork를 통해 sibling 탐색 후 존재하면 workInProgress를 sibling으로 변경 (beginWork 반복)
# 07. 29.
# React beginWork에서 만들어지는 Component
beginWork에서 만들어지는 Class 혹은 Function 컴포넌트의 tag는 IndeterminateComponent
이다.
이후 mountIndeterminateComponent()
함수를 통해 mount가 된 상태에서는 tag가 바뀐다.
# 07. 30.
# 리액트 인스턴스를 생성하고 렌더링하는 과정 feat. 초기 마운트
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
React는 여러 호스트(앱, 브라우저)에서 사용할 수 있습니다. 그 중에서 ReactDOM은 React를 브라우저에서 그려주는 역할을 담당합니다. createRoot 메소드를 통해 브라우저에서 연결된 Container (실제 DOM)을 연결하고 FiberRootNode와 HostRoot를 생성하고 render 메소드를 통해 workInProgress에서 App 컴포넌트를 생성해서 Render, Commit Phase를 거쳐 Container에 페인팅합니다.