React 톺아보기 - 03. Hooks_1

류지승·2024년 7월 13일

React

목록 보기
3/19

hook을 통해 Component state를 update한다.

hook의 동작원리
1. 훅 구현체 찾아가기
2. 훅은 어떻게 생성되는가?(mount)
3. 훅은 어떻게 상태를 변경하고 컴포넌트를 리-렌더링시키는가?
4. 상태가 변경되어 리렌더될 떄 변경된 상태 값은 어떻게 가지고 오는 것일까?(update)

훅 구현체 찾아가기

1-1 hook의 구현체는 어디에 있을까?

import { useState } from "react";

우리는 일반적으로 react에서 useState를 갖고오지만, 실질적으론 ReactHooks.js module에서 들고온다.

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

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

// ReactHooks.js
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)
}

// ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
  current: null,
}

export default ReactCurrentDispatcher

아직까지 내 생각 react core package에는 useState 관련 hook정의가 존재하지 않는다.

이해가 안 돼요..
코어와 훅 사이의 관계에 대해서 좀만 더 생각해보자면 코어는 컴포넌트의 모델인 React element만 알고 있습니다. 이 element를 인스턴스화되기 전인 클래스라고 생각해보자면 훅은 이 클래스의 인스턴스화된 객체의 상태 값을 관리하는 역할을 합니다. 이렇게 따져보면 둘 사이의 괴리감이 존재합니다.

React element는 fiber로 확장되고 나서야 살아 숨 쉬게 됩니다. 그리고 이 역할은 reconciler가 가지고 있습니다. 그러므로 훅 또한 reconciler가 알고 있는 것이 맞습니다.

근데 위 리액트 코어를 보면 어디에도 reconciler에서 훅을 가져오는 부분을 확인하지 못했습니다. 그렇다는 말은 반대로 훅 객체를 외부에서 내부로 ReactCurrentDispatcher.current를 통해 주입해준다는 말이 됩니다.

이렇듯 코어는 다른 패키지의 기능을 개발자에게 제공해 줄 때 의존성을 자기가 만들지 않고 외부에서 주입 받습니다. 스프링의 DI(Dependency Injection) 처럼요.

그리고 한발 더 나아가 리액트는 외부에서 의존성을 주입할 때 코어에 직접 주입하지 않습니다. 중간자를 하나 더 두게 되는데 코어에서는 ReactSharedInternals.js가 이에 해당하고 리액트 프로젝트 전체로 보면 shared라는 패키지가 이 역할을 합니다.
대충은 이해됐다.. 오늘 와서 같이 얘기를 해봐야할 듯?

reconciler -> shared/ReactSharedInternal -> react/ReactSharedInternal -> react/ReactCurrentDispatcher -> react/ReactHooks -> react -> 개발자로 useState를 구현함.

Hook 구현체 주입

// reconciler/ReactFiberHook.js
export function renderWithHooks(
  current: Fiber,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime
) {
  /*...*/
  currentlyRenderingFiber = workInProgress // 현재 작업 중인 fiber를 전역으로 잡아둠
  
  // current가 존재하면 mount가 이미 되었고 값은 current.memoizedState에 할당되어 있음. 
  // mount가 되어 있지 않으면, current.memoizedState가 존재하지 않으므로 null 
  nextCurrentHook = current !== null ? current.memoizedState : null

  // current가 null이면 mount를 처리해야하고 null이 아니면 값이 존재하므로 update를 처리해야함.
  ReactCurrentDispatcher.current =
    nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate

  let children = Component(props, refOrContext)

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

  const renderedWork = currentlyRenderingFiber
  renderedWork.memoizedState = firstWorkInProgressHook
// 컴포넌트를 만들었는데, 개발자 실수로 hook이 호출되면 이를 막기위해 error 발생
  ReactCurrentDispatcher.current = ContextOnlyDispatcher

  currentlyRenderingFiber = null
  /*...*/
}

// reconciler > ReactFiberHooks.js
// mount
const HooksDispatcherOnMount = {
  useState: mountState,
  useEffect: mountEffect,
  /*...*/
};

// update
const HooksDispatcherOnUpdate: = {
  useState: updateState,
  useEffect: updateEffect,
  /*...*/
};

// invalid hook call
export const ContextOnlyDispatcher: Dispatcher = {
  useState: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  /*...*/
};

훅은 어떻게 생성되는가?

훅 객체 만들기

component가 mount될 때 useState를 호출할 경우 fiber의 memoizedState는 null이므로 HooksDispatcherOnMount에서 mount 구현체인 mountState()를 사용한다.

