[React 까보기] 5. setState (feat. dispatchAction)

useState를 호출한다는 것은 mountState 함수를 호출하는 것과 같다는 것을 이전 포스팅에서 알아보았다.
따라서 우리가 잘 알고있는 setState를 통해 상태를 업데이트 하는 과정은 dispatch 함수를 분석한다면 알 수 있을 것 이다.
위에서 말한 dispatch 함수에는 dispatchAction.bind() 함수의 값이 할당 되어 있으므로 dispatchAction을 분석해보겠다.

1️⃣ dispatchAction 함수

 function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // 3. update 소비중 다시 update 발생
  // renderWithHooks 내부에서 1씩 증가시켰던 numberOfRerenders의 값을 이용하여 에러를 내보냄
  invariant(
    numberOfReRenders < RE_RENDER_LIMIT,
    'Too many re-renders. React limits the number of renders to prevent ' +
      'an infinite loop.',
  );

  // idle 상태(일을하지않는, 업데이트가 없는 상태)와 Render Phase를 구분하는 조건문
  // 아래 두가지 Fiber 노드를 전부 확인해야 실제로 Render Phase인지 확인 가능
  // - currentlyRenderingFiber => 업데이트가 일어나고 있는 Fiber 노드(current Fiber 노드)
  // - alternate => VDOM 구조에서 current fiber의 참조복사가 되어있는 Fiber 노드(workInProgress Fiber 노드)

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
      // render phase 업데이트가 실행된다 라는 의미
    didScheduleRenderPhaseUpdate = true;

    // 0. update 객체 생성
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime,
      suspenseConfig: null,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // 1. update의 저장
    // Render Phase에서 발생한 업데이트를 저장
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }

    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);

    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } 

    else {
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }

    // 2. update의 소비 => renderWithHooks에서 didScheduleRenderPhaseUpdate의 값을 이용
  } else {
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    // 1. update 객체 생성
    const update: Update<S, A> = {
      expirationTime, // work가 진행될때 값을 할당함
      suspenseConfig,
      action, // setState의 인자 값
      eagerReducer: null, // 불필요 렌더링 최적화 단계와 관련이 있음(1)
      eagerState: null, // 불필요 렌더링 최적화 단계와 관련이 있음(2)
      next: null, // update 객체도 linked list 형태로 저장됨
    };

    // 2. update 객체를 큐에 저장
    // 아래 로직은 원형 큐를 구현하여 새로운 update 객체를 넣는 로직
    // 원형 큐로 구현한 이유는 최적화와 관련이 있음!
    const last = queue.last;

    // last === null 이면 큐가 비어있다는 뜻(최초)
    if (last === null) {
      update.next = update; // 자기 자신을 참조시킴(초기 원형 큐 준비단계)
    } 
    // 큐에 update 객체가 존재할때(최초가 아닐때)
    else {
      const first = last.next; // 첫번째 노드(update객체)

      if (first !== null) {
        update.next = first; // 새로운 update 객체에 next로 큐의 첫번째 노드를 연결
      }

      last.next = update; // 큐의 마지막을 새로운 update 객체로 변경
    }
    // update 객체를 큐에 연결
    queue.last = update;

    // 3. 불필요 렌더링 발생하지 않도록 최적화
    // 아래 조건이 만족하지 않는 경우 return (함수 중지)
    // - WORK 스케줄링 X
    // - action의 결과값 === 현재 상태값
    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer; // basicStateReducer
      if (lastRenderedReducer !== null) {
        let prevDispatcher;

        try {
          const currentState: S = (queue.lastRenderedState: any); // 마지막으로 렌더된 값
          const eagerState = lastRenderedReducer(currentState, action); // 바뀌어야 할 state 값

          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;

          // 상태가 같다면 함수 종료
          if (is(eagerState, currentState)) {
            return;
          }
        }
      }
    }

    // 4. WORK를 스케줄링
    // Render Phase 진입점
    scheduleWork(fiber, expirationTime);
  }
}
  • idle 상태가 아닐때의 setState(Render Phase 일때)
    1. update의 저장
    2. update의 소비 => renderWithHooks 함수에서 update를 소비함
      (renderWithHooks 내의 didScheduleRenderPhaseUpdate 조건문 부분 확인)
    3. update 소비 중에 다시 update의 발생
      (renderWithHooks 내의 didScheduleRenderPhaseUpdate 조건문 부분 확인)
  • idle 상태일때의 setState
    1. update 객체 생성
    2. update를 queue 객체에 저장
    3. 불필요한 렌더링 발생하지 않도록 최적화
    4. update를 적용하기 위해 WORK를 Scheduler에 예약(scheduling)
      • WORK: Reconciler가 컴포넌트의 변경을 DOM에 적용하기 위해 수행하는 일

📚 레퍼런스