React 내부 동작원리를 알아보자(5) - Dispatch은 어떻게 상태를 변경하고 컴포넌트를 리렌더링 시킬까?

방구석 코딩쟁이·2024년 1월 24일
0

이 시리즈는 "가장 쉬운 웹개발 with Boaz" 님의 [React 까보기 시리즈] 를 기반으로 만들어졌습니다.

지난번에 mountState에 대해서 살펴보았었습니다.
결국 mountState함수는 statesetState를 반환한다는 사실과 함께, hook 객체를 링크드 리스트 형태로 연결지을 수 있었고, queue객체를 생성하고, queue에는 update 객체들을 Circular 링크드 리스트 형태 연결지어 할당되었다고 이야기할 수 있었습니다.

그리고, setState 함수는 결국 dispatchActioncurrentlyRenderingFiberqueue 객체를 bind한 함수였음을 알 수 있었죠.

그렇다면 dispatchAction이 어떤 함수였는지 알아볼 필요가 있겠습니다.

dispatchAction 함수

setState 함수를 사용하기 위해 dispatchActioncurrentlyRenderingFiberqueue를 bind하는데 이를 통해 저희는 dispatchAction 로직을 수행 할 때, currentlyRenderingFiber(작업 중인 fiber)를 알아야하고, 생성한 queue 객체에 작업을 하나보구나를 추측해볼 수 있습니다.

dispatchActionqueueupdate를 추가해주고 scheduler에게 Work를 예약하는 함수입니다.

들어가기 전에
일단 reconciler 코드에서는 Render phase 도중에 발생하였는지, idle state에서 발생했는지에 대한 분기 처리를 자주 보실 수 있습니다. 두 상황에서 각각 처리하는 방법과 최적화 방식이 약간 다릅니다.

Render Phase update란?
컴포넌트가 렌더링되고 있는 상황에서 추가로 업데이트가 발생할 경우를 말합니다.
아래의 예시를 보도록 합시다.

function MyCounter() {
  const [count, setCount] = useState(0)
  if (count === 1) setCount(2)
  return <button onClick={() => setCount(1)}></button>
}

이 경우는 버튼을 클릭하면 count가 1이 됨과 동시에 setCount(2)를 호출하므로 추가적인 업데이트가 발생합니다.

일단 dispatchAction 전체 소스코드를 보시죠

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  invariant(
    numberOfReRenders < RE_RENDER_LIMIT,
    'Too many re-renders. React limits the number of renders to prevent ' +
      'an infinite loop.',
  );

  if (__DEV__) {
    warning(
      arguments.length <= 3,
      "State updates from the useState() and useReducer() Hooks don't support the " +
        'second callback argument. To execute a side effect after ' +
        'rendering, declare it in the component body with useEffect().',
    );
  }

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.
    didScheduleRenderPhaseUpdate = true;
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    flushPassiveEffects();

    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update: Update<S, A> = {
      expirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }
    if (__DEV__) {
      if (shouldWarnForUnbatchedSetState === true) {
        warnIfNotCurrentlyBatchingInDev(fiber);
      }
    }
    scheduleWork(fiber, expirationTime);
  }
}

 const alternate = fiber.alternate;

먼저 코드는 fiber.alternate 값을 alternate 변수에 할당하는 것으로 시작됩니다.

fiber.alternatealternate 변수에 할당한 코드 바로 다음에는 조건문이 나오게 됩니다.

if (
  fiber === currentlyRenderingFiber ||
  (alternate !== null && alternate === currentlyRenderingFiber)
) {
 ...
} else {
 ...
}
if(fiber === currentlyRenderingFiber ||
  (alternate !== null && alternate === currentlyRenderingFiber))

위 조건은 render phase update인지 idle update인지 구분하는 조건입니다.
저희가 setState를 통해 Work를 Scheduler에 예약을 하면 render Phase가 됩니다.

이 때, currentlyRenderingFiberrenderWithHooks 함수에서 workInProgress로 할당됩니다.
renderWithHooks는 Render phase 중에 호출되는 함수이므로 currentlyRenderingFiber가 존재하는 건 Render Phase가 진행 중이라는 의미가 됩니다.

currentlyRenderingFiber = workInProgress(Fiber) = 작업 중인 Fiber(작업을 하지 않는다면 null)입니다. workInProgress는 업데이트가 진행중인 Fiber를 의미합니다.
currentlyRenderingFiberworkInProgress의 또 다른 이름이라고 보면됩니다.

