React 의 useState
를 통한 상태 변경 흐름에서 초기화와 상태 업데이트 과정을 중심으로 코드를 분석하며 알아보았습니다.
React는 컴포넌트를 렌더링할 때, 내부적으로 renderWithHooks 함수를 호출해 컴포넌트의 훅을 추적하고 관리합니다.
// packages/react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// ...
// 훅 디스패처 설정 (마운트 시와 업데이트 시 다르게 작동)
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 마운트할 때 사용하는 훅 디스패처
: HooksDispatcherOnUpdate; // 업데이트할 때 사용하는 훅 디스패처
const children = Component(props, secondArg);
return children;
}
const HooksDispatcherOnMount: Dispatcher = {
// ...
useState: mountState,
useMemo: mountMemo,
useReducer: mountReducer,
};
const HooksDispatcherOnUpdate: Dispatcher = {
// ...
useState: updateState,
useMemo: updateMemo,
useReducer: updateReducer,
};
renderWithHooks
는 컴포넌트가 처음 렌더링되는지(HooksDispatcherOnMount
) 또는 업데이트 중인지(HooksDispatcherOnUpdate
)에 따라 적절한 훅 디스패처를ReactSharedInternals.H
에 설정합니다. => 컴포넌트가 처음 렌더링되는지 혹은 업데이트되는지에 따라 React는 디스패처를 선택적으로 사용해 훅의 동작 방식을 결정하는 것을 알 수 있습니다.current === null
일 경우, 아직 Fiber 노드가 없음을 의미하므로 초기 렌더링 상태입니다.- 이후
Component(props)
가 실행되면서 컴포넌트 내부의useState
가 호출됩니다.
// packages/react/src/ReactHooks.js
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
return ((dispatcher: any): Dispatcher);
}
useState
를 호출하면 이전에renderWithHooks
에서ReactSharedInternals.H
에 설정한 디스패처의 useState 함수 ( =HooksDispatcherOnMount.useState
=mountState
) 가 호출될 것입니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// (1) 새로운 훅을 생성하여 링크드 리스트 형태로 관리되는 훅 리스트에 추가하고, 첫 훅일 경우 현재 Fiber 노드의 memoizedState에 연결
const hook = mountWorkInProgressHook();
// (2) 함수형 초기값이 주어졌을 경우 계산하여 initialState에 저장 (게으른 초기화)
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
}
// (3) memoizedState와 baseState에 초기 상태를 설정
hook.memoizedState = hook.baseState = initialState;
// (4) 상태 업데이트를 관리할 queue 객체를 생성하고 설정
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null, // 상태 변경 요청이 대기 중인 리스트. null -> 대기 중인 상태 업데이트가 없다
lanes: NoLanes, // 우선순위 정보. 초기에는 NoLanes 로 설정됨
dispatch: null,
lastRenderedReducer: basicStateReducer, // 상태 업데이트 시 기존 상태와 업데이트 상태를 비교하고 처리하는 데 사용될 준비값
lastRenderedState: initialState,
};
// (5) 상태 변경을 위한 dispatch 함수(setState 역할)를 생성하여 반환
// 현재 Fiber와 연결된 상태에서 상태 업데이트가 발생할 수 있도록 준비
// setState를 호출할 때 이 dispatch 함수가 실행되어 queue에 업데이트 요청이 추가될 것임
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
훅 생성 및 리스트 연결
->초기 상태 계산 및 저장
->업데이트 큐 생성
->dispatch 함수 생성
->초기값과 상태 변경을 위한 dispatch 함수 반환
의 흐름으로 진행됩니다.- 상태 변경이 일어날 때 React가 업데이트를 어떻게 관리하고 처리할지를 준비하기 위해
queue
,dispatch
를 생성하고 설정합니다. bind
를 사용해dispatchSetState
가 항상currentlyRenderingFiber
(현재 렌더링 중인 컴포넌트의 Fiber 노드)와queue
(상태 업데이트 큐) 를 참조합니다. => 컴포넌트가setState
를 호출할 때마다 해당 Fiber의 상태가 업데이트되도록 보장하기 위한 작업으로 보입니다.
setState
호출 시 이루어지며, 이 과정에서 상태 업데이트가 일괄 처리되도록 예약됩니다. 이 예약 과정은 React의 Fiber 구조
와 업데이트 큐
를 사용해 관리됩니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber); // 우선순위 레인 설정
// 1. 상태 업데이트 객체 생성
// 변경 요청(action)과 우선순위(lane) 정보를 담고 있다.
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};
const alternate = fiber.alternate;
const lastRenderedReducer = queue.lastRenderedReducer;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// lastRenderedReducer를 통해 현재 상태(currentState)와 새로 계산된 상태(eagerState)가 동일한지 비교
if (is(eagerState, currentState)) {
// 동일하면 리렌더링을 생략
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
}
// 2. 업데이트가 큐에 추가되고, 그 컴포넌트 트리의 루트 Fiber를 반환
// 업데이트 큐는 링크드 리스트 형태로 여러 상태 변경 요청을 큐에 담아 대기시킴 (상태 업데이트를 업데이트 큐에 쌓아둠 -> 렌더링 단계에서 한꺼번에 처리하기 위함)
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// 3. 상태 변경이 루트 Fiber에서부터 전파되도록 루트 Fiber에서부터 리렌더링이 예약됨
scheduleUpdateOnFiber(root, fiber, lane);
}
}
- 최적화를 위해
Object.is
(없을 경우 대비하여 폴리필 함수 사용) 를 사용해 함수 현재 상태와 새 상태를 비교하고 필요할 때만 리렌더링을 예약한다는 점을 알 수 있습니다.=> 객체를 비교할 때// packages/shared/objectIs.js function is(x: any, y: any) { return ( (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare ); } const objectIs: (x: any, y: any) => boolean = // $FlowFixMe[method-unbinding] typeof Object.is === 'function' ? Object.is : is;
Object.is
는 두 객체의 참조가 같은지만 확인합니다. 객체가 동일한 참조를 가지면 상태가 바뀌지 않은 것으로 간주하여 리렌더링을 하지 않습니다.
- React는 컴포넌트의 렌더링 상태에 따라 디스패처를 선택해 훅의 동작 방식을 결정합니다.
- 초기 상태를 설정할 때
게으른 초기화(lazy initialization)
방식을 사용합니다. 초기값이 함수로 전달되면, 최초 렌더링 시에만(mountState
) 해당 함수를 호출하여 계산합니다. - 컴포넌트가
setState
를 호출할 때마다 해당 Fiber의 상태가 정확히 업데이트되도록,bind
를 사용해 현재 렌더링 중인 컴포넌트의Fiber 노드
와업데이트 큐
를 고정합니다. setState
가 호출되면, 상태 변경 요청이업데이트 큐
에 추가되어 예약됩니다. 이 큐는 상태 업데이트 요청들을 쌓아 두었다가 한 번의 렌더링 주기에 일괄 처리할 수 있도록 합니다.setState
가 호출될 때 Object.is를 사용하여 이전 상태와 새 상태를 비교합니다. 상태가 변경되지 않았다고 판단되면, 불필요한 리렌더링을 방지합니다.