본문 바로가기
프론트엔드

[React]React 와 Hooks.

by ISA(류) 2022. 3. 30.

동기

바닐라 js로 전역 store를 만들어보면서 리액트 인터페이스를 참고하면서 상태변화 구독을 자동화할 방법을 찾기 위해서 리액트 소스 코드를 분석을 진행하다가. 방대한 소스코드 분석을 차근차근 진행하면서 그 내용을 포스팅해보는 게 어떨까?라는 생각이 들어서 진행하게 되었다.(개인 학습과 생태계 기여)

개요

이번 파트는 React 패키지를 분석하면서 React Hooks 대한 그러니까 react-dom과 react 중에서 react의 컴포넌트 함수 Hooks에 대한 간략한 분석이다.

저장소 구성

리액트의 경우 자바스크립트 오픈소스 UI 라이브러리로 보통 기본 구성으로 react와 react-dom이라는 패키지를 설치하여서 사용한다. 오픈소스 라이브러리다보니 깃허브에 그 코드 전체가 repo로 공유되어있다.

https://github.com/facebook/react

 

GitHub - facebook/react: A declarative, efficient, and flexible JavaScript library for building user interfaces.

A declarative, efficient, and flexible JavaScript library for building user interfaces. - GitHub - facebook/react: A declarative, efficient, and flexible JavaScript library for building user interf...

github.com

리액트 Repo자체는 모노 레포로 구성되어 있으며 packages 내부에서 분류에 따라서 소스코드가 패키지화되어 있다.

어디선가 본거 같은 이름도 존재하고 난생처음 들어보는 패키지가 많이 있다. 오늘은 React hooks에 대해서 알아볼 예정이므로 이 중에서 react라는 이름을 가진 패키지 (스크린샷에는 안 나와있다.) 우리가 npm 또는 yarn으로 react로 설치해서 사용하는 패키지를 살펴보겠다.

패키지 root의 경우 위의 스크린샷과 같은 구성으로 되어있다. README의 설명중 아래 부분을 자세히 살펴보자.

The react
package contains only the functionality necessary to define React components. It is typically used together with a React renderer like react-dom for the web, or react-native for the native environments.

핵심 내용만 번역하면 해당 패키지에는 구성요소를 정의하는데 필요한 기능만 포함되어 있습니다.

react-dom(web)이나 react-native(moblie native) 같은 리액트 렌더러와 함께 사용됩니다.

라는 내용을 고지하고 있다.

이 문구를 통해서 우리는 React Hooks를 포함하고 있는 React package의 경우 리액트 Hooks에 대한 로직을 포함하고 있지 않고 정의에 관한 소스코드만을 가지고 있다는(일종의 인터페이스 type) 사실을 알 수 있다. 실제로 이와 같이 실제 실행되는 로직들은 리액트 렌더러인 react-dom이나 react-native가 처리한다.

그렇다면 해당 패키지는 어떻게 리액트 함수 Hook이나 클래스 컴포넌트의 추상 클래스(맞나?)를 정의 내리고 있을까? 이제 그것을 순서대로 따라가 보겠다.

먼저 index.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.
 *
 * @flow
 */

// Keep in sync with https://github.com/facebook/flow/blob/main/lib/react.js
export type StatelessFunctionalComponent<
  P,
> = React$StatelessFunctionalComponent<P>;
export type ComponentType<-P> = React$ComponentType<P>;
export type AbstractComponent<
  -Config,
  +Instance = mixed,
> = React$AbstractComponent<Config, Instance>;
export type ElementType = React$ElementType;
export type Element<+C> = React$Element<C>;
export type Key = React$Key;
export type Ref<C> = React$Ref<C>;
export type Node = React$Node;
export type Context<T> = React$Context<T>;
export type Portal = React$Portal;
export type ElementProps<C> = React$ElementProps<C>;
export type ElementConfig<C> = React$ElementConfig<C>;
export type ElementRef<C> = React$ElementRef<C>;
export type Config<Props, DefaultProps> = React$Config<Props, DefaultProps>;
export type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | T;

// Export all exports so that they're available in tests.
// We can't use export * from in Flow for some reason.
export {
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
  act as unstable_act,
  Children,
  Component,
  Fragment,
  Profiler,
  PureComponent,
  StrictMode,
  Suspense,
  SuspenseList,
  cloneElement,
  createContext,
  createElement,
  createFactory,
  createMutableSource,
  createRef,
  createServerContext,
  forwardRef,
  isValidElement,
  lazy,
  memo,
  startTransition,
  unstable_Cache,
  unstable_DebugTracingMode,
  unstable_LegacyHidden,
  unstable_Offscreen,
  unstable_Scope,
  unstable_TracingMarker,
  unstable_getCacheSignal,
  unstable_getCacheForType,
  unstable_useCacheRefresh,
  useId,
  useCallback,
  useContext,
  useDebugValue,
  useDeferredValue,
  useEffect,
  useImperativeHandle,
  useInsertionEffect,
  useLayoutEffect,
  useMemo,
  useMutableSource,
  useSyncExternalStore,
  useReducer,
  useRef,
  useState,
  useTransition,
  version,
} from './src/React';