fiberalternate를 동시에 비교하는 이유가 있을까요?
VDOM은 하나의 노드(컴포넌트)에 대해 currentworkInProgress로 관리한다고 이전 포스트에서 이야기했었습니다. 그리고, dispatchActionfiber를 bind 함수를 통해 고정해두었습니다. currentworkInProgress는 고정이 아닌 Commit phase를 지나면 서로 교체가 됩니다. 이 때문에 현재 작업 중인 currentlyRenderingFibercurrentworkInPrgress인지 알기 어렵습니다. 때문에 fiberalternate를 모두 비교해야지 Render phase update인지 체크할 수 있습니다.

fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber) 이 조건이 false인 경우, 즉, else 블록은 idle 상태입니다.
즉, 일을 안하는 상태(기본 상태)입니다.

idle 상태일 때의 dispatchAction

저희는 먼저 idle 상태일 때의 로직을 파악해보기 위해 우선 else 블록만을 해석해보도록 하겠습니다.

dispatchAction가 idle 상태일 때는 총 4가지의 단계를 수행합니다.
1. update 객체 생성
2. update를 queue에 저장(circular linked list)
3. 불필요한 렌더링이 발생하지 않도록 최적화
4. update를 적용하기 위해 Workscheduler에 예약해야 합니다.

Work란?
이전 포스트에서도 언급했듯이 render phase에서는 reconciliation을 하게 되는데 reconciliationreact element를 추가, 수정, 삭제하는 일을 말합니다.
이때, reconciler가 컴포넌트의 변경을 DOM에 적용하기 위해 수행하는 일을 WORK라고 합니다.
이러한 WORKscheduler에 등록을 하게 됩니다.

else {
  // 이 부분 설명은 나중에 하겠습니다!
  flushPassiveEffects();
  
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, fiber);

  const update: Update<S, A> = {
    expirationTime,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  };

  // Append the update to the end of the list.
  const last = queue.last;
  if (last === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    const first = last.next;
    if (first !== null) {
      // Still circular.
      update.next = first;
    }
    last.next = update;
  }
  queue.last = update;

  if (
    fiber.expirationTime === NoWork &&
    (alternate === null || alternate.expirationTime === NoWork)
  ) {
    // The queue is currently empty, which means we can eagerly compute the
    // next state before entering the render phase. If the new state is the
    // same as the current state, we may be able to bail out entirely.
    const lastRenderedReducer = queue.lastRenderedReducer;
    if (lastRenderedReducer !== null) {
      let prevDispatcher;
      if (__DEV__) {
        prevDispatcher = ReactCurrentDispatcher.current;
        ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
      }
      try {
        const currentState: S = (queue.lastRenderedState: any);
        const eagerState = lastRenderedReducer(currentState, action);
        // Stash the eagerly computed state, and the reducer used to compute
        // it, on the update object. If the reducer hasn't changed by the
        // time we enter the render phase, then the eager state can be used
        // without calling the reducer again.
        update.eagerReducer = lastRenderedReducer;
        update.eagerState = eagerState;
        if (is(eagerState, currentState)) {
          // Fast path. We can bail out without scheduling React to re-render.
          // It's still possible that we'll need to rebase this update later,
          // if the component re-renders for a different reason and by that
          // time the reducer has changed.
          return;
        }
      } catch (error) {
        // Suppress the error. It will throw again in the render phase.
      } finally {
        if (__DEV__) {
          ReactCurrentDispatcher.current = prevDispatcher;
        }
      }
    }
  }
  if (__DEV__) {
    if (shouldWarnForUnbatchedSetState === true) {
      warnIfNotCurrentlyBatchingInDev(fiber);
    }
  }
  scheduleWork(fiber, expirationTime);
}

1) update 객체 생성

const update: Update<S, A> = {
  expirationTime,
  action,
  eagerReducer: null,
  eagerState: null,
  next: null,
};

위처럼 update 객체를 만드는 코드를 확인할 수 있습니다. update 객체는 업데이트에 대한 정보를 가지고 있는 객체입니다. 각 속성에 대한 설명은 다음과 같습니다.

  • expirationTime
    Work와 관련된 값이며 Work가 진행될 때 이 속성에 특정한 값을 할당합니다.
  • action
    setState()의 인자로 넣어주는 값입니다.
  • next
    update 객체가 저장될 때, Circular Linked List 형태로 저장되는데, 다음 update 객체를 연결하기 위한 속성입니다.
  • eagerReducer
    불필요한 렌더링이 발생하지 않도록 최적화하는 것과 관련이 있는 속성입니다.
  • eagerState
    불필요한 렌더링이 발생하지 않도록 최적화하는 것과 관련이 있는 속성입니다.

