React 내부 동작원리를 알아보자(3) - 우리의 useState를 찾아서

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

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

이 글 을 읽은 후에는 "useState가 코드로 정의된 곳은 어디일까?"에 대한 답을 내릴 수 있게 됩니다.

Hook은 어디서 오는 걸까?

react core

useState를 import 해오는 곳은 react-core 패키지임을 알 수 있습니다. 그렇다면 react-core패키지의 useState는 어디서 오는 걸까요??
16 버전의 react-core패키지의 코드를 살펴보면 ReactHooks 라는 파일에서 가져온 것을 볼 수 있습니다.

이를 계속 추적해보면 useState는 아래와 같이 선언되어있는 것을 확인해볼 수 있습니다.
직접 확인해보고 싶으신 분들을 위해 링크를 첨부해두었습니다.

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

resolveDispatcher 함수를 호출해서 가져온 dispatcher 인스턴스의 useState 메서드의 반환값을 리턴받는 것을 확인할 수 있습니다. (말로는 어렵지만 코드를 보면 이해가 쉬우실 겁니다)

결국 resolveDispatcher 함수를 다시 확인해봐야 겠네요. 이것도 링크 를 첨부해둘게요

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}

ReactCurrentDispatchercurrent 속성을 dispatcher 변수에 할당하는 것을 볼 수 있습니다.

그렇다면 ReactCurrentDispatcher를 또 찾아봐야겠네요. 링크

/**
 * Keeps track of the current dispatcher.
 */
const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

ReactCurrentDispatcher 안에는 상태와 관련된 코드는 없고, current 속성만 있는 객체라는 것을 확인하시게 되었습니다.

이를 통해 react-core 패키지 내부에는 Hook에 대한 코드가 구현되어 있지 않음을 확인할 수 있습니다.

react-core 패키지는 react element에 대한 정보만을 알고 있습니다.

react element는 아직 VDOM에 올라가기 전인 React.createElement()를 호출해서 얻는 컴포넌트에 대한 필수 정보(key, props, ref, type 등)들만 가지고 있는 상태입니다. 즉, react elementhook에 대한 정보가 없습니다.

react element가 VDOM으로 올라가기 위해서는 fiber로 확장해야 하는데, 이 때, hook에 대한 정보를 포함하게 됩니다.

그렇다면 react elementfiber로 확장을 누가 담당해줄까요? 바로 reconciler입니다.
즉, hookreconciler가 알고 있을 것으로 추측할 수 있게 되는 것이죠.

reconcilerreact-core로 어떻게 전달할까요??
react-core에서 RectCurrentDispatcher를 사용하는 코드를 찾아보게 되면 react/src/ReactSharedInternals.js라는 파일에서 사용하고 있음을 확인할 수 있습니다. 링크

// ReactSharedInternals.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';

...
...

const ReactSharedInternals = {
  ReactCurrentDispatcher,
  ReactCurrentOwner,
  // Used by renderers to avoid bundling object-assign twice in UMD bundles:
  assign,
};

코드를 보면 ReactSharedInternals 객체에 property를 통해 외부 모듈을 할당받습니다.

즉 여기까지 살펴보면 react-corehook을 사용하기 위해 외부(reconciler)로부터 hook에 대한 정보를 주입받는 형태로 구성되어 있고, 그 출입구의 역할을 하는 파일이 react/src/ReactSharedInternals.js라는 것을 추측할 수 있게 됩니다. 이러한 구조를 통해서 파일 간 의존성을 끊고, 서로 필요한 파일들을 쉽게 주고 받을 수 있게 되었습니다

한 발 더 나아가서 react는 전체에서 공유되는 패키지들을 shared라는 별도의 패키지로 관리하고 있습니다. 해당 패키지에도 ReactSharedInternals.js라는 파일을 가지고 있으며 링크를 첨부해두도록 하겠습니다.

shared

shared/ReactSharedInternals.js의 소스코드는 아래와 같습니다.

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import React from 'react';

