[React 까보기] 3. Hook 구현체 찾아가기

1️⃣ useState가 코드로 정의된 곳은 어디일까? Reconciler 까지 찾아가기!

  1. react 코어 → ReactHooks.js 모듈 → ReactCurrentDispatcher 모듈 → current:null 인 객체가 끝?
    (리액트 코어 패키지는 react 요소에 대한 내용만 포함할 뿐 훅을 구현하고있지 않다.)
  2. Render Phase에서 VDOM으로 리액트 요소가 올라갈때 hook에 대한 정보를 포함하게 된다. (리액트 요소가 Fiber로 확장되기 전에는 필수적인 정보만 포함한다)
  3. VDOM에 재조정 하기 위한 과정중 하나인 Fiber로 확장하는 작업은 Reconciler가 Render Phase에서 수행하게된다.
    따라서 Reconciler가 hook에 대한 정보를 알 수 있을것이라 추측하고, Reconciler가 React 코어 패키지로 어떻게 전달하는지 관심을 가질 수 있다.
  4. 따라서 ReactCurrentDispatcher 를 검색했을때 ReactSharedInternals.js 파일을 찾을 수 있는데, 이는 파일 이름에서 추측할 수 있듯 외부에서 주입받을 수 있는 환경을 제공한다.
    • 이때 리액트에서는 shared 라는 패키지를 두어 react 코어 패키지와 외부 패키지의 의존성을 끊음과 동시에 정보를 전달할 수 있게한다 (출입구 역할)
    • ReactCurrentDispatcher는 ReactSharedInternals 객체 내부에 포함되어 있다
  5. 최종적으로 Reconciler에서 shared의 ReactSharedInternals를 이용해 hooks 의 정보를 React 코어 패키지로 전달한다.
    → React 코어(react) 패키지는 위 4번의 ReactSharedInternals를 통해 ReactCurrentDispatcher에 접근한다.

2️⃣ 알겠고, ReactCurrentDispatcher는 뭐가 할당되는 건데? (renderWithHooks)

ReactCurrentDispatcher.current 에 할당되는 것을 찾기 위해선 Reconciler 패키지의 renderWithHooks() 의 구현을 보면된다.

  1. currentlyRenderingFiber = workInProgress;
    • 리액트 내부에서 훅을 호출할 때 어떤 컴포넌트를 렌더링중인지 전역적으로 알려주는 역할
    • 왜 필요할까?
      →훅(useState, useEffect, useReducer 등)은 반드시 함수형 컴포넌트 내에서만 호출되어야 하며, 호출 순서도 보장되어야 한다. 따라서 이를 보장하기 위해 리액트는 훅이 호출될 때마다 그 훅이 어느 컴포넌트에서 호출되었는지 알아야 한다.
  1. nextCurrentHook = current !== null ? current.memoizedState : null;
    • current 란?
      이미 commit 되어 DOM에 반영된 Fiber 이다. (이전 포스팅 [React 까보기] 2. VDOM 과 React lifecycle 참고)
      업데이트 중일 경우 WorkInProgress.alternate가 이 current이다.
      즉, current !== null이면 update, null이면 mount 이다.
      current === null → DOM에 반영된 정보가 없다 → 마운트를 해야한다
      current !== null → 이미 DOM에 반영된 정보가 있다 → 마운트가 끝났다(업데이트 상태)
    • nextCurrentHook 이라는 변수명에서 미루어보아 current.memoizedState 에는 훅이 들어있을것이다! 라고 추측하고 넘어가기 (7번 과정에서 알 수 있음)
  1. ReactCurrentDispatcher.current = nextCurrentHook === null ? mount(HooksDispatcherOnMount) : update(HooksDispatcherOnUpdate);
// HooksDispatcherOnMount, HooksDispatcherOnUpdate 는 
// 아래와 같이 같은 이름의 서로 다른 훅 구현을 포함하고있는 객체이다.

const HooksDispatcherOnMount:Dispatcher = {
    readContext,

    useCallback: mountCallback,
    useEffect: mountEffect,
    useState: mountState,
    // ... other hooks
}

const HooksDispatcherOnUpdate:Dispatcher = {
    readContext,

    useCallback: updateCallback,
    useEffect: updateEffect,
    useState: updateState,
    // ... other hooks
}
  1. let children = Component(props, refOrContext);
    • 렌더링의 과정(컴포넌트 호출 → 결과를 얻고 → VDOM에 반영) 중 컴포넌트의 호출을 이부분에서 담당한다
  1. didScheduleRenderPhaseUpdate 플래그에 따른 if문 로직 처리
    • 상태를 업데이트 했을때 변하는 일련의 과정을 Update라고 일단은 알아두자 (리액트 Scheduler에서 자세히 알아볼 예정)
    • 현재는 mount 기준으로 실행 흐름을 보고있기때문에 일단은 건너뛰기
  1. ReactCurrentDispatcher.current = ContextOnlyDispatcher;
    • 위의 3번에서 할당했는데 한번 더 할당한다???
    • 컴포넌트의 호출(마운트 또는 업데이트, 일련의 과정) 후에 할당 구문이 실행된다.
    • 이때 훅을 호출하면 안되는 상황에서 훅을 덮어써 에러를 발생시키기 위해 ContextOnlyDispatcher를 할당하는 것!
// ContextOnlyDispatcher 내부에는 각 훅에 대해서 throwInvalidHookError 가 들어가있음

const ContextOnlyDispatcher:Dispatcher = {
    readContext,

    useCallback: throwInvalidHookError,
    useEffect: throwInvalidHookError,
    useState: throwInvalidHookError,
    // ... other hooks
}
  1. const renderedWork: Fiber = (currentlyRenderingFiber: any); renderWork.memoizedState = firstWorkInProgressHook;
    • 이를 통해 fiber의 memoizedState에는 hook이 담긴다는 것을 도출 할 수 있음!
    • firstWorkInProgressHook를 더 자세히 알아보면, hook 이라는 객체를 할당하고 있음
    • firstWorkInProgressHook에 hook 객체를 할당하는 함수(mountWorkInProgress)는 언제 호출되느냐? → mountState(useState) 에서 호출됨!
    • 따라서 이 과정은 Fiber에 훅 정보를 연결해주는 로직이라고 볼 수 있다
      ( = Reconciler 의 renderWithHooks 는 Fiber에 훅 정보를 연결해줌을 알 수 있다)
  2. currentHook = null; nextCurrentHook = null; firstWorkInProgressHook = null; workInProgressHook = null; nextWorkInProgressHook = null;
    • 이번 과정을 통해서 "전역변수"를 전부 초기화를 한다! → 이것이 의미하는 것은 파일안에서만 유효한 변수가 됐음
    • 컴포넌트를 작업할때 사용하고 다음 컴포넌트가 작업될때는 전역변수들을 초기화 했기때문에 이전 변수들의 값을 사용하지 않음
  3. return children;
    • 4번 과정에서 담아두었던 컴포넌트를 리턴하고 함수가 끝나게 됨

결국 현재 상태가 MountPhase 인지 UpdatePhase 인지에 따라 훅 정보를 넣어준다.
해당 훅 정보를 넣어주는 작업이 바깥에서 훅을 쓸 수 있게 하는 것이다.

 

📚 레퍼런스