2) update를 queue에 저장(circular Linked List)

// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
  // This is the first update. Create a circular list.
  update.next = update;
} else {
  const first = last.next;
  if (first !== null) {
    // Still circular.
    update.next = first;
  }
  last.next = update;
}
queue.last = update;

위의 코드는 queueupdate 저장하는 코드입니다.
먼저 queue.last에 값이 없는 경우는 첫 번째 업데이트가 발생했다는 의미이므로 circular list를 만들기 위해 update.next에 자기자신(update)을 할당하고 나서 queue.lastupdate 객체를 넣습니다.

queue.last가 있는 경우, 이미 next에 값이 있다는 의미입니다. 그렇다면 circular linked list로 연결지어줘야 합니다. 이부분의 해석이 조금 어려운데, 쉽게 설명하면 queue.last는 마지막에 들어온 업데이트(update)로 연결하고, update.next는 첫번째로 들어온 업데이트로 연결해주는 것이라고 보면 됩니다.

그림으로 쉽게 설명하면 다음과 같은 순서로 실행됩니다.
firstUpdate, secondUpdate, thirdUpdate 순으로 업데이트가 들어왔다고 가정해본 것입니다.

3) 불필요한 리렌더링이 발생하지 않도록 최적화

리액트는 idle 상태에서 업데이트가 발생한 상황이라면 간단한 성능 최적화를 해줍니다.

 if (
   fiber.expirationTime === NoWork &&
   (alternate === null || alternate.expirationTime === NoWork)
 ) {
   // The queue is currently empty, which means we can eagerly compute the
   // next state before entering the render phase. If the new state is the
   // same as the current state, we may be able to bail out entirely.
   const lastRenderedReducer = queue.lastRenderedReducer;
   if (lastRenderedReducer !== null) {
     let prevDispatcher;
     if (__DEV__) {
       prevDispatcher = ReactCurrentDispatcher.current;
       ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
     }
     try {
       const currentState: S = (queue.lastRenderedState: any);
       const eagerState = lastRenderedReducer(currentState, action);
       // Stash the eagerly computed state, and the reducer used to compute
       // it, on the update object. If the reducer hasn't changed by the
       // time we enter the render phase, then the eager state can be used
       // without calling the reducer again.
       update.eagerReducer = lastRenderedReducer;
       update.eagerState = eagerState;
       if (is(eagerState, currentState)) {
         // Fast path. We can bail out without scheduling React to re-render.
         // It's still possible that we'll need to rebase this update later,
         // if the component re-renders for a different reason and by that
         // time the reducer has changed.
         return;
       }
     } catch (error) {
       // Suppress the error. It will throw again in the render phase.
     } finally {
       if (__DEV__) {
         ReactCurrentDispatcher.current = prevDispatcher;
       }
     }
   }
 }

조건문이 등장합니다.
우선적으로 말하자면, 조건은 일단 fiberexpirationTimeNoWork이고, action(setState의 인자)가 현재 상태값과 같은 경우 함수를 실행중지(return)합니다.

  • fiber.expirationTime 프로퍼티에는 업데이트가 발생하여 Work가 스케줄링 될 경우 발생 시간을 할당합니다.

조건문의 조건에 대한 좀 더 자세한 설명

  • fiber.expirationTime === NoWork: 간단하게 말하면, 현재 컴포넌트의 업데이트로 인해 어떠한 Work도 스케줄러에 등록되지 않았는지(첫번째 업데이트인지))
  • is(eagerState, currentState): action의 결과값과 현재 상태 값이 같은지

먼저 fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork) 조건을 통과했다고 가정해봅시다.

그 이후에는 또 다른 조건문이 저희를 반깁니다.
다음에 실행될 코드를 보시죠.

if (lastRenderedReducer !== null) {
  let prevDispatcher;
  if (__DEV__) {
    prevDispatcher = ReactCurrentDispatcher.current;
    ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
  }
  try {
    const currentState: S = (queue.lastRenderedState: any);
    const eagerState = lastRenderedReducer(currentState, action);
    
    update.eagerReducer = lastRenderedReducer;
    update.eagerState = eagerState;
    if (is(eagerState, currentState)) {
     
      return;
    }
  } catch (error) {
    // Suppress the error. It will throw again in the render phase.
  } finally {
    if (__DEV__) {
      ReactCurrentDispatcher.current = prevDispatcher;
    }
  }
}

