본문 바로가기
React

useState 깊게 분석하기

by East-K 2024. 10. 10.

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