/images/article/9/9-9.webp

useState 원리 살피기

useState는 어떻게 상태를 업데이트할까

Tech

들어가며

  • 이 글은 useState가 내부에서 어떻게 동작하는지 코드 레벨에서 학습하였습니다.
    • if (__DEV__)와 같이 개발환경에서만 동작하는 코드는 제외하였습니다.
    • 학습한 내용을 기반으로 도식화하여 이해하기 쉽게 정리하였습니다.
  • 언제 무엇이 어디에 저장되고 어떤 순간에 리렌더 되는지 이해하는 것을 목표로 합니다.

useState 기본 사용법 복습

useState의 기본적인 사용법입니다.

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((v) => v + 1)}>{count}</button>;
}
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((v) => v + 1)}>{count}</button>;
}
  • useState에 기본값 0 을 전달하면, 현재 값(count)과 업데이터(setCount)를 반환합니다.
  • 이 글에서는 useState의 세 가지 부분에 대해서 다룹니다.
    • 마운트시 useState
    • setState 로 상태를 업데이트
    • 리렌더링시 useState

Fiber와 Hook

useState에 대해 알아보기 전에, Hook의 구조와 이를 포함하는 Fiber에 대해서 알아볼 필요가 있습니다.

React에서 Hook에 대한 정보는 컴포넌트가 정보가 저장되어있는 Fiber라는 객체안에 저장되어있습니다.

그럼 Fiber란 무엇일까요? 간단히 알아봅시다.

Fiber

useState 파헤치기

  • 리액트는 함수 컴포넌트 하나당 하나의 Fiber 객체를 갖습니다.
  • 이러한 Fiber는 리액트가 렌더링을 효율적으로 관리하기 위한 아키텍처입니다.
  • 이번 렌더에서 계산할 입력/상태/우선순위 등의 정보를 담고, 트리 탐색을 가능하게 합니다.

Fiber 속 Hook 객체

useState 파헤치기

  • fiber.memoizedState 는 컴포넌트내에 선언된 첫번째 훅을 가리킵니다.
  • 각 훅은 이어져있고 훅 호출 순서가 리스트의 순서입니다.

1. 마운트시 mountState 의 동작

컴포넌트가 마운트시 useState 내부에서는 mountState 라는 함수가 동작합니다.

<Title /> 라는 컴포넌트를 예시로 단계별로 알아보겠습니다.

useState 파헤치기

  • 기본 훅 객체를 Fiber에 연결합니다.

useState 파헤치기

  • 초기값이 함수인 경우는 실행하고 값인경우는 그대로 memoizedState 에 저장합니다.
    • Fiber에도 memoizedState (Hook들의 정보) 가 있고, Hook에도 memoizedState (해당 Hook의 계산된 값)가 있습니다.

useState 파헤치기

  • 업데이트 정보가 저장될 queue 를 만들어 Hook 객체에 연결합니다.

useState 파헤치기

  • Fiber와 Hook의 정보를 가진 dispatch (setState)를 만듭니다.
  • setState 함수를 실행하기만 해도, 어떤 컴포넌트의 어떤 훅에 대한 업데이트인지 바로 판단할 수 있습니다.

useState 파헤치기

  • memoizedStatedispatch를 배열로 리턴합니다.

2. setState 호출 시 일어나는 일

useState 파헤치기

  • setState 가 실행되면, 내부적으로 구현된 dispatch에는 Fiber와 Hook 정보가 담겨있어서 어디에 업데이트를 연결해야하는지 정보를 가지고 있습니다.
  • setState 가 실행되면, 현재 컨텍스트를 보고 업데이트 우선순위(lane)를 결정합니다.
  • setState 코드를 기반으로 업데이트 객체를 만듭니다.

useState 파헤치기

  • 업데이트 객체를 큐형태로 Hook 객체 안에 queue.pending에 추가합니다.

useState 파헤치기

  • setState 의 정보를 가지고 있는 Fiber lanes에 업데이트 플래그를 추가합니다.
  • 부모 Fiber로 올라가면서 childLanes 에 업데이트 플래그를 추가합니다.
  • 그리고 루트 객체인 FiberRoot 의 pendingLanes에도 업데이트 플래그를 추가하고 스케줄러에 등록합니다.
    • FiberRoot는 ReactDOM.createRoot 를 통해서 생성됩니다.

3. 업데이트시 updateState 의 동작

setState가 아니라, 리렌더링이 발생하고 useState를 만났을때 동작하는 과정입니다.

리렌더링 중에 useState를 만나면 내부적으로는 updateState 로 동작합니다.

useState 파헤치기

  • 렌더 중 useState를 만나게 되면, Fiber에서 현재 훅을 찾습니다.

useState 파헤치기

  • 훅 객체에서 [이전 렌더에서 남은 업데이트] + [새로 쌓인 업데이트] 을 합쳐서 원형 큐로 만듭니다.

useState 파헤치기

  • 렌더는 업데이트 우선순위를 가지고, 각 업데이트 객체도 우선순위(lane)를 가지고 있습니다.
  • 이번 렌더 우선순위에 해당하는 업데이트만 계산합니다.

useState 파헤치기

  • 계산된 결과는 훅 객체내에 memoizedState 에 저장합니다.
  • 계산 되지 않은 업데이트는 baseQueue 에 저장하여 다음 렌더로 넘깁니다.

추가 학습

정리하기

  • 구조
    • fiber.memoizedState → 훅 리스트의 헤드
    • HookmemoizedState(값), baseQueue(이전렌더 업데이트), queue(새 업데이트), next(다음 훅)
  • 마운트(mountState)
    • 훅 노드 생성
    • 초기값 저장
    • 훅 전용 queue 생성
    • setState(dispatch) 바인딩
    • [state, setState] 리턴
  • 업데이트(updateState)
    • fiber.memoizedState에서 현재 훅 찾기
    • pending(새 배치) + baseQueue(남은 것) 병합
    • 현재 lane에 해당하는건 적용, 그 외 스킵
    • 결과를 memoizedState에 기록
  • setState 호출
    1. update 객체 생성(필요하면 eagerState 계산)
    2. 훅의 queue.pending에 삽입
    3. scheduleUpdateOnFiberlanes/childLanes 전파
    4. 루트 스케줄 등록
    5. 다음 렌더에서 큐 소비 후 커밋
읽어주셔서 감사합니다