queue.lastRenderReducermountState 함수에서 할당을 이미 했으므로 null이 아닙니다. 때문에 lastRenderedReducer !== null 조건을 만족시킵니다.
조건을 통과했다고 가정하고, 다음에 실행될 코드를 보면 queue.lastRenderedState (마지막으로 렌더링된 state)를 currentState로, lastRenderReducercurrentStateaction(setState의 인자)을 인자로 넣어 호출해본 값을 eagerState로 할당합니다.

  • 이 작업 때문에, 순차적으로 업데이트되어야 하는 statesetState((prev) => {...}) 형태로 작성해야 했던 것입니다.

그리고 updateeagerReducer 프로퍼티에는 lastRenderedReducer를, eagerState 프로퍼티에는 eagerState를 넣습니다.

그 다음에 eagerStatecurrentState를 비교하여 같으면 함수를 종료합니다. 같지 않으면 다음 단계로 넘어가게 됩니다.

queue.lastRenderedReducer란?
mountState함수의 코드를 통해 basicStateReducer를 할당하는 것을 알 수 있고, basicStateReducer의 코드는 다음과 같습니다.

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

그리고 BasicStateAction 타입은 아래와 같습ㄴ디ㅏ.

type BasicStateAction<S> = (S => S) | S;

즉, BasicStateAction 타입은 같은 타입을 반환하는 함수 타입이거나 제네릭 타입을 의미합니다.
때문에 basicStateReducer 함수는 action이 함수인 경우흔 해당 함수를 실행해주고, 아닌 경우는 action을 그냥 반환하는 함수라고 보시면 됩니다.

4) 스케줄링

update를 적용하기 위해 Work를 scheduler에 예약하는 작업입니다.
아까 2가지 조건을 모두 만족하는 상황이 아니라면 스케줄링이 실행됩니다.

scheduleWork(fiber, expirationTime);

이 코드를 통해 scheduler에 fiberexpirationTime를 전달하여 schedule을 예약해줍니다.
이를 통해 fiberexpirationTime을 새기고 재조정을 진행할 Work 함수를 스케줄링하는 코드입니다.

Render phase에서 dispatchAction

render phase에서 setState과 호출되었을 때도 업데이트가 발생하지만 idle 상태와는 다른 방식으로 동작합니다.
이미 idle 상태 에서 WORK를 scheduler에 등록했으므로, 다시 WORK를 등록할 필요가 없고, 최적화할 필요도 없습니다.
render phase update가 더이상 발생하지 않을 때까지 컴포넌트를 재호출하고 action(setState 인자)를 소비하면 됩니다.

WORK에 등록할 필요가 없는 이유와 최적화할 필요가 없는 이유
render phase에서 dispatchAction이 호출된 경우에는 이미 idle 상태에서 WORK를 scheduler에 넘긴 상태이므로, 다시 등록할 필요가 없습니다.
또한, 최적화란 WORK를 Scheduler에 등록하는 것은 비용이 많이 드는 일이므로 등록할지 말지를 결정하는 것입니다. 때문에, 이미 WORK에 등록한 상태이므로 최적화도 필요없는 것입니다.

render Phase의 dispatchAction 코드는 아래와 같습니다.

// ...이하 생략

didScheduleRenderPhaseUpdate = true;
const update: Update<S, A> = {
  expirationTime: renderExpirationTime,
  action,
  eagerReducer: null,
  eagerState: null,
  next: null,
};
if (renderPhaseUpdates === null) {
  renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
  renderPhaseUpdates.set(queue, update);
} else {
  // Append the update to the end of the list.
  let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
  while (lastRenderPhaseUpdate.next !== null) {
    lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
  }
  lastRenderPhaseUpdate.next = update;
}
// ... 이하 생략(idle 시의 코드)

1) render phase에서 update의 저장

이미 업데이트가 진행 중인 상태에서 새로운 업데이트를 받아들여야 하므로 기존에 진행 중인 update를 저장해둬야 합니다.

didScheduleRenderPhaseUpdate = true;
const update: Update<S, A> = {
  expirationTime: renderExpirationTime,
  action,
  eagerReducer: null,
  eagerState: null,
  next: null,
};
if (renderPhaseUpdates === null) {
  renderPhaseUpdates = new Map();
}

didScheduleRenderPhaseUpdate 전역변수에 true를 할당합니다. 변수명 그대로 renderPhaseUpdate를 스케줄러에 등록했는지를 나타냅니다.
update 객체를 만들어 준 후에, renderPhaseUpdates 전역변수 값이 null인 경우, Map 인스턴스를 생성해줍니다. 변수명에서 유추할 수 있듯이 renderPhase에서 발생한 update를 임시로 저장해둔 저장소라고 볼 수 있습니다.

  • Map은 자바스크립트 객체 리터럴과 유사하지만 Key에 다양한 타입(객체까지도)을 허용합니다.
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);