아마도 처음 코드를 보게 된다면 나처럼 당황하게 될 것이다. js 파일에서 type? 이건 타입 스크립트에서 사용되는 타입 선언이 아닌가? 그러나 props-types, 타입 스크립트(js의 슈퍼셋), jsx 같은 것들을 떠올린다면 해당 소스코드 역시 그와 별반 다를 바 없는 것이라는 것을 알게 된다.

그리고 내용물을 보자 react를 한 번이라도 사용했다면 익숙한 이름들이 보이는 것을 알 수 있다. use로 시작되는 hooks나 components나 children 같은 변수명들 (사실 처음 보는 것들도 많이 보이지만)

해당 부분을 확인하니 패키지 내의 src폴더의 React라는 모듈에서 내보내진 요소들이라는 것을 확인할수 있다. 먼저 src 폴더를 들어가 보자. 그럼 아래와 비슷한 폴더 구조를 확인 할 수 있다.

그리고 그중 index.js파일에 구성요소를 제공하는 React.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.
 *
 * @flow
 */

import ReactVersion from 'shared/ReactVersion';
import {
  REACT_FRAGMENT_TYPE,
  REACT_DEBUG_TRACING_MODE_TYPE,
  REACT_PROFILER_TYPE,
  REACT_STRICT_MODE_TYPE,
  REACT_SUSPENSE_TYPE,
  REACT_SUSPENSE_LIST_TYPE,
  REACT_LEGACY_HIDDEN_TYPE,
  REACT_OFFSCREEN_TYPE,
  REACT_SCOPE_TYPE,
  REACT_CACHE_TYPE,
  REACT_TRACING_MARKER_TYPE,
} from 'shared/ReactSymbols';

import {Component, PureComponent} from './ReactBaseClasses';
import {createRef} from './ReactCreateRef';
import {forEach, map, count, toArray, only} from './ReactChildren';
import {
  createElement as createElementProd,
  createFactory as createFactoryProd,
  cloneElement as cloneElementProd,
  isValidElement,
} from './ReactElement';
import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import {forwardRef} from './ReactForwardRef';
import {memo} from './ReactMemo';
import {
  getCacheSignal,
  getCacheForType,
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useInsertionEffect,
  useLayoutEffect,
  useMemo,
  useMutableSource,
  useSyncExternalStore,
  useReducer,
  useRef,
  useState,
  useTransition,
  useDeferredValue,
  useId,
  useCacheRefresh,
} from './ReactHooks';
import {
  createElementWithValidation,
  createFactoryWithValidation,
  cloneElementWithValidation,
} from './ReactElementValidator';
import {createServerContext} from './ReactServerContext';
import {createMutableSource} from './ReactMutableSource';
import ReactSharedInternals from './ReactSharedInternals';
import {startTransition} from './ReactStartTransition';
import {act} from './ReactAct';

// TODO: Move this branching into the other module instead and just re-export.
const createElement = __DEV__ ? createElementWithValidation : createElementProd;
const cloneElement = __DEV__ ? cloneElementWithValidation : cloneElementProd;
const createFactory = __DEV__ ? createFactoryWithValidation : createFactoryProd;

const Children = {
  map,
  forEach,
  count,
  toArray,
  only,
};

export {
  Children,
  createMutableSource,
  createRef,
  Component,
  PureComponent,
  createContext,
  createServerContext,
  forwardRef,
  lazy,
  memo,
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useInsertionEffect,
  useLayoutEffect,
  useMemo,
  useMutableSource,
  useSyncExternalStore,
  useReducer,
  useRef,
  useState,
  REACT_FRAGMENT_TYPE as Fragment,
  REACT_PROFILER_TYPE as Profiler,
  REACT_STRICT_MODE_TYPE as StrictMode,
  REACT_DEBUG_TRACING_MODE_TYPE as unstable_DebugTracingMode,
  REACT_SUSPENSE_TYPE as Suspense,
  createElement,
  cloneElement,
  isValidElement,
  ReactVersion as version,
  ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
  // Deprecated behind disableCreateFactory
  createFactory,
  // Concurrent Mode
  useTransition,
  startTransition,
  useDeferredValue,
  REACT_SUSPENSE_LIST_TYPE as SuspenseList,
  REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
  REACT_OFFSCREEN_TYPE as unstable_Offscreen,
  getCacheSignal as unstable_getCacheSignal,
  getCacheForType as unstable_getCacheForType,
  useCacheRefresh as unstable_useCacheRefresh,
  REACT_CACHE_TYPE as unstable_Cache,
  // enableScopeAPI
  REACT_SCOPE_TYPE as unstable_Scope,
  // enableTransitionTracing
  REACT_TRACING_MARKER_TYPE as unstable_TracingMarker,
  useId,
  act,
};

index.js와 별다른 차이는 보이지 않는다. 몇 가지 정의 내리는 점이나 다른 소스코드들을 다시 랩핑 해서 export 해주는 정도 역할을 담당하고 있다. 이중 Hooks를 담당하는 ReactHooks라는 파일을 다시 찾아보자.