const ReactSharedInternals =
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

// Prevent newer renderers from RTE when used with older react package versions.
// Current owner and dispatcher used to share the same ref,
// but PR #14548 split them out to better support the react-debug-tools package.
if (!ReactSharedInternals.hasOwnProperty('ReactCurrentDispatcher')) {
  ReactSharedInternals.ReactCurrentDispatcher = {
    current: null,
  };
}

export default ReactSharedInternals;

react-core 패키지에 있는 react/src/ReactSharedInternals.js 파일에 어떤 값을 전달해주기 위해서는 react-shared 패키지인 shared/ReactSharedInternals.js에 전달해줘야 하는 것이죠.

요약하자면 react-corereact/src/ReactSharedInternals.js는 Injection을 기다리는 dependency들의 대기소이자 저희는 ReactCurrentDispatcher를 주입 받기를 원하고 있습니다. shared 패키지가 이를 주입해줍니다.
shared 패키지의 RectSharedInternals.js라는 파일은 react-corereact/RectSharedInternals를 import해서 ReactCurrentDispatcher을 할당해줍니다.

useState의 출처를 한 줄로 정리하면
reconciler - shared/ReactSharedInternals - react/ReactSharedInternals - react/ReactCurrentDispatcher -react/ReactHooks - react - 프론트엔드 개발자 이렇게 오게 되는거죠.

reconcilerrenderWithHooks() 에서는 무슨 일이?

어떻게 useState를 export할까?

reconciler 패키지의 ReactFiberHooks.js라는 파일을 살펴보게 되면 아래와 같은 코드를 볼 수 있게 됩니다. 링크

reconciler 패키지의 많은 모듈은 자신의 컨텍스트를 현재 작업 중인 컴포넌트 전용을 사용합니다. (해당 모듈에서 선언되는 모든 전역 변수들(firstWorkInProgressHook, nextCurrentHook..)은 작업 중인 컴포넌트에만 국한되는 상태 값으로 관리합니다)
컴포넌트(Component)의 작업이 끝나면 모두 초기화시켜 다음 컴포넌트(아마 Component 재호출 시)에서 사용할 수 있도록 준비시킵니다.

import ReactSharedInternals from 'shared/ReactSharedInternals';

...
...

const {ReactCurrentDispatcher} = ReactSharedInternals;

...
...
export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
    ...
  if (__DEV__) {
    if (nextCurrentHook !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      // This dispatcher handles an edge case where a component is updating,
      // but no stateful hooks have been used.
      // We want to match the production code behavior (which will use HooksDispatcherOnMount),
      // but with the extra DEV validation to ensure hooks ordering hasn't changed.
      // This dispatcher does that.
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
      ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;
  }
  ...
  // 이하생략
}
...
...

여기서 마지막 부분을 보시면 ReactCurrentDispatchercurrent 속성에는 nextCurrentHook이 null인 경우 HooksDispatcherOnMount를, 아닌 경우는 HooksDispatcherOnUpdate를 할당하게 되는 것을 확인할 수 있습니다.

  • 이 코드를 보면 nextCurrentHook이 null이면 mount되는 중이고, null이 아니면 update되는 중임을 유추해볼 수 있습니다.

그러면 nextCurrentHook을 알아보기 전에 HooksDispatcherOnMountHooksDispatcherOnUpdate가 무엇인지 알아봅시다.

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
};

확인해보니 우리가 사용하고 있는 hooks를 속성으로 가진 객체임을 확인해볼 수 있습니다. react-core에서 사용하고 있는 useStatereconciler로부터 왔음을 코드로 확인하게 된 순간입니다.
더 자세히 말하면 renderWithHooks 함수로부터 react-coreuseState를 주입하게 되는 것이죠.

