Skip to content

Latest commit

 

History

History
195 lines (156 loc) · 8.82 KB

2주차_최여진_공통.md

File metadata and controls

195 lines (156 loc) · 8.82 KB

들어가며

React 의 useState 를 통한 상태 변경 흐름에서 초기화와 상태 업데이트 과정을 중심으로 코드를 분석하며 알아보았습니다.

초기화

React는 컴포넌트를 렌더링할 때, 내부적으로 renderWithHooks 함수를 호출해 컴포넌트의 훅을 추적하고 관리합니다.

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가 호출됩니다.

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) 가 호출될 것입니다.

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 구조업데이트 큐를 사용해 관리됩니다.

setState

// 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를 사용하여 이전 상태와 새 상태를 비교합니다. 상태가 변경되지 않았다고 판단되면, 불필요한 리렌더링을 방지합니다.