Tech

useState 원리 살피기

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

들어가며

  • 이 글은 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

함수 컴포넌트 하나에 대응하는 Fiber 객체 구조

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

Fiber 속 Hook 객체

Fiber의 memoizedState에 훅이 연결 리스트로 매달린 구조

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

1. 마운트시 mountState 의 동작

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

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

마운트 1단계 - 기본 훅 객체를 만들어 Fiber에 연결

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

마운트 2단계 - 초기값을 계산해 훅의 memoizedState에 저장

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

마운트 3단계 - 업데이트 큐를 만들어 훅 객체에 연결

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

마운트 4단계 - Fiber와 훅 정보를 담은 dispatch 생성

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

마운트 5단계 - state와 setState를 배열로 반환

  • memoizedStatedispatch를 배열로 리턴한다.

2. setState 호출 시 일어나는 일

setState 호출 시 업데이트 우선순위를 정하고 업데이트 객체를 만드는 과정

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

만들어진 업데이트 객체가 훅의 queue.pending에 쌓이는 모습

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

Fiber의 lanes와 부모의 childLanes로 업데이트 플래그가 전파되는 모습

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

3. 업데이트시 updateState 의 동작

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

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

리렌더 1단계 - Fiber에서 현재 훅을 찾는 과정

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

리렌더 2단계 - 남은 업데이트와 새 업데이트를 합쳐 원형 큐를 만드는 과정

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

리렌더 3단계 - 이번 렌더 우선순위에 해당하는 업데이트만 계산

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

리렌더 4단계 - 계산 결과를 memoizedState에 저장하고 남은 것은 baseQueue로

  • 계산된 결과는 훅 객체내에 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. 다음 렌더에서 큐 소비 후 커밋
이 글이 도움이 되었나요?좋아요는 다음 글을 쓰는 힘이 됩니다

다른 글