React 내부 동작원리를 알아보자(4) - useState의 state는 어떻게 생겼을까?

방구석 코딩쟁이·2024년 1월 24일
1

이 시리즈는 "가장 쉬운 웹개발 with Boaz" 님의 [React 까보기 시리즈] 를 기반으로 만들어졌습니다.

useState()함수를 호출하게 되면 statesetState를 반환받습니다.

우리가 useState 함수를 호출하게 되면 hook객체를 만들게 됩니다. 이 hook객체가 1:1로 가지고 있는 queue객체에 대해서 같이 살펴봐야합니다.
또한, queue객체는 update객체를 담고 있습니다.

먼저 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];
};

여기서 훅을 얻기 위해 mountWorkInProgressHook 함수를 호출하네요.

mountWorkInProgressHook 함수

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

여기서 hook 객체가 보입니다.

hook 객체

useState를 호출할 때, 당연히 state 값을 가지게 될 것이고, 이것을 관리하는 속성이memoizedState 입니다. 즉, 컴포넌트에 적용된 마지막 상태값입니다.
baseStatebaseUpdate 속성은 업데이트에 관련된 속성입니다.
next는 링크드리스트를 위한 속성값입니다. 다음 훅을 연결하는 링크드 리스트의 속성입니다.
queuehook 객체가 가지고 있는 또 다른 queue 객체를 가지게 됩니다.

이렇게 저장된 링크드 리스트는 어디에 저장될까?
fiber안에 firstWorkInProgressHook이 할당되고 있었으며 fiber안에 저장된 firstWorkInProgressHook이 첫번째 hook이자 링크드 리스트의 head입니다.

해당 코드는 아래와 같았습니다.

const renderedWork: Fiber = (currentlyRenderingFiber: any);

renderedWork.memoizedState = firstWorkInProgressHook;

즉, fibermemoizedState 프로퍼티 안에 hook노드로 구성된 링크드리스트가 저장되어 있던 것입니다.

이후 로직 설명

workInProgress가 null인 경우는 작업 중인 hook이 없다는 의미이므로 첫 번째 hook으로 할당됩니다.

  • workInProgressfirstWorkInProgress에 차례대로 할당되어 집니다.

반대로 workInProgressHook이 null이 아닌 경우는 작업 중인 hook이 있다는 뜻이므로 next 속성에 새로운 hook을 연결해줍니다.

  • 기존 workInProgress.nexthook을 할당한 다음에 workInProgresshook을 할당하여 temp 변수 없이도 연결을 시킬 수 있게 됩니다.

그 다음 workInProgressHook을 반환하게 됩니다. 즉, 내부적으로 생성된 hook 객체를 반환하는 거죠.

mountState 함수

이제 다시 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];
};

코드를 살펴보면 인자로 받은 initialState가 함수인 경우, 함수를 실행한 값을 initialState로 다시 할당해줍니다.

저희가 useState의 인자로 함수를 넣을 수 있었던 이유가 여기 있었습니다.

그 다음, hook의 memoizedState, baseState 프로퍼티에 initialState를 할당하게 됩니다.
최종적으로 render하고 있는 state 값을 memoizedState에 저장하고 있습니다.
(저희는 일단 mount하였을 때만 가정하므로 초기값이 initialState로 저장되는 것이죠.)
이 이후에는 mount 구현체가 아닌 update 구현체를 사용하므로 initialState가 실행되는 일은 없습니다.


queue 객체

그 다음 queue객체를 봅시다.

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

만약, 한 번의 컴포넌트 호출에서 단일 훅의 setState()가 여러 번 호출되었다면 매 호출 생성된 update 객체는 이 queue에 쌓이게 되는 것입니다. 그 후 컴포넌트가 리렌더링 될 때 queue에 저장되어 있던 update을 차례대로 실행해 최종적으로 적용될 state를 도출하게 됩니다.

먼저 코드를 보면 queue객체는 hook.queue에도 할당되고, queue 변수에도 할당되는 것을 볼 수 있습니다.
그리고 queue 객체는 4가지 프로퍼티를 담고 있는데, 그중 lastdispatch에 대해 먼저 보도록 하겠습니다.
먼저 last에는 마지막 업데이트가 저장되어 있습니다.

queue 객체 안에는 update 객체가 큐의 형태로 저장됩니다.

  • update 객체는 상태를 업데이트하기 위해 필요한 정보를 가지고 있는 자바스크립트 객체입니다.
  • lastupdate 객체의 마지막을 가지고 있게 됩니다.
  • queue는 Circular Linked List의 형태로 구현되어 있습니다.

dispatch 속성에는 밑에 선언된 dispatch 함수를 할당하게 됩니다.
dispatch 함수는 queue 객체에 update를 추가할 수 있는 함수를 의미합니다.

코드를 통해 보면 dispatchAction이라는 함수를 할당하고 있습니다.

  • bind()를 통해 currentlyRenderingFiber(hook과 매핑되는 컴포넌트의 fiber)와 queue를 bind 하고 있습니다. 왜냐하면 dispatch는 우리가 사용하는 setState의 모습으로 외부로 노출이 되기 때문입니다.

예시를 들어 설명하면 좀 더 이해가 잘 될 것이므로, 예시를 들어보겠습니다.

function FunctionComponent() {
  const [a, setA] = useState(0) // aHook
  const [b, setB] = useState(0) // bHook
  setA(_a => _a + 1) // firstUpdate
  setA(_a => _a + 1) // secondUpdate
  setA(_a => _a + 1) // thirdUpdate
}

FunctionComponent는 Babel을 통해 React.createElement() 함수가 호출되어 React element로, VDOM에 올라가야 하므로 fiber 노드로 확장될 것 입니다.

지금까지 분석한 Fiber 아키텍처에 따르면 FunctionComponentfiber는 아래처럼 되겠죠?
물론 더 많은 속성들이 있긴 합니다...

profile
풀스택으로 나아가기

0개의 댓글