우리가 쓰는 리액트 훅들은 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 댓글이 재밌다 ㅎㅎ.
로그인 중...