이성열
A

CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE

우리가 쓰는 리액트 훅들은 react 패키지에서 export된다.

// react/packages/react/index.js
export {
  __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
  ...
  useEffect,
  ...
  useState,
  ...
} from './src/ReactClient';

useState를 따라가보자.

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

dispatcher는 무엇일까? resolveDispatcher를 따라가보자.

function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        '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://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return ((dispatcher: any): Dispatcher);
}

우선 데브 모드에서 dispatcher가 null인 경우 에러를 출력하는게 인상깊다. 컴포넌트 바깥에서 훅을 호출하면 dispatcher가 null인걸까?

dispatcher는 결국 ReactSharedInternals.H에 저장되는 무언가이다. H는 hook이라는 뜻 아닐까? 이어서 ReactSharedInternals를 살펴보자.

const ReactSharedInternals: SharedStateClient = ({
  H: null,
  A: null,
  T: null,
  S: null,
  V: null,
}: any);

단순한 객체이다. 초기값은 null이므로 어딘가 값을 설정하는 코드가 있을 것이다. 찾아보자.

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;
...
}

소스코드를 보면 곳곳에 위처럼 ReacSharedInternals.H에 값을 설정하는 코드가 있다. 자세히 살펴보지는 않겠지만 상황(mount, update, rerender)에 따라 다른 객체가 사용됨을 알아두자.

const HooksDispatcherOnMount: Dispatcher = {
  useContext: readContext,
  useEffect: mountEffect,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
  useEffect: updateEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  ...
};

const HooksDispatcherOnRerender: Dispatcher = {
  useEffect: updateEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: rerenderReducer,
  useRef: updateRef,
  useState: rerenderState,
  ...
};

이를 통해 우리는 useState라는 같은 이름으로 훅을 호출하지만 리액트 내부적으로는 ReacSharedInternals.H을 거치기 때문에 상황마다 다른 함수가 됨을 알 수 있다.

Level of indirection를 활용한 케이스 아닐까?

여담으로 ReactClient 패키지에서는 ReactSharedInternals__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE 라는 이름으로 내보내고 있다.

export {
  ReactSharedInternals as __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
  ...
};

왜 내부 구현 객체를 굳이 export하나했는데… gpt가 대답한 아래 내용이 일리가 있는 것 같다. 따로 검증은 안했다.

이는 흔히 라이브러리에서 트리쉐이킹, DevTools, 프레임워크 통합 등 특별한 용도를 위해 내부 객체를 export해야 할 때 쓰이는 패턴입니다.

여담으로 원래 이름은 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED라는 더 유쾌한 이름이었는데 2024년에 바꼈다. PR 댓글이 재밌다 ㅎㅎ.

댓글을 남기려면 로그인이 필요합니다.