- React
[리액트 19] 6편. React 우선순위 관리
React 우선순위 관리 React 내부에서는 우선순위 관리를 위한 타입은 기능에 따라 Lane 우선순위, Scheduler 우선순위, Event 우선순위 3가지로 나뉩니다. fiber 우선순위(LanePriority) : React-reconciler 패키지에 존재, ReactFiberLane.js 파일 스케줄링 운선순위(SchedulerPriority): Scheduler 패키지에 존재, SchedulerPriorities.js 파일 Event 우선순위(ReactEventPriorities): React-reconciler 패키지에 존재, ReactEventPriorities.js 파일 React 팀은 지속적으로 효율적인 렌더링 구현에 노력해왔으며, 그 중 2개의 매우 유명한 발표가 있습니다 2017년 Lin Clark이 fiber 아키텍처와 중단 가능한 렌더링을 소개했습니다. 2018년 Dan이 JSConf이 시간 분할(time slicing)과 비동기 렌더링(suspense) 등의 특성을 더 자세히 소개했습니다. 발표에서 보여준 중단 가능한 렌더링, 시간 분할(time slicing), 비동기 렌더링(suspense) 등의 특성들이 소스코드에서 구현되는 것은 모두 우선순위 관리에 의존합니다. scheduling 함수 Lane export const TotalLanes = 31; export const NoLanes: Lanes = // 0b0000000000000000000000000000000; export const NoLane: Lane = // 0b0000000000000000000000000000000; export const SyncHydrationLane: Lane = // 0b0000000000000000000000000000001; export const SyncLane: Lane = // 0b0000000000000000000000000000010; export const SyncLaneIndex: number = 1; export const InputContinuousHydrationLane: Lane = // 0b0000000000000000000000000000100; export const InputContinuousLane: Lane = // 0b0000000000000000000000000001000; export const DefaultHydrationLane: Lane = // 0b0000000000000000000000000010000; export const DefaultLane: Lane = // 0b0000000000000000000000000100000; export const SyncUpdateLanes: Lane = SyncLane | InputContinuousLane | DefaultLane; export const GestureLane: Lane = // 0b0000000000000000000000001000000; const TransitionHydrationLane: Lane = // 0b0000000000000000000000010000000; const TransitionLanes: Lanes = // 0b0000000001111111111111100000000; const TransitionLane1: Lane = // 0b0000000000000000000000100000000; const TransitionLane2: Lane = // 0b0000000000000000000001000000000; const TransitionLane3: Lane = // 0b0000000000000000000010000000000; const TransitionLane4: Lane = // 0b0000000000000000000100000000000; const TransitionLane5: Lane = // 0b0000000000000000001000000000000; const TransitionLane6: Lane = // 0b0000000000000000010000000000000; const TransitionLane7: Lane = // 0b0000000000000000100000000000000; const TransitionLane8: Lane = // 0b0000000000000001000000000000000; const TransitionLane9: Lane = // 0b0000000000000010000000000000000; const TransitionLane10: Lane = // 0b0000000000000100000000000000000; const TransitionLane11: Lane = // 0b0000000000001000000000000000000; const TransitionLane12: Lane = // 0b0000000000010000000000000000000; const TransitionLane13: Lane = // 0b0000000000100000000000000000000; const TransitionLane14: Lane = // 0b0000000001000000000000000000000; const RetryLanes: Lanes = // 0b0000011110000000000000000000000; const RetryLane1: Lane = // 0b0000000010000000000000000000000; const RetryLane2: Lane = // 0b0000000100000000000000000000000; const RetryLane3: Lane = // 0b0000001000000000000000000000000; const RetryLane4: Lane = // 0b0000010000000000000000000000000; export const SomeRetryLane: Lane = RetryLane1; export const SelectiveHydrationLane: Lane = // 0b0000100000000000000000000000000; const NonIdleLanes: Lanes = // 0b0000111111111111111111111111111; export const IdleHydrationLane: Lane = // 0b0001000000000000000000000000000; export const IdleLane: Lane = // 0b0010000000000000000000000000000; export const OffscreenLane: Lane = // 0b0100000000000000000000000000000; export const DeferredLane: Lane = // 0b1000000000000000000000000000000; // Any lane that might schedule an update. This is used to detect infinite // update loops, so it doesn't include hydration lanes or retries. export const UpdateLanes: Lanes = SyncLane | InputContinuousLane | DefaultLane | TransitionLanes; export const HydrationLanes = SyncHydrationLane | InputContinuousHydrationLane | DefaultHydrationLane | TransitionHydrationLane | SelectiveHydrationLane | IdleHydrationLane; let nextTransitionLane: Lane = TransitionLane1; let nextRetryLane: Lane = RetryLane1; Scheduler Prioirity export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5; export const NoPriority = 0; export const ImmediatePriority = 1; export const UserBlockingPriority = 2; export const NormalPriority = 3; export const LowPriority = 4; export const IdlePriority = 5; Event Priority DOM 이벤트에 따라 처리할 우선순위를 Lane 모델에 따라 처리되도록 변경. Scheduler의 Priority로 변경하는 것도 EventPriority가 기준. ReactDOMEventListener.js (getEventPriority 함수, resolveUpdatePriority 함수) export opaque type EventPriority = Lane; export const NoEventPriority: EventPriority = NoLane; export const DiscreteEventPriority: EventPriority = SyncLane; export const ContinuousEventPriority: EventPriority = InputContinuousLane; export const DefaultEventPriority: EventPriority = DefaultLane; export const IdleEventPriority: EventPriority = IdleLane; export function eventPriorityToLane(updatePriority: EventPriority): Lane { return updatePriority; } export function lanesToEventPriority(lanes: Lanes): EventPriority { const lane = getHighestPriorityLane(lanes); if (!isHigherEventPriority(DiscreteEventPriority, lane)) { return DiscreteEventPriority; } if (!isHigherEventPriority(ContinuousEventPriority, lane)) { return ContinuousEventPriority; } if (includesNonIdleWork(lane)) { return DefaultEventPriority; } return IdleEventPriority; } 2.1 LanePriority 2.2 SchedulePriority 2.3 EventPriority Event 별로 Lane 모델에 따라 알맞은 우선순위로 처리될 수 있도록 하는 우선순위. 또한 EventPriority 기준에 따라서 SchedulePriority도 함께 결정되어 스케줄링.
2025-05-22006✏️ 작성중... - React
[리액트 19] 5편. React의 시작 과정
React의 시작 과정 4편에서는 React 흐름의 핵심인 reconciler의 동작 흐름을 4단계로 정리해봤습니다. 5편에서는 React 애플리케이션의 시작 과정을 설명하고자 합니다. 이 과정은 4편의 입력 단계에 해당하며 react-dom 패키지에 대한 설명입니다. 우선 본격적인 분석에 앞서 React 앱의 시작 방식에 대해 설명드리겠습니다. React 19버전 기준으로는 크게 2가지(Legacy, Concurrent) 입니다. React 19 모드들 소스코드 1.1 Legacy 모드 (React 16~18 버전에서 사용) React 19.1에서 render 함수 소스코드 ReactDOM.render(, document.getElementById('root'), (dom) => {}); // 콜백 함수 지원, 매개변수로는 DOM 객체가 전달됨 1.2 Concurrent 모드 (React 18부터 본격 사용) React 19 부터는 기본으로 사용되는 모드입니다. 여러 컴포넌트들을 Concurrent하게 렌더링할 수 있습니다. import { createRoot } from 'react-dom/client' // const reactDOMRoot = createRoot(document.getElementById('root')); // reactDOMRoot.render(); // 콜백 미지원 진입 함수 render을 하기 전까지는 DOM 요소와 (리액트 엘리먼트, createElement로 변환된 것) 사이에는 아무런 연결관계가 존재하지 않습니다. 그리고 React의 시작은 react-dom 패키지에서 시작되어 내부적으로 react-reconciler 패키지를 호출합니다. 2.1 전역 객체 생성 모드에 관계없이 React는 시작 시 3가지의 전역 객체를 생성합니다. ReactDOMRoot (createRoot 함수 , RootType 타입, ReactDOMRoot 생성자 함수) react-dom 패키지에 속합니다. 이 객체는 render와 unmount 메서드만 갖습니다. FiberRoot (createContainer 함수, createFiberRoot 함수, FiberRootNode 생성자 함수 ) react-reconciler 패키지에 속합니다. react-reconciler 실행 중 전역 컨텍스트 역할을 합니다. Fiber를 구성하는 과정에서 필요한 전역 상태를 저장합니다. 대부분의 인스턴스 변수는 Fiber 작업 루프 과정에서 상태를 저장합니다. React 내부적으로 위 변수들의 값에 따라 로직이 제어됩니다. HostRootFiber (createHostRootFiber 함수, HostRoot 태그) react-reconciler 패키지에 속합니다. React 내에서 첫 번째 Fiber 객체 입니다. Fiber 트리의 Root 노드이며 Node Type은 HostRoot 입니다. 이 세 가지 객체는 React 시스템이 동작할 수 있는 핵심 기반입니다. unmount하지 않는 경우 소멸되지 않으며, React의 전체 생명주기를 통틀어 유지됩니다. 위 3가지 객체는 createRoot 함수를 통해 생깁니다. 최초로 다음과 같은 구조를 갖게 됩니다. 위 구조 생성 과정을 그림으로 그리면 다음과 같습니다. legacyRenderSubtreeIntoContainer, legacyCreateRootFromDOMContainer, 2.2 ReactDOMRoot 객체 생성 React 애플리케이션을 시작하는 2가지 모드의 API는 서로 다릅니다. 그래서 각 소스 코드는 각기 다른 파일(Legacy, Concurrent)에 존재합니다. 하지만 최종적으로는 createContainer 함수를 이용하여 ReactDOMRoot 인스턴스(this.reactcontainer)를 생성하는 점은 똑같습니다 Legacy 모드의 ReactDOMRoot 생성 function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ): React$Component | PublicInstance | null { // container._reactRootContainer에 이미 ReactDOMRoot 존재하는지 확인. const maybeRoot = container._reactRootContainer; let root: FiberRoot; if (!maybeRoot) { // Initial mount, 초기 호출 // root = legacyCreateRootFromDOMContainer( container, children, parentComponent, callback, forceHydrate, ); } else { // root가 이미 초기화 된 경우 // root = maybeRoot; if (typeof callback === 'function') { const originalCallback = callback; callback = function () { const instance = getPublicRootInstance(root); originalCallback.call(instance); }; } // updateContainer(children, root, parentComponent, callback); } return getPublicRootInstance(root); } function legacyCreateRootFromDOMContainer( container: Container, initialChildren: ReactNodeList, parentComponent: ?React$Component, callback: ?Function, isHydrationContainer: boolean, ): FiberRoot { if (isHydrationContainer) { if (typeof callback === 'function') { const originalCallback = callback; callback = function () { const instance = getPublicRootInstance(root); originalCallback.call(instance); }; } const root: FiberRoot = createHydrationContainer( initialChildren, callback, container, LegacyRoot, null, // hydrationCallbacks false, // isStrictMode false, // concurrentUpdatesByDefaultOverride, '', // identifierPrefix wwwOnUncaughtError, wwwOnCaughtError, noopOnRecoverableError, // TODO(luna) Support hydration later null, null, ); container._reactRootContainer = root; // 여기서 ReactDOMRoot 저장 markContainerAsRoot(root.current, container); const rootContainerElement = !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ? container.parentNode : container; // $FlowFixMe[incompatible-call] listenToAllSupportedEvents(rootContainerElement); flushSyncWork(); return root; } else { // First clear any existing content. clearContainer(container); if (typeof callback === 'function') { const originalCallback = callback; callback = function () { const instance = getPublicRootInstance(root); originalCallback.call(instance); }; } const root = createContainer( container, LegacyRoot, null, // hydrationCallbacks false, // isStrictMode false, // concurrentUpdatesByDefaultOverride, '', // identifierPrefix wwwOnUncaughtError, wwwOnCaughtError, noopOnRecoverableError, null, // transitionCallbacks ); container._reactRootContainer = root; // 여기서 ReactDOMRoot 저장 markContainerAsRoot(root.current, container); const rootContainerElement = !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ? container.parentNode : container; // $FlowFixMe[incompatible-call] listenToAllSupportedEvents(rootContainerElement); // Initial mount should not be batched. updateContainerSync(initialChildren, root, parentComponent, callback); flushSyncWork(); return root; } } Concurrent 모드의 ReactDOMRoot 생성 function ReactDOMRoot(internalRoot: FiberRoot) { this._internalRoot = internalRoot; } export function createRoot( container: Element | Document | DocumentFragment, options?: CreateRootOptions, ): RootType { const root = createContainer( container, ConcurrentRoot, null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transitionCallbacks, ); // 이게 ReactDOMRoot 본체로 보면 됌. return new ReactDOMRoot(root); // this._internalRoot = root 가 내부적으로 수행. } // render가 별도로 나뉘어져 updateContainer가 여기서 실행. ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = // $FlowFixMe[missing-this-annot] function (children: ReactNodeList): void { const root = this._internalRoot; updateContainer(children, root, null, null); }; 2.3 FiberRoot 생성 ReactDOMRoot을 생성 과정에 React의 첫 번째 Fiber 객체인 FiberRoot를 생성합니다. (createContainer 함수 실행하며 createFiberRoot 함수 실행) 이때 legacy모드이면 tag 그대로 Fiber가 생성되고 아니라면 concurrent로 FiberRoot가 생성됩니다. Legacy FiberRoot (create-root-impl) , createRoot 함수, createRootImpl 함수(Concurrent에서 사용하는 createRoot함수와 같음) export function createRoot( container: Element | Document | DocumentFragment, options?: CreateRootOptions, ): RootType { return createRootImpl( container, assign( ({ onUncaughtError: wwwOnUncaughtError, onCaughtError: wwwOnCaughtError, }: any), options, ), ); } // createFiberRoot 함수 축약 export function createFiberRoot( containerInfo: Container, tag: RootTag, hydrate: boolean, initialChildren: ReactNodeList, hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, identifierPrefix: string, onUncaughtError: ( error: mixed, errorInfo: {+componentStack?: ?string}, ) => void, onCaughtError: ( error: mixed, errorInfo: { +componentStack?: ?string, +errorBoundary?: ?React$Component, }, ) => void, onRecoverableError: ( error: mixed, errorInfo: {+componentStack?: ?string}, ) => void, transitionCallbacks: null | TransitionTracingCallbacks, formState: ReactFormState | null, ): FiberRoot { // const root: FiberRoot = (new FiberRootNode( containerInfo, tag, hydrate, identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, formState, ): any); const uninitializedFiber = createHostRootFiber(tag, isStrictMode); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; return root; } function FiberRootNode( this: $FlowFixMe, containerInfo: any, // $FlowFixMe[missing-local-annot] tag, hydrate: any, identifierPrefix: any, onUncaughtError: any, onCaughtError: any, onRecoverableError: any, formState: ReactFormState | null, ) { this.tag = disableLegacyMode ? ConcurrentRoot : tag; this.containerInfo = containerInfo; this.pendingChildren = null; this.current = null; this.pingCache = null; this.timeoutHandle = noTimeout; this.cancelPendingCommit = null; this.context = null; this.pendingContext = null; this.next = null; this.callbackNode = null; this.callbackPriority = NoLane; this.expirationTimes = createLaneMap(NoTimestamp); this.pendingLanes = NoLanes; this.suspendedLanes = NoLanes; this.pingedLanes = NoLanes; this.warmLanes = NoLanes; this.expiredLanes = NoLanes; this.errorRecoveryDisabledLanes = NoLanes; this.shellSuspendCounter = 0; this.entangledLanes = NoLanes; this.entanglements = createLaneMap(NoLanes); this.hiddenUpdates = createLaneMap(null); this.identifierPrefix = identifierPrefix; this.onUncaughtError = onUncaughtError; this.onCaughtError = onCaughtError; this.onRecoverableError = onRecoverableError; this.pooledCache = null; this.pooledCacheLanes = NoLanes; this.formState = formState; this.incompleteTransitions = new Map(); } 2.4 HostRootFiber 생성 HostRootFiber는 createHostRootFiber로 생성됩니다. 처음에는 1개만 생성되며 추후 더블 버퍼 구조로 복사본을 만들어 상태관리를 하여 화면을 그리는데 사용됩니다. Fiber 트리에서 모든 노드의 mode는 HostRootFiber의 mode와 동일합니다. 왜냐하면 새로 생성되는 Fiber 노드의 mode는 부모 노드에서 상속되기 때문입니다. export function createHostRootFiber( tag: RootTag, isStrictMode: boolean, ): Fiber { let mode; if (disableLegacyMode || tag === ConcurrentRoot) { mode = ConcurrentMode; if (isStrictMode === true) { mode |= StrictLegacyMode | StrictEffectsMode; } } else { mode = NoMode; } return createFiber(HostRoot, null, null, mode); //HostRoot타입으로 생성 } 2.5 객체들 생성 후 모습 간략도 및 정리 legacy 모드 legacy모드에서는 DOM 객체에 _reactRootContainer 프로퍼티를 통해서 ReactFiberRoot를 추가적으로 참조한다. concurrent 모드 2.6 render, update 함수 React19.1에서는 Legacy와 Concurrent 모두 같은 updateContainer 함수를 사용합니다. 그리고 이 updateContainer를 통해 react-dom과 react-reconciler가 연결됩니다. 다만 render 함수 진입 소스 코드가 다릅니다. 그리고 첫 마운트시 Legacy 코드는 Sync한 방식으로 Concurrent는 일반적인 방식으로 updateContainerImpl 함수를 수행합니다. 중요한 점은 마지막에 scheduleUpdateOnFiber 함수를 실행하여 Reconciler의 "입력" 단계의 진입점을 실행한다는 것입니다. Legacy의 updateContainer Legacy의 render 함수 updateContainer 함수 export function render( element: React$Element, container: Container, callback: ?Function, ): React$Component | PublicInstance | null { if (!isValidContainer(container)) { throw new Error('Target container is not a DOM element.'); } return legacyRenderSubtreeIntoContainer( null, element, container, false, callback, ); } function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ): React$Component | PublicInstance | null { const maybeRoot = container._reactRootContainer; let root: FiberRoot; if (!maybeRoot) { // Initial mount (내부적으로 업데이트) root = legacyCreateRootFromDOMContainer( container, children, parentComponent, callback, forceHydrate, ); } else { root = maybeRoot; if (typeof callback === 'function') { const originalCallback = callback; callback = function () { const instance = getPublicRootInstance(root); originalCallback.call(instance); }; } // 업데이트 컨테이너 updateContainer(children, root, parentComponent, callback); } return getPublicRootInstance(root); } function legacyCreateRootFromDOMContainer( container: Container, initialChildren: ReactNodeList, parentComponent: ?React$Component, callback: ?Function, isHydrationContainer: boolean, ): FiberRoot { if (isHydrationContainer) { if (typeof callback === 'function') { const originalCallback = callback; callback = function () { const instance = getPublicRootInstance(root); originalCallback.call(instance); }; } const root: FiberRoot = createHydrationContainer( initialChildren, callback, container, LegacyRoot, null, // hydrationCallbacks false, // isStrictMode false, // concurrentUpdatesByDefaultOverride, '', // identifierPrefix wwwOnUncaughtError, wwwOnCaughtError, noopOnRecoverableError, // TODO(luna) Support hydration later null, null, ); container._reactRootContainer = root; markContainerAsRoot(root.current, container); const rootContainerElement = !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ? container.parentNode : container; // $FlowFixMe[incompatible-call] listenToAllSupportedEvents(rootContainerElement); flushSyncWork(); return root; } else { // First clear any existing content. clearContainer(container); if (typeof callback === 'function') { const originalCallback = callback; callback = function () { const instance = getPublicRootInstance(root); originalCallback.call(instance); }; } const root = createContainer( container, LegacyRoot, null, // hydrationCallbacks false, // isStrictMode false, // concurrentUpdatesByDefaultOverride, '', // identifierPrefix wwwOnUncaughtError, wwwOnCaughtError, noopOnRecoverableError, null, // transitionCallbacks ); container._reactRootContainer = root; markContainerAsRoot(root.current, container); const rootContainerElement = !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ? container.parentNode : container; // $FlowFixMe[incompatible-call] listenToAllSupportedEvents(rootContainerElement); // Initial mount should not be batched. Sync하게 업데이트 updateContainerSync(initialChildren, root, parentComponent, callback); flushSyncWork(); return root; } } export function updateContainerSync( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, ): Lane { if (!disableLegacyMode && container.tag === LegacyRoot) { flushPendingEffects(); } const current = container.current; updateContainerImpl( current, SyncLane, //Sync한 레인으로 할당 element, container, parentComponent, callback, ); return SyncLane; } Concurrent의 updateContainer Concurrent의 render 함수 ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function (children: ReactNodeList): void { const root = this._internalRoot; if (root === null) { throw new Error('Cannot update an unmounted root.'); } // 업데이터 컨테이너 updateContainer(children, root, null, null); }; export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, ): Lane { const current = container.current; const lane = requestUpdateLane(current); updateContainerImpl( current, lane, // 적절한 비동기 Lane 할당 element, container, parentComponent, callback, ); return lane; } // updateContainerImpl 함수 function updateContainerImpl( rootFiber: Fiber, lane: Lane, element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, ): void { const context = getContextForSubtree(parentComponent); if (container.context === null) { container.context = context; } else { container.pendingContext = context; } const update = createUpdate(lane); // lane에 맞는 update 생성 callback = callback === undefined ? null : callback; if (callback !== null) { update.callback = callback; } const root = enqueueUpdate(rootFiber, update, lane); //업데이트 추가 if (root !== null) { startUpdateTimerByLane(lane); // 업데이트 타이머 시작 scheduleUpdateOnFiber(root, rootFiber, lane); // Reconciler 동작 흐름의 '입력' 단계 진입. entangleTransitions(root, rootFiber, lane); } } Legacy 모드 HostRootFiber.mode가 NoMode로 실행됩니다. Interruptible Rendering(중단 가능한 렌더링)이 불가능 합니다. 최초 렌더링이나 후속 업데이트에 동기 작업 루프만을 사용합니다. Reconciliation이 중단될 수 없어 생명주기 함수는 한번만 실행됩니다. 호환용으로만 존재하며 React 19에서는 사용할 수 없는 상태입니다. Concurrent 모드 HostRootFiber.mode가 Current 모드로 실행합니다. 중단 가능한 렌더링이 가능하여 일부 생명주기 함수가 여러번 실행될 수 있습니다. 여러번의 프레임에 걸쳐서 업데이트를 완료할 수 있습니다. Concurrent 모드가 도입되며 데이터 로딩도 Suspense 적용이 가능해졌습니다.
2025-05-060019 - React
[리액트 19] 4편. Reconciler 동작 흐름
4편. Reconciler 동작 흐름 지금까지 거시적인 패키지 구조와 주요 두 작업루프에 대한 설명을 통해 react-reconciler 패키지에 대해 어느 정도 이해했습니다. 4편에서는 react-reconciler 패키지의 주요 역할을 정리하며 주요 기능들을 4가지로 나눠서 설명하겠습니다. 입력: schedulerUpdateOnFiber와 같은 API 함수를 외부에 노출하여, react 패키지 등 다른 패키지에서 호출할 수 있도록 합니다. 스케줄링 Task 등록: scheduler 패키지와 상호작용하여 task를 등록하고 콜백을 기다립니다. task 콜백 실행: fiber 트리를 구성하고, 동시에 렌더러(react-dom 또는 react-native)와 상호작용하여 fiber에 대응하는 DOM 노드를 생성합니다. 출력: 렌더러와 상호작용하여 실제 DOM 노드를 렌더링합니다. (커밋) 위 1~4번 과정을 그림에서 나타냈습니다. (ReactFiberWorkLoop.js, ReactFiberRootScheduler.js) ReactFiberWorkLoop.js에서 입력을 담당하는 sccheduleUpdateOnFiber 뿐입니다. react-reconciler 패키지에서 외부에 노출되는 api 함수들 중에서 fiber를 변경해야 하는 작업이 포함된다면 모두다 최종적으로는 scheduleUpdateOnFiber를 호출하게 됩니다. 따라서 scheduleUpdateOnFiber 함수는 입력 흐름에서 반드시 거쳐야 하는 경로입니다. export function scheduleUpdateOnFiber( root: FiberRoot, fiber: Fiber, lane: Lane, ) { // Check if the work loop is currently suspended and waiting for data to // finish loading. if ( // Suspended render phase (root === workInProgressRoot && (workInProgressSuspendedReason === SuspendedOnData || workInProgressSuspendedReason === SuspendedOnAction)) || // Suspended commit phase root.cancelPendingCommit !== null ) { // The incoming update might unblock the current render. Interrupt the // current attempt and restart from the top. prepareFreshStack(root, NoLanes); const didAttemptEntireTree = false; markRootSuspended( root, workInProgressRootRenderLanes, workInProgressDeferredLane, didAttemptEntireTree, ); } // Mark that the root has a pending update. markRootUpdated(root, lane); if ( (executionContext & RenderContext) !== NoLanes && root === workInProgressRoot ) { // This update was dispatched during the render phase. This is a mistake // if the update originates from user space (with the exception of local // hook updates, which are handled differently and don't reach this // function), but there are some internal React features that use this as // an implementation detail, like selective hydration. warnAboutRenderPhaseUpdatesInDEV(fiber); // Track lanes that were updated during the render phase workInProgressRootRenderPhaseUpdatedLanes = mergeLanes( workInProgressRootRenderPhaseUpdatedLanes, lane, ); } else { if (root === workInProgressRoot) { // Received an update to a tree that's in the middle of rendering. Mark // that there was an interleaved update work on this root. if ((executionContext & RenderContext) === NoContext) { workInProgressRootInterleavedUpdatedLanes = mergeLanes( workInProgressRootInterleavedUpdatedLanes, lane, ); } if (workInProgressRootExitStatus === RootSuspendedWithDelay) { // The root already suspended with a delay, which means this render // definitely won't finish. Since we have a new update, let's mark it as // suspended now, right before marking the incoming update. This has the // effect of interrupting the current render and switching to the update. // TODO: Make sure this doesn't override pings that happen while we've // already started rendering. const didAttemptEntireTree = false; markRootSuspended( root, workInProgressRootRenderLanes, workInProgressDeferredLane, didAttemptEntireTree, ); } } // 스케줄러(Scheduler) 패키지를 통한 스케줄링 작업 등록, // 간접적으로 `fiber 구성` 수행 ensureRootIsScheduled(root); } scheduleUpdateOnFiber 함수로 진입한 이후 항상 ensureRootIsScheduled가 실행되는 것을 알 수 있습니다. (React 18버전 이전에는 SyncLane인지 확인한 후에 스케줄링을 거치지 않고 직접 fiber를 구성하는 함수가 있었습니다. 하지만 현재는 모든 업데이트가 Scheduler를 거쳐서 fiber를 구성합니다.) 위 코드 예시를 보면 scheduleUpdateOnFiber 함수 실행 후 ensureRootIsScheduled 함수를 실행합니다. // 중복된 마이크로태스크가 예약되는 것을 방지하기 위해 사용됩니다. let didScheduleMicrotask: boolean = false; // 동기화 작업(sync work)이 없으면 flushSync에서 빠르게 탈출하기 위해 사용됩니다.( let mightHavePendingSyncWork: boolean = false; export function ensureRootIsScheduled(root: FiberRoot): void { // 이 함수는 root가 update를 받을때마다 항상 실행됩니다. 이 함수는 2가지 기능을 수행합니다. // 1) root가 root schedule에 포함되어 있는지 확인합니다. // 2) root schedule을 처리하기 위한 pending microtask가 있는지 확인합니다. // // 실제 스케줄링 로직의 대부분은 `scheduleTaskForRootDuringMicrotask`가 실행될 때 수행됩니다. // Add the root to the schedule if (root === lastScheduledRoot || root.next !== null) { // Fast path. This root is already scheduled. } else { if (lastScheduledRoot === null) { firstScheduledRoot = lastScheduledRoot = root; } else { lastScheduledRoot.next = root; lastScheduledRoot = root; } } // Any time a root received an update, we set this to true until the next time // we process the schedule. If it's false, then we can quickly exit flushSync // without consulting the schedule. mightHavePendingSyncWork = true; if (!didScheduleMicrotask) { didScheduleMicrotask = true; scheduleImmediateRootScheduleTask(); } } function scheduleImmediateRootScheduleTask() { // TODO: Can we land supportsMicrotasks? Which environments don't support it? // Alternatively, can we move this check to the host config? if (supportsMicrotasks) { scheduleMicrotask(() => { // In Safari, appending an iframe forces microtasks to run. // https://github.com/facebook/react/issues/22459 // We don't support running callbacks in the middle of render // or commit so we need to check against that. const executionContext = getExecutionContext(); if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { // Note that this would still prematurely flush the callbacks // if this happens outside render or commit phase (e.g. in an event). // Intentionally using a macrotask instead of a microtask here. This is // wrong semantically but it prevents an infinite loop. The bug is // Safari's, not ours, so we just do our best to not crash even though // the behavior isn't completely correct. Scheduler_scheduleCallback( ImmediateSchedulerPriority, processRootScheduleInImmediateTask, ); return; } // 처리할 작업이 없으면 실행. processRootScheduleInMicrotask(); }); } else { // micarotask가 지원되지 않는 브라우저나 환경에서 scheduler 바로 예약 Scheduler_scheduleCallback( ImmediateSchedulerPriority, processRootScheduleInImmediateTask, ); } } ensureRootIsScheduled는 scheduled되었는지 확인하고 scheduleImmediateRootScheduleTask를 통해 Scheduler로 task가 예약되는 것을 확인할 수 있습니다. task 콜백은 performWorkOnRoot를 통해 실행됩니다. 이 함수의 파라미터 값 중에 forceSync 값이 true이면 performSyncWorkOnRoot를 실행하고, false면 performConcurrentWorkOnRoot 함수를 실행합니다. 이 단계에서는 아래와 같이 3가지 과정을 거칩니다. shouldTimeSlice(Concurrent 여부) 체크 후 Fiber 트리 구성. renderRootSync일 경우 renderRootSync 함수(React19에서는 Legacy 호환용으로 거의 사용 X) renderRootSync는 렌더링 중간에 중단이 불가능합니다. remderRootConcurrent일 경우 renderRootConcurrent 힘수 renderRootConcurrent는 렌더링 중간에 중단이 가능하며, 렌더링 과정 중인지, 이전 렌더링을 복구해야 하는지 확인합니다. 중간에 중단되었다면, performWorkOnRootViaSchedulerTask 함수를 다시 스케줄링하여 이어서 처리합니다. 예외 처리(Suspense일 경우 해당 상태로 변경하여 출력시 반영) performWorkOnRoot => finishConcurrentRender => commitRootWhenReady => commitRoot 함수로 출력 (commit) performWorkOnRoot export function performWorkOnRoot( root: FiberRoot, lanes: Lanes, forceSync: boolean, ): void { if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { throw new Error('Should not already be working.'); } // We disable time-slicing in some cases: if the work has been CPU-bound // for too long ("expired" work, to prevent starvation), or we're in // sync-updates-by-default mode. const shouldTimeSlice = (!forceSync && !includesBlockingLane(lanes) && !includesExpiredLane(root, lanes)) || // If we're prerendering, then we should use the concurrent work loop // even if the lanes are synchronous, so that prerendering never blocks // the main thread. // TODO: We should consider doing this whenever a sync lane is suspended, // even for regular pings. (enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes)); // // let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes, true); let renderWasConcurrent = shouldTimeSlice; // do { if (exitStatus === RootInProgress) { // Render phase is still in progress. if ( enableSiblingPrerendering && workInProgressRootIsPrerendering && !shouldTimeSlice ) { // We're in prerendering mode, but time slicing is not enabled. This // happens when something suspends during a synchronous update. Exit the // the work loop. When we resume, we'll use the concurrent work loop so // that prerendering is non-blocking. // // Mark the root as suspended. Usually we do this at the end of the // render phase, but we do it here so that we resume in // prerendering mode. // TODO: Consider always calling markRootSuspended immediately. // Needs to be we attach a ping listener, though. const didAttemptEntireTree = false; markRootSuspended(root, lanes, NoLane, didAttemptEntireTree); } break; } else { // The render completed. // Check if this render may have yielded to a concurrent event, and if so, // confirm that any newly rendered stores are consistent. // TODO: It's possible that even a concurrent render may never have yielded // to the main thread, if it was fast enough, or if it expired. We could // skip the consistency check in that case, too. const finishedWork: Fiber = (root.current.alternate: any); if ( renderWasConcurrent && !isRenderConsistentWithExternalStores(finishedWork) ) { if (enableProfilerTimer && enableComponentPerformanceTrack) { setCurrentTrackFromLanes(lanes); logInconsistentRender(renderStartTime, renderEndTime); finalizeRender(lanes, renderEndTime); } // A store was mutated in an interleaved event. Render again, // synchronously, to block further mutations. exitStatus = renderRootSync(root, lanes, false); // We assume the tree is now consistent because we didn't yield to any // concurrent events. renderWasConcurrent = false; // Need to check the exit status again. continue; } // 에러 발생했는지 확인 if ( (disableLegacyMode || root.tag !== LegacyRoot) && exitStatus === RootErrored ) { const lanesThatJustErrored = lanes; const errorRetryLanes = getLanesToRetrySynchronouslyOnError( root, lanesThatJustErrored, ); if (errorRetryLanes !== NoLanes) { if (enableProfilerTimer && enableComponentPerformanceTrack) { setCurrentTrackFromLanes(lanes); logErroredRenderPhase(renderStartTime, renderEndTime, lanes); finalizeRender(lanes, renderEndTime); } lanes = errorRetryLanes; exitStatus = recoverFromConcurrentError( root, lanesThatJustErrored, errorRetryLanes, ); renderWasConcurrent = false; // Need to check the exit status again. if (exitStatus !== RootErrored) { // The root did not error this time. Restart the exit algorithm // from the beginning. // TODO: Refactor the exit algorithm to be less confusing. Maybe // more branches + recursion instead of a loop. I think the only // thing that causes it to be a loop is the RootSuspendedAtTheShell // check. If that's true, then we don't need a loop/recursion // at all. continue; } } } if (exitStatus === RootFatalErrored) { prepareFreshStack(root, NoLanes); // Since this is a fatal error, we're going to pretend we attempted // the entire tree, to avoid scheduling a prerender. const didAttemptEntireTree = true; markRootSuspended(root, lanes, NoLane, didAttemptEntireTree); break; } // We now have a consistent tree. The next step is either to commit it, // or, if something suspended, wait to commit it after a timeout. // finishConcurrentRender( root, exitStatus, finishedWork, lanes, renderEndTime, ); } break; } while (true); // 종료 전에 다시 확인: 다른 업데이트가 있는지, 새 스케줄이 필요한지. ensureRootIsScheduled(root); } performSyncWorkOnRoot, performWorkOnRootViaSchedulerTask function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) { // This is the entry point for synchronous tasks that don't go // through Scheduler. const didFlushPassiveEffects = flushPendingEffects(); if (didFlushPassiveEffects) { // If passive effects were flushed, exit to the outer work loop in the root // scheduler, so we can recompute the priority. return null; } const forceSync = true; performWorkOnRoot(root, lanes, forceSync); } // SchedulerTask를 통해 work function performWorkOnRootViaSchedulerTask( root: FiberRoot, didTimeout: boolean, ): RenderTaskFn | null { // This is the entry point for concurrent tasks scheduled via Scheduler (and // postTask, in the future). if (hasPendingCommitEffects()) { // We are currently in the middle of an async committing (such as a View Transition). // We could force these to flush eagerly but it's better to defer any work until // it finishes. This may not be the same root as we're waiting on. // TODO: This relies on the commit eventually calling ensureRootIsScheduled which // always calls processRootScheduleInMicrotask which in turn always loops through // all the roots to figure out. This is all a bit inefficient and if optimized // it'll need to consider rescheduling a task for any skipped roots. root.callbackNode = null; root.callbackPriority = NoLane; return null; } // Flush any pending passive effects before deciding which lanes to work on, // in case they schedule additional work. const originalCallbackNode = root.callbackNode; const didFlushPassiveEffects = flushPendingEffects(true); if (didFlushPassiveEffects) { // Something in the passive effect phase may have canceled the current task. // Check if the task node for this root was changed. if (root.callbackNode !== originalCallbackNode) { // The current task was canceled. Exit. We don't need to call // `ensureRootIsScheduled` because the check above implies either that // there's a new task, or that there's no remaining work on this root. return null; } else { // Current task was not canceled. Continue. } } // Determine the next lanes to work on, using the fields stored on the root. // TODO: We already called getNextLanes when we scheduled the callback; we // should be able to avoid calling it again by stashing the result on the // root object. However, because we always schedule the callback during // a microtask (scheduleTaskForRootDuringMicrotask), it's possible that // an update was scheduled earlier during this same browser task (and // therefore before the microtasks have run). That's because Scheduler batches // together multiple callbacks into a single browser macrotask, without // yielding to microtasks in between. We should probably change this to align // with the postTask behavior (and literally use postTask when // it's available). const workInProgressRoot = getWorkInProgressRoot(); const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); const rootHasPendingCommit = root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout; const lanes = getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, rootHasPendingCommit, ); if (lanes === NoLanes) { // No more work on this root. return null; } // Enter the work loop. // TODO: We only check `didTimeout` defensively, to account for a Scheduler // bug we're still investigating. Once the bug in Scheduler is fixed, // we can remove this, since we track expiration ourselves. const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout; //performWorkOnRoot 실행. performWorkOnRoot(root, lanes, forceSync); // The work loop yielded, but there may or may not be work left at the current // priority. Need to determine whether we need to schedule a continuation. // Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask; // however, since most of the logic for determining if we need a continuation // versus a new task is the same, we cheat a bit and call it here. This is // only safe to do because we know we're at the end of the browser task. // So although it's not an actual microtask, it might as well be. scheduleTaskForRootDuringMicrotask(root, now()); if (root.callbackNode != null && root.callbackNode === originalCallbackNode) { // The task node scheduled for this root is the same one that's // currently executed. Need to return a continuation. return performWorkOnRootViaSchedulerTask.bind(null, root); } return null; } 출력 단계에서는 commitRoot 함수를 통해 Effetcs가 실행되고, 화면에 그려집니다. 크게 3단계로 나눠진다고 볼 수 있습니다. commitBeforeMutationEffects DOM 변경 이전 단계. fiber.flags가 Snapshot, Passive에 해당하는 경우 처리합니다. commitMutationEffects host tree(DOM 또는 RN)을 변경하는 단계. fiber.flags가 Placement, Update, Deletion, Hydrating에 해당하는 경우 처리합니다. commitLayoutEffects DOM 변경 이후, Effects(Side Effects)를 실행하는 단계입니다. fiber.flags가 Update, Callback인 경우 처리합니다. ReactFiberFlags.js FiberWorkLoop.js(commitRoot) finishConcurrentRender, commitRootWhenReady function finishConcurrentRender( root: FiberRoot, exitStatus: RootExitStatus, finishedWork: Fiber, lanes: Lanes, renderEndTime: number, // Profiling-only ) { // TODO: The fact that most of these branches are identical suggests that some // of the exit statuses are not best modeled as exit statuses and should be // tracked orthogonally. switch (exitStatus) { case RootInProgress: case RootFatalErrored: { throw new Error('Root did not complete. This is a bug in React.'); } case RootSuspendedWithDelay: { if (!includesOnlyTransitions(lanes)) { // Commit the placeholder. break; } } // Fallthrough case RootSuspendedAtTheShell: { const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings; markRootSuspended( root, lanes, workInProgressDeferredLane, didAttemptEntireTree, ); return; } case RootErrored: { // This render errored. Ignore any recoverable errors because we weren't actually // able to recover. Instead, whatever the final errors were is the ones we log. // This ensures that we only log the actual client side error if it's just a plain // error thrown from a component on the server and the client. workInProgressRootRecoverableErrors = null; break; } case RootSuspended: case RootCompleted: { break; } default: { throw new Error('Unknown root exit status.'); } } if ( includesOnlyRetries(lanes) && (alwaysThrottleRetries || exitStatus === RootSuspended) ) { // This render only included retries, no updates. Throttle committing // retries so that we don't show too many loading states too quickly. const msUntilTimeout = globalMostRecentFallbackTime + FALLBACKMS // Don't bother with a very short suspense time. if (msUntilTimeout > 10) { const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings; markRootSuspended( root, lanes, workInProgressDeferredLane, didAttemptEntireTree, ); const nextLanes = getNextLanes(root, NoLanes, true); if (nextLanes !== NoLanes) { // There's additional work we can do on this root. We might as well // attempt to work on that while we're suspended. return; } // The render is suspended, it hasn't timed out, and there's no // lower priority work to do. Instead of committing the fallback // immediately, wait for more data to arrive. // TODO: Combine retry throttling with Suspensey commits. Right now they // run one after the other. root.timeoutHandle = scheduleTimeout( commitRootWhenReady.bind( null, root, finishedWork, workInProgressRootRecoverableErrors, workInProgressTransitions, workInProgressRootDidIncludeRecursiveRenderUpdate, lanes, workInProgressDeferredLane, workInProgressRootInterleavedUpdatedLanes, workInProgressSuspendedRetryLanes, workInProgressRootDidSkipSuspendedSiblings, exitStatus, THROTTLED_COMMIT, renderStartTime, renderEndTime, ), msUntilTimeout, ); return; } } // 출력: Fiber 트리를 기반으로 실제 렌더링(Coomit). commitRootWhenReady( root, finishedWork, workInProgressRootRecoverableErrors, workInProgressTransitions, workInProgressRootDidIncludeRecursiveRenderUpdate, lanes, workInProgressDeferredLane, workInProgressRootInterleavedUpdatedLanes, workInProgressSuspendedRetryLanes, workInProgressRootDidSkipSuspendedSiblings, exitStatus, IMMEDIATE_COMMIT, renderStartTime, renderEndTime, ); } function commitRootWhenReady( root: FiberRoot, finishedWork: Fiber, recoverableErrors: Array> | null, transitions: Array | null, didIncludeRenderPhaseUpdate: boolean, lanes: Lanes, spawnedLane: Lane, updatedLanes: Lanes, suspendedRetryLanes: Lanes, didSkipSuspendedSiblings: boolean, exitStatus: RootExitStatus, suspendedCommitReason: SuspendedCommitReason, // Profiling-only completedRenderStartTime: number, // Profiling-only completedRenderEndTime: number, // Profiling-only ) { root.timeoutHandle = noTimeout; // TODO: Combine retry throttling with Suspensey commits. Right now they run // one after the other. const BothVisibilityAndMaySuspendCommit = Visibility | MaySuspendCommit; const subtreeFlags = finishedWork.subtreeFlags; const isViewTransitionEligible = enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes); // TODO: Use a subtreeFlag to optimize. const isGestureTransition = enableSwipeTransition && isGestureRender(lanes); const maySuspendCommit = subtreeFlags & ShouldSuspendCommit || (subtreeFlags & BothVisibilityAndMaySuspendCommit) === BothVisibilityAndMaySuspendCommit; if (isViewTransitionEligible || maySuspendCommit || isGestureTransition) { // Before committing, ask the renderer whether the host tree is ready. // If it's not, we'll wait until it notifies us. startSuspendingCommit(); // This will walk the completed fiber tree and attach listeners to all // the suspensey resources. The renderer is responsible for accumulating // all the load events. This all happens in a single synchronous // transaction, so it track state in its own module scope. // This will also track any newly added or appearing ViewTransition // components for the purposes of forming pairs. accumulateSuspenseyCommit(finishedWork); if (isViewTransitionEligible || isGestureTransition) { // If we're stopping gestures we don't have to wait for any pending // view transition. We'll stop it when we commit. if (!enableSwipeTransition || root.stoppingGestures === null) { suspendOnActiveViewTransition(root.containerInfo); } } // At the end, ask the renderer if it's ready to commit, or if we should // suspend. If it's not ready, it will return a callback to subscribe to // a ready event. const schedulePendingCommit = waitForCommitToBeReady(); if (schedulePendingCommit !== null) { // NOTE: waitForCommitToBeReady returns a subscribe function so that we // only allocate a function if the commit isn't ready yet. The other // pattern would be to always pass a callback to waitForCommitToBeReady. // Not yet ready to commit. Delay the commit until the renderer notifies // us that it's ready. This will be canceled if we start work on the // root again. root.cancelPendingCommit = schedulePendingCommit( commitRoot.bind( null, root, finishedWork, lanes, recoverableErrors, transitions, didIncludeRenderPhaseUpdate, spawnedLane, updatedLanes, suspendedRetryLanes, exitStatus, SUSPENDED_COMMIT, completedRenderStartTime, completedRenderEndTime, ), ); const didAttemptEntireTree = !didSkipSuspendedSiblings; markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree); return; } } // Otherwise, commit immediately.; commitRoot( root, finishedWork, lanes, recoverableErrors, transitions, didIncludeRenderPhaseUpdate, spawnedLane, updatedLanes, suspendedRetryLanes, exitStatus, suspendedCommitReason, completedRenderStartTime, completedRenderEndTime, ); } commitRoot function commitRoot( root: FiberRoot, finishedWork: null | Fiber, lanes: Lanes, recoverableErrors: null | Array>, transitions: Array | null, didIncludeRenderPhaseUpdate: boolean, spawnedLane: Lane, updatedLanes: Lanes, suspendedRetryLanes: Lanes, exitStatus: RootExitStatus, suspendedCommitReason: SuspendedCommitReason, // Profiling-only completedRenderStartTime: number, // Profiling-only completedRenderEndTime: number, // Profiling-only ): void { root.cancelPendingCommit = null; do { // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which // means `flushPassiveEffects` will sometimes result in additional // passive effects. So we need to keep flushing in a loop until there are // no more pending effects. // TODO: Might be better if `flushPassiveEffects` did not automatically // flush synchronous work at the end, to avoid factoring hazards like this. flushPendingEffects(); } while (pendingEffectsStatus !== NOEFFECTS); flushRenderPhaseStrictModeWarningsInDEV(); if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { throw new Error('Should not already be working.'); } if (finishedWork === null) { if (enableSwipeTransition) { // Stop any gestures that were completed and is now being reverted. if (root.stoppingGestures !== null) { stopCompletedGestures(root); } } return; } if (finishedWork === root.current) { throw new Error( 'Cannot commit the same tree as before. This error is likely caused by ' + 'a bug in React. Please file an issue.', ); } // Check which lanes no longer have any work scheduled on them, and mark // those as finished. let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); // Make sure to account for lanes that were updated by a concurrent event // during the render phase; don't mark them as finished. const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes(); remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes); if (enableSwipeTransition && root.pendingGestures === null) { // Gestures don't clear their lanes while the gesture is still active but it // might not be scheduled to do any more renders and so we shouldn't schedule // any more gesture lane work until a new gesture is scheduled. remainingLanes &= ~GestureLane; } markRootFinished( root, lanes, remainingLanes, spawnedLane, updatedLanes, suspendedRetryLanes, ); // Reset this before firing side effects so we can detect recursive updates. didIncludeCommitPhaseUpdate = false; if (root === workInProgressRoot) { // We can reset these now that they are finished. workInProgressRoot = null; workInProgress = null; workInProgressRootRenderLanes = NoLanes; } else { // This indicates that the last root we worked on is not the same one that // we're committing now. This most commonly happens when a suspended root // times out. } // workInProgressX might be overwritten, so we want // to store it in pendingPassiveX until they get processed // We need to pass this through as an argument to commitRoot // because workInProgressX might have changed between // the previous render and commit if we throttle the commit // with setTimeout pendingFinishedWork = finishedWork; pendingEffectsRoot = root; pendingEffectsLanes = lanes; pendingEffectsRemainingLanes = remainingLanes; pendingPassiveTransitions = transitions; pendingRecoverableErrors = recoverableErrors; pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate; if (enableSwipeTransition && isGestureRender(lanes)) { // This is a special kind of render that doesn't commit regular effects. commitGestureOnRoot( root, finishedWork, recoverableErrors, enableProfilerTimer ? suspendedCommitReason === IMMEDIATE_COMMIT ? completedRenderEndTime : commitStartTime : 0, ); return; } // If there are pending passive effects, schedule a callback to process them. // Do this as early as possible, so it is queued before anything else that // might get scheduled in the commit phase. (See #16714.) // TODO: Delete all other places that schedule the passive effect callback // They're redundant. let passiveSubtreeMask; if (enableViewTransition) { pendingViewTransitionEvents = null; if (includesOnlyViewTransitionEligibleLanes(lanes)) { // Claim any pending Transition Types for this commit. // This means that multiple roots committing independent View Transitions // 1) end up staggered because we can only have one at a time. // 2) only the first one gets all the Transition Types. pendingTransitionTypes = ReactSharedInternals.V; ReactSharedInternals.V = null; passiveSubtreeMask = PassiveTransitionMask; } else { pendingTransitionTypes = null; passiveSubtreeMask = PassiveMask; } } else { passiveSubtreeMask = PassiveMask; } // If we don't have passive effects, we're not going to need to perform more work // so we can clear the callback now. root.callbackNode = null; root.callbackPriority = NoLane; resetShouldStartViewTransition(); // The commit phase is broken into several sub-phases. We do a separate pass // of the effect list for each phase: all mutation effects come before all // layout effects, and so on. // Check if there are any effects in the whole tree. // TODO: This is left over from the effect list implementation, where we had // to check for the existence of `firstEffect` to satisfy Flow. I think the // only other reason this optimization exists is because it affects profiling. // Reconsider whether this is necessary. const subtreeHasBeforeMutationEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask)) !== NoFlags; const rootHasBeforeMutationEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask)) !== NoFlags; if (subtreeHasBeforeMutationEffects || rootHasBeforeMutationEffect) { const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority(DiscreteEventPriority); const prevExecutionContext = executionContext; executionContext |= CommitContext; try { // 첫번째 "before mutaion" 단계. // The first phase a "before mutation" phase. We use this phase to read the // state of the host tree right before we mutate it. This is where // getSnapshotBeforeUpdate is called. commitBeforeMutationEffects(root, finishedWork, lanes); } finally { // Reset the priority to the previous non-sync value. executionContext = prevExecutionContext; setCurrentUpdatePriority(previousPriority); ReactSharedInternals.T = prevTransition; } } let willStartViewTransition = shouldStartViewTransition; if (enableSwipeTransition) { // Stop any gestures that were completed and is now being committed. if (root.stoppingGestures !== null) { stopCompletedGestures(root); // If we are in the process of stopping some gesture we shouldn't start // a View Transition because that would start from the previous state to // the next state. willStartViewTransition = false; } } pendingEffectsStatus = PENDINGPHASE; const startedViewTransition = enableViewTransition && willStartViewTransition && startViewTransition( root.containerInfo, pendingTransitionTypes, flushMutationEffects, flushLayoutEffects, flushAfterMutationEffects, flushSpawnedWork, flushPassiveEffects, reportViewTransitionError, ); if (!startedViewTransition) { // Flush synchronously. // 두번째 "mutaion" 단계. host tree(Dom 트리)를 변경하는 단계 flushMutationEffects(); // 세번째 "layout" 단계. host tree 변경 후 effects를 실행하는 단계 flushLayoutEffects(); // Skip flushAfterMutationEffects flushSpawnedWork(); } } 정리 이번 편에서는 reconciler의 동작 과정을 4단계로 나누어 거시적으로 분석했습니다. 한 문장으로 쉽게 요약하면 setState같은 함수로 입력이 일어나고, scheduler에 task로 등록이 되고, task가 실행되고, 마지막으로 commit을 통해 화면에 출력이 되며 Effects까지 실행됩니다. 다음 편에서는 React의 시작 과정에 대해 설명하도록 하겠습니다.
2025-04-300028 - React
[리액트 19] 3편. React에서 자주 사용되는 객체
3편. React에서 자주 사용되는 객체 React에는 다양한 객체와 데이터 구조가 등장합니다. 2편에서 소개한 fiber tree나 WorkLoop의 time min-heap 자료구조인 task schedule queue가 그 예시들 입니다. 3편에서는 리액트가 시작되고 렌더링될때 까지 자주 등장하는 중요한 객체들을 패키지별로 정리해보려 합니다. react 패키지는 ReactElement(리액트 엘리먼트)를 생성하는 필수 함수들과 API를 제공합니다. 예를 들면 다음과 같은 JSX 문법들은 모두 ReactElement를 생성하는 createElement로 Babel 플러그인에 의해 바뀝니다. ReactDOM.createRoot(document.getElementById('root')).render(); // => React.createElement(App,{}) // React.createElement("div",null) 1.1 ReactElement ReactElement 객체는 보통 JSX 문법이 React.createElement로 변환되어 생성됩니다. export type ReactElement = { $$typeof: any, type: any, // 노드 종류 (일반 DOM 노드[div, span,...], 리액트 컴포넌트[함수형, 클래스형, Portal, Fragment,Provider,Consumer]) key: any, // diff 알고리즘에 사용(변경됐는지 확인) ref: any, props: any, // or for string refs _owner: any, // 이 객체를 생성한 Fiber 노드 (초기에는 null) // _store: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail _debugInfo: null | ReactDebugInfo, _debugStack: Error, _debugTask: null | ConsoleTask, }; 1.2 React Component Component는 Class Component를 의미합니다. ReactElement의 한 종류이기도 합니다. Class 타입이라 render나 setState 같은 메서드를 가집니다. Reconcile 단계에서 Fiber 노드로 변환되고 render가 호출되어 자식 ReactElement 들을 만듭니다. class Component { static contextType?: Context | undefined; // 구독중인 컨텍스트 타입 (런타임에 결정) static propTypes?: any; // 삭제 예정이 프로퍼티 context: unknown; // 구독중인 컨텍스트 값 constructor(props: P); setState( state: ((prevState: Readonly, props: Readonly) => Pick | S | null) | (Pick | S | null), callback?: () => void, ): void; forceUpdate(callback?: () => void): void; render(): ReactNode; // 컴포넌트 렌더링 readonly props: Readonly; state: Readonly; } class App extends React.Component { render() { return ( header footer ); } } class Counter extends React.Component { state = { count: 0, }; handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <> {this.state.count} +1 ); } } // babel에 의해 jsx 컴파일 후 class Appdefault.a.Component { render() { return // react_default.a.createElement( 'div', { className: 'app', }, react_default.a.createElement('header', null, 'header'), reactCounter, null), react_default.a.createElement('footer', null, 'footer'), ); } } class APPdefault.a.Component { state = { count: 0, }; handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return reactdefault.a.Fragment, null, react_default.a.createElement('p',null, this.state.count), react_default.a.createElement('button', { onClick: this.handleClick }, '+1') ); } } 1.3 React FunctionComponent Function Component는 Hook을 사용하여 리액트 라이프 사이클에 관여합니다. interface FunctionComponent { (props: P): ReactNode; propTypes?: any; // React에서 사용하지 않음. 제거 예정 displayName?: string | undefined; // 디버깅에 사용. } 리액트 reconciler는 React의 중추라고 볼 수 있습니다. Scheduler와 react-dom(혹은 react-native)를 연결하는 역할을 하며, Fiber 트리를 생성하여 상태관리를 합니다. 2.1 Fiber Fiber 객체는 "곧 렌더링될" 또는 "이미 렌더링된" 하나의 컴포넌트(ReactElement)를 의미합니다. 하나의 컴포넌트는 더블 버퍼 구조에따라 2개의 Fiber를 가집니다. (현재 렌더링된 current Fiber와 업데이트 준비중인 workInProgress Fiber를 가짐. alternate가 workInProgress에서 작업해서 상태가 바뀔 Fiber라 보면 됨) export type Fiber = { tag: WorkTag, //Fiber 노드 종류, 32가지가 있다 (FC, ClassComponent, HostRoot, MemoCOmponent, ForwardRef ...) key: null | string, // ReactElement의 key값 elementType: any, // child reconcilation할때 쓰임. MemoizedComponenet 같은 타입. 보통 ReactElement의 type과 동일 type: any, // FC면 function, Class형이면 class. 이 컴포넌트 렌더링에 쓰이는 함수나 Class 본체. 보통 fiber.elementType과 동일 stateNode: any, // 대응되는 DOM 노드(HostComponent타입이면 DOM 노드, root fiber는 FiberRoot 객체를 가르킴, classComponent는 class 인스턴스 가리킴) // Conceptual aliases // parent : Instance -> return The parent happens to be the same as the // return fiber since we've merged the fiber and instance. // Remaining fields belong to Fiber // The Fiber to return to after finishing processing this one. // This is effectively the parent, but there can be multiple parents (two) // so this is only the parent of the thing we're currently processing. // It is conceptually the same as the return address of a stack frame. return: Fiber | null, // 이 fiber 노드 처리 끝나고 계속 처리할 Fiber, 부모 Fiber를 가르킴. // Singly Linked List Tree Structure. child: Fiber | null, // 첫 번재 자식 Fiber sibling: Fiber | null, // 다음 형제 Fiber index: number, // 형제 노드들 중 이 Fiber의 순서 인덱스. (단일이면 0) // The ref last used to attach this node. // I'll avoid adding an owner field for prod and model that as functions. ref: | null | (((handle: mixed) => void) & {_stringRef: ?string, ...}) | RefObject, // 이 Fiber에 걸린 ref, 단 refCleanup: null | (() => void), // Input is the data coming into process this fiber. Arguments. Props. pendingProps: any, // 새로 들어온 Props, memoizedProps와 비교하여 바뀐지 판단. (diff 알고리즘에 사용) memoizedProps: any, // 이전 렌더링 시 사용했던 props updateQueue: mixed, // 이 Fiber에서 발생한 update를 저장하는 큐 memoizedState: any, // 이전 렌더 시 사용했던 state dependencies: Dependencies | null, // 이 Fiber가 의존하고 있는 항목들(context, events mode: TypeOfMode, // 2진수 bitField로 구성됨. 부모 Fiber로부터 상속되어 자식 트리 전체에 영향. React의 실행 모드 설정관련.(ConcurrentMode, BlockingMode, NoMode 등) // Effect flags: Flags, // 이 Fiber에서 발생한 부수효과(Side Effect). 17버전 이전에는 effectTag라 불림. Commit 단계에서 처리되는 것들. subtreeFlags: Flags, // 17버전 이전에서의 firstEffect, nextEffect를 대체하는 것. 기본 비활성화 deletions: Array | null, // 삭제 예정인 자식 Fiber들을 저장하는 배열 lanes: Lanes, // 이 Fiber가 속하는 업데이트 우선순위 childLanes: Lanes, // 이 Fiber의 모든 자식들이 가진 우선순위 (비트마스킹으로 계산하는 거.) alternate: Fiber | null, // 이 Fiber의 짝(더블 버퍼 구조에서 다른 버퍼를 의미) actualDuration?: number, // 이 Fiber와 그 하위 트리를 렌더링하는데 걸린 총 시간 actualStartTime?: number, //이 Fiber의 렌더링 시작 시간. selfBaseDuration?: number, // 이 Fiber 단독 렌더링에 걸린 시간. treeBaseDuration?: number, // 이 Fiber와 모든 하위 트리의 렌더링 시간 총합 (complete phase 동안 계산됨) // DEV 디버깅 용 _debugInfo?: ReactDebugInfo | null, _debugOwner?: ReactComponentInfo | Fiber | null, _debugStack?: string | Error | null, _debugTask?: ConsoleTask | null, _debugNeedsRemount?: boolean, _debugHookTypes?: Array | null, // renders 도중 hooks 순서 바꼈는지 검사용 }; 1.2에서 그린 컴포넌트를 Fiber Tree로 그리면 다음과 같이 나타납니다. 아래 그림을 통해 ReactElement 트리와 Fiber 트리를 비교해보겠습니다. FiberTree는 HostRootFiber인 루트 노드에서 시작합니다.(실제로는 current HostRootFiber와 workInProgress(또는 alternate) HostRootFiber 2개가 있을 것입니다.) , 는 ClassComponent 타입의 Fiber Node를 가지고 있고, 나머지는 일반적인 HostComponent 타입의 Fiber Node를 가집니다. 그리고 Fiber Tree와 ReactElement Tree의 차이점은 React.Fragment 엘레멘트는 Fiber 노드에서 대응되는 타입의 노드가 없습니다. 이는 reconcile 단계에서 Fragment타입의 노드가 별도 처리되기 때문에 일대일 대응되지 않는다고 합니다. 2.2 Update, UpdateQueue Fiber 객체에는 UpdateQueue라는 필드가 존재합니다. 이 큐는 LinkedList 자료 구조로 되어있습니다. export type Update = {| lane: Lane, // update가 속한 우선순위 tag: 0 | 1 | 2 | 3, // UpdateState, ReplaceState, ForceUpdate, CaptureUpdate를 나타냄.(업데이트 종류) payload: any, // 업데이트가 필요한 데이터, 상황에 따라 콜백 함수 또는 객체로 설정 가능 callback: (() => mixed) | null, // 콜백 함수 next: Update | null, // 리스트에서 다음 업데이트를 가리킴, UpdateQueue는 원형 링크드 리스트이므로 마지막 update.next는 첫 번째 update 객체를 가리킴 |}; type SharedQueue = {| pending: Update | null, lanes: Lanes, hiddenCallbacks: Array mixed> | null, |}; export type UpdateQueue = {| baseState: State, // 이 큐의 기본 상태 firstBaseUpdate: Update | null, // 기본 큐의 첫 번째 업데이트 lastBaseUpdate: Update | null, // 기본 큐의 마지막 업데이트 shared: SharedQueue, // 공유 큐, 대기 중인 업데이트 큐로 setState가 호출되면 새로운 업데이트 객체가 이 큐에 생성 callbacks: Array mixed> | null, // 콜백 함수가 있는 update 객체를 저장. commit 이후, 순차적으로 콜백 함수가 호출됨(이펙트) |}; 그림으로 나타내면 다음과 같습니다. 2.3 Hook Hook은 함수형 컴포넌트의 상태 관리를 위해 사용됩니다. 클래스형 컴포넌트의 state를 사용하는 것과 같습니다. 자주 사용되는 Hook으로는 useState, useEffect 같은 것이 있습니다. 리액트 19버전 기준으로 총 21가지의 hook 존재합니다. 아래는 훅 관련 타입들 입니다. export type Hook = {| memoizedState: any, // 메모이즈된 상태, 최종적으로 fiber 트리에 출력됨 baseState: any, // 기본 상태, Hook.queue가 업데이트되면 baseState도 업데이트됨 baseQueue: Update | null, // 기본 상태 큐, reconciler 단계에서 상태 병합을 지원함 queue: UpdateQueue | null, // Update 큐를 가리킴 19버전에서는 any 타입 next: Hook | null, // 해당 함수형 컴포넌트의 다음 Hook 객체를 가리킴, 여러 Hook이 연결된 형태로 링크드 리스트를 구성 |}; export type Update = { lane: Lane, // 업데이트의 우선순위 revertLane: Lane, // 업데이트를 되돌리는 용도 action: A, // 액션 hasEagerState: boolean, eagerState: S | null, // eager 상태 next: Update, // 링크드 리스트에서 다음 업데이트 객체를 가리킴 }; type UpdateQueue = { pending: Update | null, // 대기 중인 업데이트 lanes: Lanes, // 우선순위 확인 레인 dispatch: ((A) => mixed) | null, // 디스패치 함수 lastRenderedReducer: ((S, A) => S) | null, // 마지막 렌더링 리듀서 lastRenderedState: S | null, // 마지막 렌더링 상태 }; Hook.queue와 Hook.baseQueue는 Hook 객체가 제대로 업데이트될 수 있도록 보장하는 역할을 합니다. 또한 Hook들은 링크드 리스트로 구성되어 있기 때문에 항상 순서가 보장되어야 하고 이것이 if문 안에 hook을 못쓰는 이유이기도 합니다. 또한 Hook과 fiber의 관계를 살펴보면 fiber 객체에는 fiber.memoizedState라는 속성이 있으며, 이는 fiber 노드의 상태를 가리킵니다. 함수형 컴포넌트에서는 fiber.memoizedState가 Hook 리스트를 가리키며, 이 Hook 리스트는 함수형 컴포넌트의 상태를 저장합니다. 함수형 컴포넌트에서 memoizedState가 어떤 구조로 저장되는지 예시 그림을 보여드리겠습니다. 클래스형 컴포넌트의 경우엔 Fiber.memoizedState에 클래스 인스턴스의 this.state 복사본이 할당될 것입니다. scheduler 패키지는 작업을 스케줄링하고 내부적으로 작업 큐(taskQueue)를 가집니다. 이 큐는 최소 힙 배열(min-heap array)로 구현되어 있고, 그 안에는 task 객체가 저장됩니다. 3.1 Task scheduler 패키지에서 Task 객체는 아래와 같은 타입을 갖습니다. Task는 최소 힙 배열을 통해 task의 우선순위가 결정됩니다. 또한 Priority에 따라 지연되는 작업과 바로 실행되는 작업에 따라 TimerQueue와 TaskQueue에 Task가 추가됩니다. TimerQueue에서는 일정 시간 경과후 지연 작업들이 TaskQueue로 이동되고 실행되게 됩니다. type Task = { id: number, // 고유 식별자 callback: Callback | null, // react-reconciler 패키지에서 제공하는 콜백 함수로 핵심 priorityLevel: PriorityLevel, // 우선 순위 startTime: number, // 작업 시작 시간 (작업 생성 시간 + 지연 시간) expirationTime: number, // 만료 시간 (startTime + PrioirityLevel에 따른 가중치 시간) sortIndex: number, // 큐 내에서 task의 인덱스. isQueued?: boolean, // 큐 여부. DEV 환경 전용 }; 아래는 최소 이진 힙 큐 자료구조로 담긴 Task들의 예시 그림입니다. 정리 이렇게 각 패키지의 자주 쓰이는 객체들을 정리해 보았습니다. 아직 내부적으로 어떻게 돌아가는지는 설명하지 않았지만, 대략적인 구조는 파악할 수 있는 시간이였던 것 같습니다. 다음 편에는 리액트에서 가장 중요한 reconciler에 대해 설명하겠습니다.
2025-04-280015 - React
[리액트 19] 2편. React WorkLoop
React WorkLoop 1편에서 React 핵심 패키지 간의 의존성과 호출 관계를 그림을 그리며 소개했습니다. 개요도에는 2개의 WorkLoop를 볼 수 있는데, 각각 scheduler와 react-reconciler 패키지에 있습니다. 2편에서는 두 루프에 대해서 설명하려고 합니다. 이 글에선 두 루프는 각각 작업 스케줄 루프(Scheduler.js)와 fiber 구성 루프(ReactFiberWorkLoop.js)라고 표현하겠습니다. 자세한 소스 코드 분석은 scheduler의 스케줄링 매커니즘과 fiber 트리 구성에 관하여 다룰 때 설명하고, 거시적 관점에서 분석하겠습니다. 1.1 작업 스케줄 루프 (Scheduler workLoop) 작업 스케줄 루프는 React 앱이 동작하기 위한 핵심입니다. 반복적으로 호출되는 모든 작업(task)의 스케줄을 우선순위에 따라 제어합니다. 우선순위에 따라 expiration time이 결정되고, 이값을 기준으로 min-heap 정렬이 적용됩니다. 1.2 fiber 구성 루프 (fiber workLoop) fiber 구성 루프는 fiber 트리의 구성을 제어하며, 전체 과정은 DFS(깊이 우선 탐색)으로 이루어집니다. 1.3 두 루프의 공통점, 차이점, 연관성 공통점 두 루프는 javaScript 소스 코드 상에서 실행 시점에서도 클로저 형태를 유지하고 있습니다. 내부에 정의된 전역 변수들은 해당 Scope의 private 변수이며 앱 실행을 제어하는 변수로 사용됩니다. 차이점 작업 스케줄 루프는 이진 힙 구조를 기반으로 힙의 루트 노드를 반복 실행하다가 힙이 비워질 때까지 계속 실행합니다. 스케줄링 기능만 하며, 내부 callback 함수의 기능은 무엇인지 알지 못합니다. fiber 작업 루프는 트리 구조 기반입니다. Root부터 DFS 알고리즘으로 child 연관성 fiber 구성 루프는 작업 스케줄 루프에서 실행되는 task의 일부입니다. 둘은 종속 관계이고 각 작업마다 새로운 fiber 트리를 다시 구성합니다. 위 설명한 내용처럼 두 개의 루프는 다음과 같은 분업 구조를 가집니다. 작업 스케줄 루프 : task를 스케줄링 fiber 구성 루프 : task의 실제 실행 담당. React 앱의 실행 흐름은 입력 -> 출력으로 변환하는 과정이 위 두 workLoop를 중심으로 이루어 집니다. 이를 다시 한번 순서대로 요약해보겠습니다. 입력(Input) : DOM 노드의 업데이트(노드 추가, 삭제, 수정 등)를 목표로 하는 "업데이트 요구"라고 볼 수 있습니다. 작업(Task) 등록 : react-reconciler는 "업데이트 요구"를 받으면 즉시 fiber 트리를 구성하지 않고 scheduler에 새 작업(task)를 등록합니다. 간단히 말해 "업데이트 요구"를 task로 변환합니다. 작업(Task) 실행 : Scheduler는 작업 스케줄 루프를 통해 task를 실행합니다. 그러면 task의 callback은 react-reconciler에서 담은 callback이기 때문에 다시 react-reconciler로 돌아옵니다. fiber 트리 구성 : fiber 구성 루프는 task 실행의 일부로 fiber 트리를 구성합니다. commitRoot : task 실행의 일부로 최신 fiber 트리를 실제 DOM에 렌더링 합니다. 그러면 task가 완료됩니다. 이렇게 입력 -> 출력 까지 전체 체인이 주 실행 흐름입니다. React는 이 흐름에서 더 나은 성능을 위해서 다양한 최적화 전략(Batch update, Interruptable rendering)을 구현합니다. 그 중 하나가 작업 스케줄 루프와 fiber 구성 루프의 협업 구조로 가능한 중단 가능한 렌더링 입니다. 이번 글에서는 두 workLoop를 거시적인 관점에서 설명했습니다. 이 두개의 루프를 중심으로 React 앱의 실행 흐름을 요약할 수 있었고 이 실행 흐름으로 중단 가능한 렌더링이 구현될 수 있음을 설명했습니다. react-reconciler와 scheduler 패키지는 코드량이 많고 복잡하지만 리액트의 주 실행 흐름을 지원하는 패키지이기 때문에 파악만하면 더 쉽게 파악할 수 있습니다.
2025-04-090024 - React
[리액트 19] 1편. 패키지 구조
React 패키지 구조 (웹 개발) 혼자서 리액트 코드도 뜯어보고, 잘 만들어진 중국어 리액트 설명도 많이 차용했습니다. 설명이 틀릴 수도 있지만, 최대한 맞는 설명이 되도록 많이 자료를 찾아보고 코드도 보면서 만들었으니 도움이 되시면 좋겠습니다. 현재 댓글 기능이 없어서 크로스 체크가 불가 합니다. 얼른 댓글 관리할 수 있는 백오피스를 만들어 함께 의견을 공유할 수 있도록 노력하겠습니다. 리액트 프로젝트는 packages 디렉토리에 총 37개(@19.1.0 기준)의 패키지가 있습니다. 이 중 웹 개발을 위해 사용자가 설치하는 기본 패키지는 2개이고 기본 패키지를 구현하는데 필요한 내부 구현 패키지가 4개가 있습니다. 사용자가 설치하는 기본 패키지는 react, react-dom 패키지입니다. 내부 구현 패키지로 shard, react-dom-bindings, react-reconciler, scheduler 4개가 있습니다. 1.1 react 패키지 React의 기본 패키지로 React Component(React Element)를 정의하는 데 필요한 핵심 함수들만을 제공합니다. 단독으로는 사용되지 않고, 보통 Renderer(react-dom, react-native)과 함께 사용합니다. React 애플리케이션 작성 시 대부분 이 패키지의 API를 사용합니다. 1.2 react-dom 패키지 Renderer 중 하나로, React와 플랫폼(브라우저) 간의 연결 다리 역할을 합니다. Browser나 Node.js 환경에서 사용할 수 있습니다. 내부 구현 패키지로 react-dom-bindings를 사용합니다. React-Reconciler에서 만들어낸 결과를 실제 웹 UI로 출력하는 기능을 담당합니다. 이 패키지는 다음 API 한줄로 설명할 수 있습니다. //React 17 까지 ReactDOM.render(, document.getElementById('root')); // React 18 이후 ReactDOM.createRoot(document.getElementById('root')).render(); 1.3 react-reconciler 패키지 React가 작동할 수 있도록 하는 핵심 패키지입니다. react-dom, react, scheduler 간의 호출과 협업을 조율합니다. React 애플리케이션의 상태 입력과 결과 출력을 관리하며, 입력된 데이터를 Renderer가 사용할 수 있는 형식으로 변환해 전달합니다. 역할 입력을 받아들인 후(scheduleUpdateOnFiber), Fiber 트리 생성 로직을 하나의 콜백 함수로 캡슐화합니다(여기에는 Fiber 트리 구조, fiber.updateQueue 큐, 조정 알고리즘 등이 포함됩니다). 콜백 함수(performSyncWorkOnRoot 또는 performConcurrentWorkOnRoot)를 scheduler에 전달하여 스케줄링하도록 합니다. scheduler는 콜백 함수 실행 시점을 제어하며, 콜백 함수가 실행을 마치면 새로운 Fiber 트리를 얻게 됩니다. 그 다음 렌더러(예: react-dom, react-native 등)를 호출하여, Fiber 트리 구조를 실제 화면에 반영합니다. 1.4 scheduler 스케줄링 로직의 핵심 구현체로, react-reconciler가 넘긴 콜백 함수의 실행 시점을 조정합니다. Concurrent 모드에서는 이 스케줄러 덕분에 작업 분할(타임 슬라이싱)을 통해 렌더링을 중단할 수 있습니다. 역할 react-reconciler가 전달한 콜백을 실행. 콜백의 실행 타이밍을 조절하여 작업을 분할할 수 있게 해줘서 중단 가능한 렌더링을 구현할 수 있게 해줍니다.(Concurrent 모드) 1.5 react-dom-bindings react-dom의 내부 구현 패키지로 React에서 DOM환경(브라우저)와 연결해주는 다리 역할을 합니다. React18 버전부터 react-dom으로부터 분리되었습니다. 플랫폼에 따라 다른 renderer 구현이 필요한데, 모바일 환경인 react-native는 react-native-renderer를 사용하고, 브라우저는 readt-dom으로 react-dom-bindings를 내부 구현으로 사용합니다. 역할 react-dom에서 내부 구현체로 사용하여 DOM 노드 생성. 실제 DOM 설정 및 업데이트. 1.6 shared React 여러 패키지에서 공통적으로 사용하는 유틸리티, 상수, 구조체 등을 모아놓은 패키지입니다. 또한 사용하는 객체들을 공유하는 패키지입니다. 만약 react 패키지가 useState훅을 사용한다면, react-reconciler 패키지에서 shared에 할당한 useState훅을 사용하는 것입니다. react에서 직접 react-reconciler 패키지를 참조하지 않고 shared 패키지를 거쳐서 가져오게 됩니다. 역할 런타임 중 React 패키지 간의 변수 공유. Fiber 노드의 타입, 상태 관련 상수들 제공. React 내부 전용 유틸리티 함수 제공. React 애플리케이션 전체 구조는 인터페이스 계층(api)와 코어 계층(core) 두 부분으로 나눌 수 있습니다. (비공식적입니다.) 2.1 인터페이스 계층(api) react 패키지가 인터페이스 계층에 해당합니다. 우리가 개발 중에 사용하는 대부분의 API가 이 패키지에서 제공됩니다.(전부는 아닙니다 ex. useFormState는 react-dom 패키지) React 실행 후 렌더링을 변경할 수 있는 기본 동작 3가지는 다음과 같습니다. 클래스 컴포넌트에서 setState() 사용. 함수형 컴포넌트에서 hook을 사용하고, dispatchAction을 통해 hook 객체를 변경. context 변경 (이 또한 setState나 dispatchAction을 사용하긴 함) 위에서 언급한 setState와 dispatchAction 모두 react 패키지에서 제공됩니다. 그리고 우리는 React의 다른 패키지와 상호작용하고 동작시키기 위해서 react 패키지의 api를 사용합니다. 2.2 코어 계층(core) React의 코어 계층은 크게 3부분(scheduler, reconciler, renderer)으로 구성됩니다. scheduler (스케줄러) react-reconciler가 제공하는 콜백 함수를 작업(task) 객체로 포장합니다. 내부적으로 task queue(작업 큐)를 유지하며, 우선순위가 높은 작업이 앞에 위치합니다. 작업 큐를 순회하며 작업을 하나씩 처리하고, 큐가 빌 때까지 실행합니다. reconciler (리컨실러) 개발자가 선택한 renderer를 장착합니다. (HostConfig(렌더링 인터페이스)가 구현된 객체. ex. react-dom, react-native) react와 react-dom에서 발생한 업데이트 요청 수신 Fiber 트리 구성 과정을 콜백 함수로 감싸 scheduler로 전달. renderer (렌더러) React 앱 시작(createRoot, render) HostConfig를 구현하여 fiber 트리를 실제 DOM 구조로 렌더링 브라우저 환경 : DOM 생성 SSR : 문자열 생성 HostConfig HostConfig는 실제 렌더링을 하는 인터페이스라고 생각하면 됩니다. 예를 들면, rendering이라는 함수를 인터페이스로 구현을 하는데 react-dom은 DOM 객체를 추가하는 방식으로 구현하고, react-native는 모바일 확경에 맞는 객체를 추가하는 방식으로 구현될 것입니다. 다음은 React의 핵심 세 가지 패키지의 주요 역할과 호출 관계를 하나의 개요도로 표현했습니다. 빨간색은 진입 함수, 초록색은 출구 함수 입니다. 거시적인 아키텍처 관점에서 패키지들 간의 의존성과 호출 관계를 설명하는 그림을통해 먼저 전체 구조를 파악하고 함께 각 모듈을 공략해 나갑시다!!
2025-04-080023