useState란?
React 공식 문서에서는 useState를 "컴포넌트에 상태 변수를 추가할 수 있게 해주는 Hook"이라고 정의하고 있습니다. 쉽게 말하면, useState를 사용하면 컴포넌트 안에서 데이터를 저장하고 관리할 수 있습니다.
단순히 데이터를 컴포넌트에 넣는 것만으로는 상태가 변경되더라도 UI에 변경 사항이 반영되지 않습니다. 그러나 useState를 사용하면 상태가 변경될 때마다 React가 해당 컴포넌트를 다시 렌더링합니다. 이때 "컴포넌트에 상태 변수를 추가할 수 있게 해주는" 기능이 구현되는 것입니다.
사용 예시
가장 기본적인 사용 방식은 다음과 같습니다:
const [state, setState] = useState(initialValue);
이 Hook은 두 가지를 반환합니다:
- state: 현재 상태 값
- setState: 상태를 업데이트하고 컴포넌트를 다시 렌더링하는 함수
자주 사용되는 패턴들
1. 기본적인 상태 업데이트
가장 기본적인 사용 방식:
const [count, setCount] = useState(0);
setCount(5); // count를 5로 설정
2. 이전 상태 기반 업데이트
새로운 상태가 이전 상태를 기반으로 되어야 할 때 유용합니다:
setCount(prev => prev + 1); // count를 1 증가시킴
3. 지연 초기화 (Lazy Initialization)
초기 값 설정이 복잡하여 성능에 부담을 줄 수 있을 때, 컴포넌트가 리렌더링되어도 초기값 연산 로직이 다시 실행되지 않도록 할 수 있습니다:
const complexCalculation = () => {
const result = [];
//... 복잡한 연산
return result;
};
// 리렌더링마다 복잡한 연산이 다시 실행됩니다.
const [value, setValue] = useState(complexCalculation());
// Lazy Initialization 방식 - 함수로 전달하여 복잡한 연산이 최초 렌더링 시에만 실행되도록 합니다.
const [lazyValue, setLazyValue] = useState(() => complexCalculation());
어떻게 이런 패턴을 사용할 수 있을까?
단순히 다양한 패턴으로 사용하기만 했던 useState와 관련된 내부 코드를 보며 어떻게 위와 같은 패턴들로 사용할 수 있었던 것인지 살펴보겠습니다.
ReactSharedInternals
React에서 ReactSharedInternals는 내부적으로 H(Hooks Dispatcher) 를 관리합니다. 이를 통해 React는 각 컴포넌트의 상태 및 Hooks 동작을 관리합니다. 초기 렌더링과 리렌더링 시에 각각 다르게 Dispatcher가 설정됩니다.
// packages/react/src/ReactSharedInternalsClient.js
const ReactSharedInternals: SharedStateClient = ({
H: null,
A: null,
T: null,
S: null,
}: any);
renderWithHooks를 통해 Dispatcher 할당
React는 컴포넌트를 렌더링할 때, ReactSharedInternals.H에 HooksDispatcherOnMount 또는 HooksDispatcherOnUpdate를 할당합니다. 처음 렌더링될 때는 HooksDispatcherOnMount, 리렌더링될 때는 HooksDispatcherOnUpdate가 각각 할당되어 적절한 Hook 동작을 관리합니다.
// 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 {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
if (__DEV__) {
// DEV 모드일 때 처리 로직
} else {
ReactSharedInternals.H = current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
let children = Component(props, secondArg);
// 렌더링 중에 업데이트가 발생했는지 확인 후, 다시 렌더링 필요 시 반복
if (didScheduleRenderPhaseUpdateDuringThisPass) {
children = renderWithHooksAgain(workInProgress, Component, props, secondArg);
}
finishRenderingHooks(current, workInProgress, Compon
ent);
return children;
}
mountStateImpl 함수
최초로 useState를 호출할 때, Lazy Initialization 패턴을 지원합니다. useState가 함수형 인자를 받으면 해당 함수를 실행하여 나온 결과 값으로 상태가 초기화됩니다.
리렌더링 시에는 이 시점에서 할당된 hook.memoizedState만 보기 때문에 Lazy Initialization 패턴이 가능하게 됩니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
initialStateInitializer();
setIsStrictModeForDevtools(false);
}
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
return hook;
}
mountState 함수
mountState는 처음 렌더링 시에 상태를 초기화하는 함수입니다. mountStateImpl 를 통해 Lazy Initialization을 지원하며, 상태 업데이트를 위한 dispatch 함수를 반환합니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispath];
}
updateReducer 함수
리렌더링 시에 호출되는 함수로, 상태를 업데이트하는 로직을 담당합니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
updateReducerImpl 함수
리렌더링 시, 기존 상태를 기반으로 새로운 상태를 계산하는 로직입니다. 상태가 업데이트되는 과정에서 대기 중인 상태 변화를 처리하며, 리듀서를 통해 새로운 상태를 계산합니다.
newState = reducer(newState, action);
부분을 통해 setState(prev => ())에서 prev가 이전 값을 가리키게 됩니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
const queue = hook.queue;
if (queue === null) {
throw new Error('업데이트 큐가 없습니다.');
}
queue.lastRenderedReducer = reducer;
let baseQueue = hook.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
const baseState = hook.baseState;
let newState = baseState;
if (baseQueue !== null) {
const first = baseQueue.next;
let update = first;
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
hook.baseState = newState;
hook.baseQueue = baseQueue;
}
return [ook.memoizedState, queue.dispatch];
}
updateState 함수
useState가 리렌더링 시 실행되는 함수입니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
기본 리듀서: basicStateReducer
basicStateReducer는 useState에서 상태를 계산하는 기본 리듀서입니다. 전달된 action이 함수일 경우 첫 번째 인자인 state를 인자로 하여 action을 실행한 값을 반환합니다.
updateReducerImpl 에서 사용되는 reducer로 사용되고, 위에 언급했듯 newState = reducer(newState, action); 부분을 통해 첫 번째 인자로 이전 값을 할당하여 setState(prev => ()) 패턴을 지원합니다.
// packages/react-reconciler/src/ReactFiberHooks.js
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
useState 함수
React의 useState는 내부적으로 Dispatcher를 사용하여, 렌더링 시점에 따라 mountState 또는 updateState가 호출되도록 관리됩니다.
// packages/react/src/ReactHooks.js
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
resolveDispatcher 함수는 현재 Dispatcher가 무엇인지 확인하여 적절한 Hook을 반환합니다.
// packages/react/src/ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
return dispatcher;
}
이로써, useState가 어떻게 작동하는지에 대한 전체적인 로직을 정리할 수 있었습니다. mountState와 updateState 함수, 그리고 이를 뒷받침하는 기본 리듀서 및 Dispatcher의 역할을 통해 React의 상태 관리가 이루어지는 과정을 이해할 수 있습니다.
위 코드는 a8fc4b1ef8149a0f2b55942683bf96409e3f313f 커밋 기준으로 작성되었으며, 글을 읽는 시점과 다를 수 있으니 대략적인 방향만 확인하시길 권장드립니다.
잘못 해석한 부분이 있거나 궁금한 점은 댓글 부탁드려요!
'React' 카테고리의 다른 글
React-Router 기본 원리 알아보기 (0) | 2024.11.18 |
---|---|
useCallback 알아보기 (2) | 2024.10.14 |
useRef 알아보기 (0) | 2024.10.13 |