if (firstRenderPhaseUpdate === undefined) {
  renderPhaseUpdates.set(queue, update);
} else {
  // Append the update to the end of the list.
  let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
  while (lastRenderPhaseUpdate.next !== null) {
    lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
  }
  lastRenderPhaseUpdate.next = update;
}

그런 다음, renderPhaseUpdates 객체의 get 메소드에 queue 객체를 넣어 queue를 키를 갖는 값이 있는 값을 꺼내옵니다. (없을 수도 있습니다)
없는 경우라면 render Phase일 때, 첫번째로 setState()가 호출된 경우겠죠?

만약 없다면, renderPhaseUpdates에 queue를 키로 update 객체를 value로 갖도록 저장을 합니다.

만약 있다면, renderPhaseUpdates.get(queue)를 통해 반환받는 리스트의 마지막에 update를 추가해줍니다.

다시 말하지만 queue 객체는 update들을 circular linked list로 갖는 객체입니다.

2) render phase에서 update의 소비

render phase에서 이제 update의 소비가 발생해야 합니다. 이 소비는 renderWithHooks에서 발생하게 됩니다.

다시 renderWithHooks를 보도록 하죠.

if (didScheduleRenderPhaseUpdate) {
  do {
    didScheduleRenderPhaseUpdate = false;
    // 무한 루프 방지와 업데이트 구현체에게 Render phase update를 알려주는 플래그
    numberOfReRenders += 1;

	//이하 훅 업데이트 구현체에서 Render phase update를 소비하는데 필요한 변수들을 설정
    nextCurrentHook = current !== null ? current.memoizedState : null;
    nextWorkInProgressHook = firstWorkInProgressHook;

    currentHook = null;
    workInProgressHook = null;
    componentUpdateQueue = null;

    if (__DEV__) {
      // Also validate hook order for cascading updates.
      hookTypesUpdateIndexDev = -1;
    }

    ReactCurrentDispatcher.current = __DEV__
      ? HooksDispatcherOnUpdateInDEV
    : HooksDispatcherOnUpdate;

    children = Component(props, refOrContext);
  } while (didScheduleRenderPhaseUpdate);

  renderPhaseUpdates = null;
  numberOfReRenders = 0;
}

조금 전에 봤듯이 renderPhase에서 setState가 다시 호출된다면 didScheduleRenderPhaseUpdate가 true가 되고, 위의 조건문이 실행됩니다.

조건문 내부를 해석해보도록 합시다.
먼저 조건문 및 반복문이 실행되지 않도록 didScheduleRenderPhaseUpdate를 false로 만듭니다. 그 다음, nubmerOfReRenders 변수를 1 증가시킵니다.

ReactCurrentDispatcher.current에는 HooksDispatcherOnUpdate(Update Hook 구현체)가 주입되는 것을 볼 수 있고, HooksDispatcherOnUpdate(훅 업데이트 구현체)에서 render phase update를 소비하는데 필요한 변수를 초기화/할당하는 것을 볼 수 있습니다.

반복문을 종료하게 되면, 그 이후에는 update를 초기화합니다.

3) update 소비 중에 다시 update 발생

children에 Component를 호출하여 반환받은 값을 재할당해준 것을 볼 수 있습니다.
만약 정상적으로 결과값을 받으면 반복문을 종료하게 되겠죠.

하지만 만약 아래의 예제의 경우, 컴포넌트를 return 하기 전에 setState()를 다시 호출할 가능성이 있습니다.

function MyCounter() {
  const [count, setCount] = useState(0)
  if (count === 1) setCount(2)
  return <button onClick={() => setCount(1)}></button>
}

그러면 setState가 다시 호출되므로, 반복문을 한 사이클 돌게 됩니다.

그렇다면 아래처럼 코드를 변경하게 어떻게 될까요??

function MyCounter() {
  const [count, setCount] = useState(0)
  if (count < 3) setCount(2)
  return <button onClick={() => setCount(1)}></button>
}

무한히 반복문을 돌게 되겠죠.

이러한 불상사를 막기 위해서 dispatchAction함수 상단에는 아래와 같은 코드가 있었습니다.

invariant(
  numberOfReRenders < RE_RENDER_LIMIT,
  'Too many re-renders. React limits the number of renders to prevent ' +
  'an infinite loop.',
);

RE_RENDER_LIMIT 값은 25이며, 25번까지는 컴포넌트가 재호출되면서 리렌더를 일으킵니다.

profile
풀스택으로 나아가기

0개의 댓글