본문 바로가기
React

[React] 오픈소스 살펴보기_Hook의 구현체를 찾아서

by 2__50 2023. 12. 1.
공부한 내용을 정리한 글입니다 
내용에 오류가 있거나 더 좋은 의견이 있다면 댓글로 남겨주세요.
배움에 큰 도움이 됩니다. 🖋

 

 

1-1. Hook의 구현체를 찾아서


아 학습지에 물 쏟음;;.jpeg  / https://twitter.com/danji_04/status/1463781251320463362

 

 

함수형 컴포넌트에서 useState나 useEffect등의 hook을 불러오는 import문은 일상속의 물처럼 코드에 자연스럽게 스며들어 있다. Hook은 마치 물 속에 있는 수소2 + 산소1의 물 분자처럼 컴포넌트를 구성한다. 그런데 이 hook들은 react 내부에 어떻게 구성되어 있을까? 오늘 react 패키지 내부 구조를 탐험하며 내부에 어떤 일들이 일어나고 있는지 알아보자.

 

개발자가 hook을 불러오면 react는 다음 순서로 구현체를 찾아간다.

 

 

 

 

하나씩 차근히 알아보자.

 

 

1. 개발자

개발자가 컴포넌트에서 react 객체가 제공하는 hook을 불러온다.

 

import { useState, useEffect } from 'react';

 

 


2. React.js

개발자가 불러온 React 객체는 이곳에서 내보내진다.

React.js는 내보내는 다양한 hook을 모두 ReactHooks.js에서 가져왔다.

 

import { useState, useEffect, ... } from './ReactHooks'
import ReactSharedInternals from './ReactSharedInternals' // 의존성을 주입받는 징검다리

const React = {
  useState,
  useEffect,
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
  /*...*/
}

export default React

 



3. ReactHooks.js
React.js에서 hook을 가져오는 곳이지만 실제 hook의 구현은 직접적으로 담겨있지 않다.
대신 이곳에서 ReactCurrentDispatcher.current가 가리키는 디스패처의 메소드를 호출한다.
이 디스패처는 현재 활성화된 렌더러에 따라 변경되는데,  Renderer1이 렌더링하는 동안에는 current가 Dispatcher1을 가리키고, Renderer2가 렌더링하는 동안에는 current가 Dispatcher2를 가리킨다. useState, useEffect와 같은 hook은 현재 활성화된 렌더러에 따라 디스패처의 메소드를 호출하게 되는 것이다. react는 이런 방식으로 다양한 렌더러에서의 동작을 유연하게 수행한다.

 

import ReactCurrentDispatcher from './ReactCurrentDispatcher'

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current
  return dispatcher
}

export function useState(initialState) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useState(initialState)
}

export function useEffect(create, inputs) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useEffect(create, inputs)
}
/*...*/

 

 

 

4. ReactCurrentDispatcher.js

ReactSharedInternals를 통해 얻은 정보를 사용해 현재 사용 중인 dispatcher를 관리하는 곳이다.

이를 통해 다양한 렌더러가 서로 다른 디스패처를 사용할 수 있으며, 초기값은 null이다.

 

const ReactCurrentDispatcher = {
  current: null,
}

export default ReactCurrentDispatcher



 

5. react / ReactSharedInternals.js

Shared/ReactSharedInternals에서 가져온 정보를 활용해 react 전체에서 일관된 동작을 유지하는 곳이다.
이 모듈은 react의 핵심 부분들이 공유하는 내부 정보를 포함한다.
  ‎

import ReactCurrentDispatcher from './ReactCurrentDispatcher'
import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'
import ReactCurrentOwner from './ReactCurrentOwner'
/*...*/

const ReactSharedInternals = {
  ReactCurrentDispatcher,
  ReactCurrentBatchConfig,
  ReactCurrentOwner,
  /*...*/
}

export default ReactSharedInternals

 

 

 

5. shared / ReactSharedInternals.js