// reconciler/ReactFiberHooks.js
function mountState(initialState) {
  const hook = mountWorkInProgressHook() // 훅 객체를 생성한다.
  /*...*/
}

function mountWorkInProgressHook(): Hook {
  // hook 객체에 대해서는 업데이트 구현체에서 자세히 다룹니다.
  const hook: Hook = {
    memoizedState: null, // 컴포넌트에 적용된 마지막 상태 값
    queue: null, // 훅이 호출될 때마다 update를 연결 리스트로 queue에 집어넣습니다.
    next: null, // 다음 훅을 가리키는 포인터

    // 업데이트 구현체에서 설명
    baseState: null,
    baseUpdate: null,
  }

  if (workInProgressHook === null) {
    // 맨 처음 실행되는 훅인 경우 연결 리스트의 head로 잡아둠
    firstWorkInProgressHook = workInProgressHook = hook
  } else {
    // 두번 째부터는 연결 리스트에 추가
    workInProgressHook = workInProgressHook.next = hook
  }
  return workInProgressHook
}

연결 리스트를 사용해서 이전 값들을 저장하는 이유
1. 상태 업데이트의 효율성
React는 상태 업데이트 시 효율적인 재렌더링을 위해 다양한 최적화 기법을 사용합니다. 상태가 변경될 때마다 컴포넌트는 다시 렌더링되지만, React는 변경된 부분만을 효율적으로 업데이트하려고 합니다. 이를 위해 상태 업데이트의 변화를 추적하고, 이전 상태와 현재 상태를 비교하는 작업이 필요합니다.

  1. 상태 업데이트의 일관성
    상태 업데이트는 비동기적으로 처리될 수 있으며, 여러 상태 업데이트가 한 번에 이루어질 수도 있습니다. 이때, 상태 업데이트의 순서를 보장하고, 일관된 상태를 유지하기 위해 이전 상태 값을 참조해야 할 필요가 있습니다.
  1. 이전 상태 값을 통한 최적화
    상태 업데이트 함수는 종종 이전 상태 값을 기반으로 새로운 상태를 계산해야 합니다. 예를 들어, 카운터를 증가시키는 함수에서는 이전 카운트 값을 기반으로 새로운 카운트 값을 계산해야 합니다. React는 이를 위해 상태 업데이트 함수에서 이전 상태 값을 참조할 수 있도록 합니다.

update를 담을 queue 생성

훅을 이용하여 컴포넌트 상태를 변경하고자 할 때 업데이트 정보를 담고 있는 update 객체가 생성된다.

// reconciler/ReactFiberHook.js
function mountState(initialState) {
  /*...*/
  // hook.memoizedState = hook.baseState = initialState;

  const queue = (hook.queue = {
    last: null, // 마지막 update
    dispatch: null, // push 함수

    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  })

  const dispatch = (queue.dispatch = dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue
  ))
  return [hook.memoizedState, dispatch]
}

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action
}

훅은 어떻게 상태를 변경하고 컴포넌트를 리-렌더링시키는가?

훅 상태를 업데이트하는 dispatchAction

함수 dispatchAction은 queue에 update를 push함과 동시에 scheduler에게 Work를 예약하는 함수입니다.

function dispatchAction(fiber, queue, action) {
  const alternate = fiber.alternate

  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // Render phase update
  } else {
    // idle update(유후 상태)
  }
}

유후상태(idle update) -
유후 상태(Idling state)란 시스템이 활동을 멈추고 대기하고 있는 상태를 의미합니다. 이 상태에서는 시스템이 명령을 기다리고 있지만 실제 작업을 수행하지는 않고 있는 상태입니다.

  1. currentlyRenderingFiber는 renderWithHooks()에서 workInProgress로 할당됩니다.
  2. fiber === currentlyRenderingFiber라는 뜻은 currentlyRenderingFiber이 존재한다는 뜻이고 이는 Render phase가 진행중이라는 뜻이다.
  3. VDOM은 하나의 노드를 current와 workInProgress로 관리한다고 했습니다. 하지만 우리는 fiber를 bind()를 통해 고정해 놨습니다. 문제는 current와 workInProgress는 고정이 아닌 Commit phase를 지나면 서로 교체 된다는 점입니다. 그래서 현재 작업 중인 currentlyRenderingFiber가 둘 중 어떤 것인지 알 수가 없습니다. 이 때문에 fiber와 alternate를 모두 비교해야지만 올바르게 Render phase update임을 확인할 수 있습니다.
profile
성실(誠實)한 사람만이 목표를 성실(成實)한다

0개의 댓글