이 시리즈는 "가장 쉬운 웹개발 with Boaz" 님의 [React 까보기 시리즈] 를 기반으로 만들어졌습니다.
이 글 을 읽은 후에는 "useState
가 코드로 정의된 곳은 어디일까?"에 대한 답을 내릴 수 있게 됩니다.
Hook
은 어디서 오는 걸까?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;
}
ReactCurrentDispatcher
의 current
속성을 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 element
는hook
에 대한 정보가 없습니다.
react element
가 VDOM으로 올라가기 위해서는 fiber
로 확장해야 하는데, 이 때, hook
에 대한 정보를 포함하게 됩니다.
그렇다면 react element
를 fiber
로 확장을 누가 담당해줄까요? 바로 reconciler
입니다.
즉, hook
도 reconciler
가 알고 있을 것으로 추측할 수 있게 되는 것이죠.
reconciler
를 react-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-core
는 hook
을 사용하기 위해 외부(reconciler
)로부터 hook
에 대한 정보를 주입받는 형태로 구성되어 있고, 그 출입구의 역할을 하는 파일이 react/src/ReactSharedInternals.js
라는 것을 추측할 수 있게 됩니다. 이러한 구조를 통해서 파일 간 의존성을 끊고, 서로 필요한 파일들을 쉽게 주고 받을 수 있게 되었습니다
한 발 더 나아가서 react는 전체에서 공유되는 패키지들을 shared
라는 별도의 패키지로 관리하고 있습니다. 해당 패키지에도 ReactSharedInternals.js
라는 파일을 가지고 있으며 링크를 첨부해두도록 하겠습니다.
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-core
의 react/src/ReactSharedInternals.js
는 Injection을 기다리는 dependency들의 대기소이자 저희는 ReactCurrentDispatcher
를 주입 받기를 원하고 있습니다. shared
패키지가 이를 주입해줍니다.
shared
패키지의 RectSharedInternals.js
라는 파일은 react-core
의 react/RectSharedInternals
를 import해서 ReactCurrentDispatcher
을 할당해줍니다.
즉 useState
의 출처를 한 줄로 정리하면
reconciler
- shared/ReactSharedInternals
- react/ReactSharedInternals
- react/ReactCurrentDispatcher
-react/ReactHooks
- react
- 프론트엔드 개발자 이렇게 오게 되는거죠.
reconciler
의 renderWithHooks()
에서는 무슨 일이?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;
}
...
// 이하생략
}
...
...
여기서 마지막 부분을 보시면 ReactCurrentDispatcher
의 current
속성에는 nextCurrentHook
이 null인 경우 HooksDispatcherOnMount
를, 아닌 경우는 HooksDispatcherOnUpdate
를 할당하게 되는 것을 확인할 수 있습니다.
nextCurrentHook
이 null이면 mount되는 중이고, null이 아니면 update되는 중임을 유추해볼 수 있습니다.그러면 nextCurrentHook
을 알아보기 전에 HooksDispatcherOnMount
와 HooksDispatcherOnUpdate
가 무엇인지 알아봅시다.
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
에서 사용하고 있는 useState
가 reconciler
로부터 왔음을 코드로 확인하게 된 순간입니다.
더 자세히 말하면 renderWithHooks
함수로부터 react-core
의 useState
를 주입하게 되는 것이죠.
그렇다면 HooksDispatcherOnMount
와 HooksDispatcherOnUpdate
가 어떤 차이가 있는지를 확인해보기 위해서는 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
또한, nextCurrentHook
에 current.memoizedState
를 할당하는 로직을 통해 fiber
의 memoizedState
내부에는 hook
이 들어있다는 것을 추측해볼 수 있습니다.
컴포넌트가 mount될 때, hook은 마운트용 구현체(
HooksDispatcherOnMount
)를 사용하고, 그 이후부터 unmount가 되기 직전까지 업데이트용 구현체(HooksDispatcherOnUpdate
)를 사용합니다.
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
위에서 본 로직을 다시 보면 nextCurrentHook
에 따라 ReactCurrentDispatcher.current
에 다른 HooksDispatcher를 할당하는 것을 볼 수 있습니다.
그 다음 코드를 볼까요?
let children = Component(props, refOrContext);
Component
함수는 fiber
의 type
프로퍼티에서 꺼내왔으며 함수형 컴포넌트의 경우 개발자가 작성한 컴포넌트가 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;
currentlyRenderingFiber
는 workingProgress
를 전역으로 저장해둔 변수입니다. 이 currentRenderingFiber
를 renderedWork
라는 변수에 할당을 해주고 있습니다.
이후, renderedWork
의 memoizedState
에 firstWorkInProgressHook
을 할당해주고 있으며, 정확한 역할은 아직 모르지만 hook에 관련된 무언가를 할당해주고 있음을 알 수 있습니다.
즉, fiber
의 memoizedState
에는 hook
이 담기는 것이죠. (hook
과 component
를 매핑시켜주는 역할을 합니다)
그렇다면
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의 renderWithHooks
가 fiber
에 hooks
정보를 연결해주는 것을 확인할 수 있었습니다.
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
을 반환하는 것으로 함수는 종료됩니다.
이 과정을 통해
fiber
와hook
을 어떻게 연결하는지를 살펴볼 수 있었고, 상황에 맞게ReactCurrentDispatcher.current
에hook
정보를 주입하고 있음을 확인할 수 있었습니다.
fiber
와hook
을 어떻게 연결하는지 (이를 통해 리액트 내부적으로fiber
와hook
을 연결해줌)renderedWork.memoizedState = firstWorkInProgressHook;
- 상황에 맞게
ReactCurrentDispatcher.current
에hook
정보를 주입 (이를 통해 프론트엔드 개발자가 외부에서 사용할 수 있게 됨)ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;