그렇다면 HooksDispatcherOnMountHooksDispatcherOnUpdate가 어떤 차이가 있는지를 확인해보기 위해서는 nextCurrentHook이 무엇인지를 알아봐야하고, 이를 위해서는 결국 코드가 실행되는 renderWithHooks 함수를 파악해봐야 합니다.

renderWithHooks 이해하기

renderWithHooks()는 함수명에서도 보이듯이 hooks와 함께 render하는 함수이며, hook을 주입하는 역할을 합니다.
이 함수는 Render Phase에서 실행됩니다.

렌더링: 컴포넌트 호출 후의 결과가 VDOM에 반영되는 과정

함수 로직을 살펴보기 위해 함수 전체를 가져오겠습니다.

export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
  nextCurrentHook = current !== null ? current.memoizedState : null;

  if (__DEV__) {
    hookTypesDev =
      current !== null
        ? ((current._debugHookTypes: any): Array<HookType>)
        : null;
    hookTypesUpdateIndexDev = -1;
  }

  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;

  // remainingExpirationTime = NoWork;
  // componentUpdateQueue = null;

  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;
  // sideEffectTag = 0;

  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because nextCurrentHook === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)

  // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so nextCurrentHook would be null during updates and mounts.
  if (__DEV__) {
    if (nextCurrentHook !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      // This dispatcher handles an edge case where a component is updating,
      // but no stateful hooks have been used.
      // We want to match the production code behavior (which will use HooksDispatcherOnMount),
      // but with the extra DEV validation to ensure hooks ordering hasn't changed.
      // This dispatcher does that.
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

  let children = Component(props, refOrContext);

  if (didScheduleRenderPhaseUpdate) {
    do {
      didScheduleRenderPhaseUpdate = false;
      numberOfReRenders += 1;

      // Start over from the beginning of the list
      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;
  }

  // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there's no re-entrancy.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const renderedWork: Fiber = (currentlyRenderingFiber: any);

  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  renderedWork.effectTag |= sideEffectTag;

  if (__DEV__) {
    renderedWork._debugHookTypes = hookTypesDev;
  }

  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null;
  nextCurrentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;
  nextWorkInProgressHook = null;

  if (__DEV__) {
    currentHookNameInDev = null;
    hookTypesDev = null;
    hookTypesUpdateIndexDev = -1;
  }

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;
  sideEffectTag = 0;

  // These were reset above
  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;

  invariant(
    !didRenderTooFewHooks,
    'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',
  );

  return children;
}

함수 전체를 보면 매우 길기 때문에 천천히 쪼개서 봅시다.
먼저 Component(fiber)와 hook을 연결하는 코드가 있습니다.

nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
nextCurrentHook = current !== null ? current.memoizedState : null;

currentlyRenderingFiber는 선언 키워드(const,let,var)가 없이 workInProgress라는 Fiber 타입의 값을 받아옵니다.
선언 키워드가 없으므로, 외부에 선언된 값임을 알 수 있습니다. (실제로 전역변수로 선언되어 있습니다)
즉, 현재 작업 중인 fiber를 전역으로 잡아둡니다.

workInProgress는 작업중인 파이버를 의미했습니다.
지난 포스트에서 설명했듯이 현재 렌더링 중인 Fiber를 workInProgress로 잡아두는 코드로 이해할 수 있습니다.

전역변수로 선언한 이유는 함수가 다 끝나고 다시 함수를 호출하는 경우가 있는데, 이전 함수에서 활용한 변수를 사용해야 하는 경우, 전역변수를 사용하면 됩니다.

nextCurrentHook를 보시면 current가 null이 아닌 경우, current.memoizedState를 할당하고 있는 것을 볼 수 있습니다.

그렇다면 current는 무엇일까요?? DOM에 이미 반영된 정보를 가지고 있는(mount된) fiber를 의미합니다. 즉, mount가 이미 끝난 경우를 의미하고 이는 업데이트 상태라고도 볼 수 있습니다.

current가 null이면 mount
current가 null이 아니면 update

또한, nextCurrentHookcurrent.memoizedState를 할당하는 로직을 통해 fibermemoizedState 내부에는 hook이 들어있다는 것을 추측해볼 수 있습니다.

컴포넌트가 mount될 때, hook은 마운트용 구현체(HooksDispatcherOnMount)를 사용하고, 그 이후부터 unmount가 되기 직전까지 업데이트용 구현체(HooksDispatcherOnUpdate)를 사용합니다.

ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

위에서 본 로직을 다시 보면 nextCurrentHook에 따라 ReactCurrentDispatcher.current에 다른 HooksDispatcher를 할당하는 것을 볼 수 있습니다.


그 다음 코드를 볼까요?

 let children = Component(props, refOrContext);

Component 함수는 fibertype 프로퍼티에서 꺼내왔으며 함수형 컴포넌트의 경우 개발자가 작성한 컴포넌트가 type이 됩니다.
이 부분을 통해서 renderWithHooks함수는 컴포넌트를 호출하는 역할도 가지고 있음을 알게 되었습니다.
렌더링의 과정은 컴포넌트를 호출 후의 결과를 VDOM을 반영하는 것이라고 보면 되는데 컴포넌트 호출이 renderWithHooks 내부에서 일어나고 있는 것입니다.


그 다음 코드를 또 보죠.

 if (didScheduleRenderPhaseUpdate) {
   do {
     didScheduleRenderPhaseUpdate = false;
     numberOfReRenders += 1;

     // Start over from the beginning of the list
     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;
 }

didScheduleRenderPhaseUpdate라는 플래그를 통해 분기적으로 실행되는 코드입니다.
render phase의 업데이트를 스케줄러에게 전달했는지에 대한 플래그입니다. 업데이트란 setState 등을 통해 상태들이 변화시킬 때, 변화된 상태를 업데이트 시키는 일련의 과정을 update라고 알아두면 됩니다. (실제로는 JS 객체라고 합니다.)

mount 상황에서는 아직은 업데이트를 전달하지는 않습니다. 일단 이 과정에서는 mount된 부분만 우선적으로 생각할 것이고, 때문에 이 코드는 실행되지 않는다고 생각하고 넘어갑시다.


그리고 나서의 코드를 봅시다.

// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;

ReactCurrentDispatcher.current에 새로운 값을 할당하기 때문에 "이전에 삼항 연산자를 통해서 할당한 코드가 덮어씌워지는 것이 아닌가?" 라는 합리적인 판단을 할 수 있습니다. 하지만 중간에, Component 함수를 호출하게 되면 많은 일들이 일어납니다. 이 때, mount, update 등의 작업이 다 끝나고 난 뒤에 ContextOnlyDispatcher를 할당하는 코드가 수행됩니다. 이것을 할당하는 이유는 <u>작업을 다 수행하고 나서 hook을 호출하면 안되는 상황에서 hook을 호출했을 때, 에러를 던지기 위한 코드입니다.

그렇다면 ContextOnlyDispatcher가 무엇일까?

export const ContextOnlyDispatcher: Dispatcher = {
  readContext,
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  useDebugValue: throwInvalidHookError,
};

위 코드를 보면 알 수 있듯이, 우리가 알고있는 Hook을 프로퍼티로 가지고 있으며, HookError를 throw 해주는 객체로 보면 됩니다.


다음 코드를 봅시다.

 const renderedWork: Fiber = (currentlyRenderingFiber: any);

  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  renderedWork.effectTag |= sideEffectTag;

currentlyRenderingFiberworkingProgress를 전역으로 저장해둔 변수입니다. 이 currentRenderingFiberrenderedWork라는 변수에 할당을 해주고 있습니다.

이후, renderedWorkmemoizedStatefirstWorkInProgressHook을 할당해주고 있으며, 정확한 역할은 아직 모르지만 hook에 관련된 무언가를 할당해주고 있음을 알 수 있습니다.

즉, fibermemoizedState에는 hook이 담기는 것이죠. (hookcomponent를 매핑시켜주는 역할을 합니다)

그렇다면 firstWorkInProgressHook이 무엇일까요?
아래의 mountWorkInProgressHook 함수와 updateWorkInProgressHook 함수를 보면 workInProgressHook에는 hook이 담긴 것을 볼 수 있고, 결국 workInProgressHook = hook인 셈인 것이죠.

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null, // 컴포넌트에 적용된 마지막 상태 값
    baseState: null, 
    queue: null, // 훅이 호출될 때마다 update를 연결리스트로 queue에 할당
    baseUpdate: null, 
    next: null, // 다음 훅을 가리키는 포인터
  };

  if (workInProgressHook === null) {
    // 맨 처음 실행되는 훅인 경우 링크드 리스트의 head로 
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 두 번째 이후부터는 링크드리스트에 추가
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
  • workInProgressHook는 현재 처리되고 있는 훅을 나타내면서 리스트의 tail 포인터입니다.
  • firstWorkInProgresHook은 훅 링스트 리스트의 head로 컴포넌트 실행이 끝났을 때, fiber에 저장되어 컴포넌트와 훅 리스트를 연결해 줍니다.

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.
  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
    nextCurrentHook = currentHook !== null ? currentHook.next : null;
  } else {
    // Clone from the current hook.
    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      queue: currentHook.queue,
      baseUpdate: currentHook.baseUpdate,

      next: null,
    };

    if (workInProgressHook === null) {
      // This is the first hook in the list.
      workInProgressHook = firstWorkInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
    nextCurrentHook = currentHook.next;
  }
  return workInProgressHook;
}

잠깐 딴길로 새서 그러면 mountWorkInProgressHook 함수는 어디서 호출될까요??

바로 mountState 함수 내부입니다.

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
= (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

mount할 때, useState를 호출한다는 것은 mountState를 호출하는 것과 같은 의미이고 이것은 이미 HooksDispatcherOnMount에서 보았습니다.

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
};

이렇게 살펴봄으로써 reconciler의 renderWithHooksfiberhooks 정보를 연결해주는 것을 확인할 수 있었습니다.
renderedWork.memoizedState = firstWorkInProgressHook 코드를 통해서 눈으로 볼 수 있게 된 것이죠.


그 다음을 봅시다

// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
      currentHook !== null && currentHook.next !== null;

renderExpirationTime = NoWork;
currentlyRenderingFiber = null;

currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null

remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;

// These were reset above
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;

invariant(
  !didRenderTooFewHooks,
  'Rendered fewer hooks than expected. This may be caused by an accidental ' +
  'early return statement.',
);

return children;

다른 곳에서 쓸 수 있는 전역변수를 null로 초기화시켜주는 것을 볼 수 있습니다.
즉, 작업(hook 주입, 렌더링 등)이 끝나면 null로 초기화해서 다음 컴포넌트가 이를 활용할 수 있도록 준비하는 로직입니다

마지막으로 Component() 함수를 통해 얻은 children을 반환하는 것으로 함수는 종료됩니다.

이 과정을 통해 fiberhook을 어떻게 연결하는지를 살펴볼 수 있었고, 상황에 맞게 ReactCurrentDispatcher.currenthook 정보를 주입하고 있음을 확인할 수 있었습니다.

  • fiberhook을 어떻게 연결하는지 (이를 통해 리액트 내부적으로 fiberhook을 연결해줌)
renderedWork.memoizedState = firstWorkInProgressHook;
  • 상황에 맞게 ReactCurrentDispatcher.currenthook 정보를 주입 (이를 통해 프론트엔드 개발자가 외부에서 사용할 수 있게 됨)
ReactCurrentDispatcher.current =
  nextCurrentHook === null 
    ? HooksDispatcherOnMount
	: HooksDispatcherOnUpdate;
profile
풀스택으로 나아가기

0개의 댓글