[ Frontend > React ]
[리액트 19] 3편. React에서 자주 사용되는 객체
3편. React에서 자주 사용되는 객체
React에는 다양한 객체와 데이터 구조가 등장합니다. 2편에서 소개한 fiber tree나 WorkLoop의 time min-heap 자료구조인 task schedule queue가 그 예시들 입니다. 3편에서는 리액트가 시작되고 렌더링될때 까지 자주 등장하는 중요한 객체들을 패키지별로 정리해보려 합니다.
1. react 패키지
react 패키지는 ReactElement(리액트 엘리먼트)를 생성하는 필수 함수들과 API를 제공합니다. 예를 들면 다음과 같은 JSX 문법들은 모두 ReactElement를 생성하는 createElement로 Babel 플러그인에 의해 바뀝니다.
ReactDOM.createRoot(document.getElementById('root')).render(<App />); // <App/> => React.createElement(App,{})<div/> // 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, // __DEV__ or for string refs _owner: any, // 이 객체를 생성한 Fiber 노드 (초기에는 null) // __DEV__ _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<P, S> { static contextType?: Context<any> | undefined; // 구독중인 컨텍스트 타입 (런타임에 결정) static propTypes?: any; // 삭제 예정이 프로퍼티 context: unknown; // 구독중인 컨텍스트 값 constructor(props: P); setState<K extends keyof S>( state: ((prevState: Readonly<S>, props: Readonly<P>) => Pick<S, K> | S | null) | (Pick<S, K> | S | null), callback?: () => void, ): void; forceUpdate(callback?: () => void): void; render(): ReactNode; // 컴포넌트 렌더링 readonly props: Readonly<P>; state: Readonly<S>;}
class App extends React.Component { render() { return ( <div className="app"> <header>header</header> <Counter /> <footer>footer</footer> </div> ); }}class Counter extends React.Component { state = { count: 0, }; handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <> <p>{this.state.count}</p> <button onClick={this.handleClick}>+1</button> </> ); }}// babel에 의해 jsx 컴파일 후class App_App extends react_default.a.Component { render() { return /*#__PURE__*/ react_default.a.createElement( 'div', { className: 'app', }, react_default.a.createElement('header', null, 'header'), react_default.a.createElement(App_Counter, null), react_default.a.createElement('footer', null, 'footer'), ); }}class APP_Counter extends react_default.a.Component { state = { count: 0, }; handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return react_default.a.createElement(react_default.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<P = {}> { (props: P): ReactNode; propTypes?: any; // React에서 사용하지 않음. 제거 예정 displayName?: string | undefined; // 디버깅에 사용. }
2. react-reconciler 패키지
리액트 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<Fiber> | 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<HookType> | null, // renders 도중 hooks 순서 바꼈는지 검사용};
1.2에서 그린 컴포넌트를 Fiber Tree로 그리면 다음과 같이 나타납니다.

아래 그림을 통해 ReactElement 트리와 Fiber 트리를 비교해보겠습니다.

FiberTree는 HostRootFiber인 루트 노드에서 시작합니다.(실제로는 current HostRootFiber와 workInProgress(또는 alternate) HostRootFiber 2개가 있을 것입니다.) <App/>, <Counter/>는 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<State> = {| lane: Lane, // update가 속한 우선순위 tag: 0 | 1 | 2 | 3, // UpdateState, ReplaceState, ForceUpdate, CaptureUpdate를 나타냄.(업데이트 종류) payload: any, // 업데이트가 필요한 데이터, 상황에 따라 콜백 함수 또는 객체로 설정 가능 callback: (() => mixed) | null, // 콜백 함수 next: Update<State> | null, // 리스트에서 다음 업데이트를 가리킴, UpdateQueue는 원형 링크드 리스트이므로 마지막 update.next는 첫 번째 update 객체를 가리킴|};type SharedQueue<State> = {| pending: Update<State> | null, lanes: Lanes, hiddenCallbacks: Array<() => mixed> | null,|};export type UpdateQueue<State> = {| baseState: State, // 이 큐의 기본 상태 firstBaseUpdate: Update<State> | null, // 기본 큐의 첫 번째 업데이트 lastBaseUpdate: Update<State> | null, // 기본 큐의 마지막 업데이트 shared: SharedQueue<State>, // 공유 큐, 대기 중인 업데이트 큐로 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<any, any> | null, // 기본 상태 큐, reconciler 단계에서 상태 병합을 지원함 queue: UpdateQueue<any, any> | null, // Update 큐를 가리킴 19버전에서는 any 타입 next: Hook | null, // 해당 함수형 컴포넌트의 다음 Hook 객체를 가리킴, 여러 Hook이 연결된 형태로 링크드 리스트를 구성|};export type Update<S, A> = { lane: Lane, // 업데이트의 우선순위 revertLane: Lane, // 업데이트를 되돌리는 용도 action: A, // 액션 hasEagerState: boolean, eagerState: S | null, // eager 상태 next: Update<S, A>, // 링크드 리스트에서 다음 업데이트 객체를 가리킴};type UpdateQueue<S, A> = { pending: Update<S, A> | 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 복사본이 할당될 것입니다.
3. Scheduler 패키지
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에 대해 설명하겠습니다.