React 내부의 여러 모듈이 서로 소통하고 특정 정보에 접근하기 위해서는 전역적으로 공유되는 객체나 함수가 필요하다.
이러한 중간 매개체 역할을 하는 곳이 shared / ReactSharedInternals.js다.

 

import React from 'react'

const ReactSharedInternals =
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED // core의 ReactSharedInternals.js

if (!ReactSharedInternals.hasOwnProperty('ReactCurrentDispatcher')) {
  ReactSharedInternals.ReactCurrentDispatcher = {
    current: null,
  }
}
/*...*/

export default ReactSharedInternals

 

 

 

6. Reconciler

React는 Reconciler(조정기)를 거치며 내부적으로 필요한 정보를 shared/ReactSharedInternals에 전달한다.

Reconciler는 react의 핵심 알고리즘을 담당하는 곳이며 react의 가상 DOM을 관리하고 실제 DOM에 반영하는 역할을 한다.

변경 사항이 발생하면 이를 순차적으로 처리한다.

 

 

1-2. Hook 구현체의 주입


 

1. reconciler/ReactFiberHooks.js

React는 렌더링 단계에서 renderWithHooks() 함수를 통해 hook을 주입하고 관리한다.

이 함수는 컴포넌트를 렌더링하며 해당 컴포넌트에 맞는 hook을 설정하는데, 이 때 개발자가 작성한 함수형 컴포넌트(컴포넌트의 타입)를 인자로 받아 해당 컴포넌트를 호출한다. 중요한 점은, 함수가 호출되는 시점에 ReactCurrentDispatcher.current가 가리키는 디스패처가 바뀐다는 것이다.

 

디스패처는 컴포넌트가 처음 마운트되는 시점에는 HooksDispatcherOnMount, 업데이트 시점에는 HooksDispatcherOnUpdate, 라이프사이클이 끝나면 ContextOnlyDispatcher를 가르킨다. 라이프사이클 종료 후 ContextOnlyDispatcher를 가리키는 이유는 컴포넌트 실행 시점이 지났을 때 hook 호출이 발생하는 것을 방지해 개발자가 hook을 올바르게 사용하도록 안내하기 위함이다.

 

 

export function renderWithHooks(
  current: Fiber,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime
) {
  /*...*/
  currentlyRenderingFiber = workInProgress // 현재 작업 중인 fiber를 전역으로 잡아둠
  nextCurrentHook = current !== null ? current.memoizedState : null

  ReactCurrentDispatcher.current =
    nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate

  let children = Component(props, refOrContext)

  /*컴포넌트 재호출 로직..*/

  const renderedWork = currentlyRenderingFiber
  renderedWork.memoizedState = firstWorkInProgressHook

  ReactCurrentDispatcher.current = ContextOnlyDispatcher

  currentlyRenderingFiber = null
  /*...*/
}

 

 

 

한 줄 정리


React hook의 구현체를 따라가며 살펴보니,

react가 외부에서 주입되는 정보를 활용해 유연성을 높이고 다양한 환경에서 동작할 수 있도록 설계되어 있음을 알 수 있었다.

 

 

 

참고


https://goidle.github.io/react/in-depth-react-hooks_1/

 

React 톺아보기 - 03. Hooks_1 | Deep Dive Magic Code

모든 설명은 v16.12.0 버전 함수형 컴포넌트와 브라우저 환경을 기준으로 합니다. 버전에 따라 코드는 변경될 수 있으며 클래스 컴포넌트는 설명에서 제외됨을 알려 드립니다. 각 포스트의 주제는

goidle.github.io

 

'React' 카테고리의 다른 글

[Next.js] Redux  (0) 2023.08.06
React에서의 상태관리  (0) 2023.02.13
React의 useMemo와 useCallback  (0) 2023.02.09
React Component와 Hooks의 Lifecycle  (0) 2023.02.09
[React] React vs Vue  (0) 2020.07.02

댓글