[React] 스냅샷으로서의 state

현수·2025년 3월 13일
0

useState의 동작

컴포넌트 내부에 선언한 변수는 컴포넌트가 다시 호출될 때마다 초기화된다. 그러나 컴포넌트 리렌더링이 일어난다고 해서 state가 초기화되진 않는다. 컴포넌트 안에 state를 선언했는데 어떻게 그게 가능할까?

state는 함수의 실행 및 종료에 관계없이 리액트 자체에 존재하기 때문이다.

let stateStorage = {}; // 컴포넌트별 상태 저장소

function useState(initialValue) {
  const componentId = getCurrentComponentId(); // 현재 컴포넌트의 ID 가져오기
  
  if (!(componentId in stateStorage)) {
    stateStorage[componentId] = initialValue; // 최초 렌더링 시 초기화
  }
  
  return [
    stateStorage[componentId], // 저장된 상태 반환
    (newValue) => {
      stateStorage[componentId] = newValue; // 상태 업데이트
      reRenderComponent(componentId); // 해당 컴포넌트만 리렌더링
    },
  ];
}

GPT가 리액트에서 상태를 관리하는 방식을 간략히 보여줬다. 한 번도 선언된 적 없는 state는 useState의 인자로 들어온 값으로 초기화하고, 그 외에는 리액트 자체적으로 저장된 상태를 반환한다.

즉, 컴포넌트 호출 시 리액트는 호출 당시의 state를 스냅샷으로서 제공한다. 그리고 그 state가 props, 이벤트 핸들러, 로컬 변수 등 컴포넌트 내부의 모든 계산에 사용된다. 이게 스냅샷으로서의 state의 개괄적인 개념이다.

지난 포스팅의 예제를 다시 가져와 봤다. number의 현재 값이 0이라면, 버튼 클릭 시 동작은 다음과 같다.

<button onClick={() => {
  setNumber(number + 1); // setNumber(1)
  setNumber(number + 2); // setNumber(2)
  setNumber(number + 3); // setNumber(3)
}}>+3</button>
  1. 상태 변화 계산
    이때 현재의 스냅샷은 number <- 0이므로 수행은 주석과 같다.
    따라서 최종적으로 number <- 3으로의 변화가 계산된다.

  2. 렌더링 트리거
    number 업데이트가 렌더링을 트리거한다.

  3. 컴포넌트 렌더링
    이제 새 스냅샷에는 number <- 3이 찍혀 있고, 모든 number와 number를 사용하는 props, 이벤트 핸들러, 로컬 변수는 number <- 3을 이용해 계산된다.

  4. DOM에 커밋

다음 예제에서 alert에 표시되는 값은 뭘까? 정답을 맞힌다면 스냅샷의 개념을 잘 이해했다고 봐도 되겠다!

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        alert(number);
      }}>+5</button>
    </>
  )
}

setState의 동작

이제부터는 state 업데이트 큐의 내용이다. 자연스럽게 연결되는 내용이라 한꺼번에 정리했다.

한때 나는 업데이터 함수를 남발해 왔다. 업데이터 함수란 setNumber(prev => prev + 3)에서 prev => prev + 3을 말한다. state의 동작을 정확히 이해하지 못했던 때에는 state가 바로 업데이트되지 않을 우려가 있을 때 무적의 prev를 꺼내는 줄 알았다...💦💦 하지만 state는 스냅샷으로서 동작하며, 렌더링 시 컴포넌트 전체에서 동일한 값을 가진다는 것을 이제는 안다.

아래 예제는 업데이터 함수를 사용했다. 왠지 결과는 n <- n + 3이 될 것만 같다. 그 이유를 살펴보자.

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(prev => prev + 1);
        setNumber(prev => prev + 1);
        setNumber(prev => prev + 1);
      }}>+3</button>
    </>
  )
}

setState는 사실 바로 값을 업데이트하지 않는다. 대신에 state 업데이트 큐에 인자로 들어온 값을 저장한다. 지난 포스팅에서 언급한 배치 업데이트의 핵심이 여기 있다고도 볼 수 있겠다. 그러니까 위 코드를 실행한 결과 큐에는 첫 번째 표와 같이 값이 저장된다.

그러면 다음 useState가 호출될 때, 즉, 다음 렌더링 중에 큐를 순회하며 state를 업데이트한다. 큐를 순회하면서 반환값을 prev에 넘겨주기 때문에 아래 표에서와 같이 연쇄적인 업데이트가 가능해진다.

prev => prev + 1
prev => prev + 1
prev => prev + 1

prev 다음 업데이트 return
0 prev => prev + 1 0 + 1
1 prev => prev + 1 1 + 1
2 prev => prev + 1 2 + 1

이 방식으로 처음 예제도 다시 보자.

버튼을 클릭하면, setNumber는 사실상 주석과 같이 동작하기 때문에 큐에는 첫 번째 표와 같이 값이 저장될 것이다. prev 값은 사용하지 않고, number를 계속해서 덮어씌우면서 큐를 순회하여 최종적으로는 number에 3이 저장되게 된다.

<button onClick={() => {
  setNumber(number + 1); // setNumber(1)
  setNumber(number + 2); // setNumber(2)
  setNumber(number + 3); // setNumber(3)
}}>+3</button>
number를 1로 바꾸기
number를 2로 바꾸기
number를 3로 바꾸기

prev 다음 업데이트 return
0 (사용 안 함) number를 1로 바꾸기 1
1 (사용 안 함) number를 2로 바꾸기 2
2 (사용 안 함) number를 3으로 바꾸기 3

업데이터 함수의 매개변수 이름은 그냥 앞자를 따서 n을 사용해도 되고, 명시적으로 prevNumber, prev, ... 등으로도 사용한다.

이제 리액트의 핵심 기능인 state 전반에 대한 이해가 가능해진 것 같다! 다음 포스팅에서는 State 구조 선택하기를 정리하면서 어떻게 state를 잘 관리할지 살펴보려고 한다.

0개의 댓글