현시점에서는 206 라인 정도 길이를 가진 소스코드다. 해당 부분을 이해하는데 전체 소스코드는 필요 없으니 필요한 순서대로 짜깁기 해보겠다.

// 우리들이 자주쓰는 React Hooks를 볼 수 있다. 함수로 감싸져서 내보내기 되어져 있다.
export function useState<S>(
  initialState: (() => S) | S, // 초기 상태값 타입 콜백 함수 방식과 일반 방식 두가지.
): [S, Dispatch<BasicStateAction<S>>] { 
// 반환하는 타입 S는 상태 Dispatch<action<상태>>는 setState
  const dispatcher = resolveDispatcher(); // dispatcher라는 resolveDispatcher 함수 반환값을 받고
  return dispatcher.useState(initialState); // 거기서 useState라는 메서드(함수를 실행해서 상태를 넣어준다.)
}
export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

export function useRef<T>(initialValue: T): {|current: T|} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

export function useInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useInsertionEffect(create, deps);
}

export function useLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useLayoutEffect(create, deps);
}
...Hooks 생략...

// resolveDispatcher와 Hooks의 상태에 대한 코드 역시 같은 파일내에 존재한다.
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
// 액션 함수 type과 Dispatch 타입
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;

function resolveDispatcher() {// Hooks에서 호출하는 함수 코드.
  const dispatcher = ReactCurrentDispatcher.current; // ReactCurrentDispatcher의 current를 본다.
  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://reactjs.org/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라는 것에서 받아온 Hooks를 실행한다. 그 과정에서 해당 호출이 유효한지 검증하는 정도와 type 추론을 위한 type 주입 정도 로직이 존재한다.

하지만 어떻게 보면 해당 코드들이 리액트의 인터페이스에 해당하니 개인적인 관심이 있다면 추가적으로 찾아보는 것도 좋을 거 같다.

이제 ReactCurrentDispatcher라는 파일을 살펴보자.

아주 짧은 단순한 타입 추론에 대한 내용이고 굳이 표현하자면 Dispatcher이거나 null이다. 해당 Dispatcher라는 타입의 경우 react-reconciler라는 패키지의 src폴더의 ReactInternalTypes라는 파일에서 불러온다.

 

react-reconciler라는 패키지는 리액트 저장소의 packages 폴더 안에 존재하는 패키지중 하나인데 리액트 렌더러를 만드는 용도로 사용되는 패키지이다.(이걸로 커스텀 렌더러를 만들 수 있을 거 같다. 요즘 이슈가 된 리믹스를 까 보진 않았으나 그게 이걸 활용한 게 아닐까 짐작해본다.)

ReactInternalTypes.js라는 파일의 경우 391 라인 정도 되고 사실 순수하게 타입에 관한 내용이다 보니 관련 있는 부분만 따로 가져오겠다.

type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;

export type Dispatcher = {|
  getCacheSignal?: () => AbortSignal,
  getCacheForType?: <T>(resourceType: () => T) => T,
  readContext<T>(context: ReactContext<T>): T,
  useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
  useReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: (I) => S,
  ): [S, Dispatch<A>],
  useContext<T>(context: ReactContext<T>): T,
  useRef<T>(initialValue: T): {|current: T|},
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useInsertionEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useLayoutEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
  useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
  useImperativeHandle<T>(
    ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
    create: () => T,
    deps: Array<mixed> | void | null,
  ): void,
  useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
  useDeferredValue<T>(value: T): T,
  useTransition(): [
    boolean,
    (callback: () => void, options?: StartTransitionOptions) => void,
  ],
  useMutableSource<Source, Snapshot>(
    source: MutableSource<Source>,
    getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
    subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
  ): Snapshot,
  useSyncExternalStore<T>(
    subscribe: (() => void) => () => void,
    getSnapshot: () => T,
    getServerSnapshot?: () => T,
  ): T,
  useId(): string,
  useCacheRefresh?: () => <T>(?() => T, ?T) => void,

  unstable_isNewReconciler?: boolean,
|};

이게 React가 Hooks를 정의하는 부분인데 type이라는 지시어에서 알 수 있다시피 순수하게 type이다. 즉 React Hooks의 경우 처음 패키지에서 설명한 것처럼 react 패키지에서 정의 내린 타입을 토대로 react-dom 내부의 로직을 호출한다는 명쾌한 결론을 얻을 수 있다.

이게 React Hooks를 React 컴포넌트 외부에서 호출할 수 없는 이유이다. 예외적으로 몇 가지 Hook이나 클래스 컴포넌트에 사용되는 componets 타입의 경우 순수 타입이 아닌 실체가 존재하지만 대부분의 React Hooks의 경우 그저 순수한 타입에 해당한다.

처음에 목표로 했던 React패키지와 그 패키지에 속한 React Hooks에 대한 분석이 끝났으니 해당 포스팅은 여기서 종료된다.

추가적인 리액트에 대한 분석의 경우 추후 시간적 여유와 개인적 관심사에 따라서 추가적으로 진행할 예정이고 React Hooks의 실제 작동 로직의 경우 그때 가서 분석해봐야겠다.

반응형