FrontEnd Develop/React Deep Dive

useState는 여간 쉬운게 아니다.

Frisbeen 2025. 4. 3. 21:01

React 상태 관리 제대로 이해하기

React를 사용하다 보면 자주 마주치는 개념 중 하나가 상태(state)입니다.

하지만 상태의 작동 방식은 자칫 오해할 수 있는 비동기적 특성과 렌더링 사이클 덕분에 헷갈렸어서 시험기간 전 시간이 좀 남을때 글을 정리했습니다.


1. 상태는 비동기적 특성을 지닌다.

React에서 상태를 setState()로 변경하더라도, 즉시 반영되지 않습니다.

이는 React가 여러 상태 업데이트를 모아서 한 번에 처리하는 Batching 기법 때문입니다.

이 과정은 특정 렌더링 사이클 타이밍에 맞춰 일괄 처리됩니다.

 

따라서 변경되는 양상이 비동기적으로 보여서 상태는 정확히 비동기다. 라고도 오해할 수 있지만 그렇지는 않다고 합니다.

 

상태가 변경됨으로써, 리렌더링이 한번 이루어지고 나고, 또 한번 더 이뤄져야 그 전에 우리가 변경했던 id(상태)를 그제서야 보는 겁니다.

-> 즉시 바뀌는것이 아니다!

function App() {
  const [id, setId] = useState('');

  const onChangeEmail = (e) => {
    console.log('1️⃣ beforeChange:', id); // ''
    setId(e.target.value);
    console.log('2️⃣ afterChange:', id); // 여전히 이전 값 출력 ''
  };

  console.log('🔁 render:', id);

  return (
    <div>
      <input onChange={onChangeEmail} placeholder="이메일 입력" />
      <p>입력한 ID: {id}</p>
    </div>
  );
}

세 줄 요약

  • setId() 이후 바로 상태가 변경되지 않는다.
  • 변경된 상태는 다음 렌더링에서 반영된다.
  • 함수 내부에서 상태값을 바로 사용하면 이전 값을 보게 된다.

2. 파생 상태는 따로 만들지 말자

리액트의 핵심은 담백함에서 나온다.

React는 최소한의 상태를 가지고 UI를 그리는 것을 지향합니다.

파생 상태란, 다른 상태나 props에서 계산 가능한 값을 굳이 별도로 상태로 선언한 경우를 말합니다.

파생 상태를 쓴다면..

const [items, setItems] = useState([...]);
const [itemCount, setItemCount] = useState(items.length); // ❌

왜 문제인가요?

  • 데이터 불일치 발생 위험
  • 중복 데이터 관리로 인한 복잡도 증가
  • 불필요한 렌더링 유발 가능성

JS 문법을 쓰면 더 좋겠다.

const length = items.length; // 계산해서 사용하면 OK

3. 지연 초기화 (Lazy Initialization)

초기 상태값이 무거운 연산이라면, 매 렌더링마다 이 연산이 반복되면 비효율적입니다.

그렇다면 저 무거운 함수가 상태가 리렌더링 될때마다 실행될까?

그렇지 않습니다!

 

React의 useState(초기화)의 동작방식

초기화한 값이 함수면 (typeof initializer === ‘function’)이면, 이건 초기 렌더링 시에만 호출됩니다.

이후 리렌더링할때는 그 함수값을 캐싱하여 다시 부르지 않습니다. (너무 다행)

function veryHeavyComputation() {
  console.log('🔥 실행');
  let sum = 0;
  for (let i = 0; i < 1e8; i++) sum += i;
  return sum;
}

function App() {
  const [num, setNum] = useState(veryHeavyComputation); // ✅ 지연 초기화
  console.log('🌀 렌더링');
  return <button onClick={() => setNum(num + 1)}>{num}</button>;
}

 

  • 초기값 계산이 무겁다면 useState(() => someHeavyFn())처럼 함수 전달로 해결하는 이 방식이 지연 초기화이다.

4. setState()는 변덕쟁이다

상태는 비동기적으로 작동한다고 방금 읽으셨을텐데

아래 코드처럼 동일한 count 값을 기준으로 세 번 setCount(count + 1)을 해도 실제로는 한 번만 반영됩니다.

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

결과

🌀 렌더링 count: 1

왜냐하면 위 코드는 모두 동일한 0을 기준으로 "1로 바꿔줘!"를 3번 요청한 셈이라, 결국 1번만 처리되는 셈입니다.

 


5. 이전 상태(prev)를 사용하는 업데이트 방식

비동기 상태 변경을 정확하게 누적하고 싶다면, 이전 상태를 기반으로 한 업데이트 함수 방식으로 처리해야 합니다.

setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);

 

이 경우 React는 각각의 이전 상태를 기준으로 누적 계산하여 최종적으로 +3의 결과를 보여줍니다.

function App() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);
  renderCount.current += 1;

  const handleClick = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return (
    <div>
      <p>카운트: {count}</p>
      <p>렌더링 횟수: {renderCount.current}</p>
      <button onClick={handleClick}>+3</button>
    </div>
  );
}
  • prev => prev + 1 방식은 각 요청이 독립적으로 누적된다.

그렇다면 렌더링도 3번 발생하나요?

  • 렌더링은 여전히 한 번만 발생한다. (batch 처리 덕분)

도파민 중독자를 위한 정리

상태는 즉시 변하지 않음 비동기적으로 batch 처리됨
파생 상태 지양 계산해서 쓰는 게 정석
무거운 초기화는 함수로 전달 지연 초기화 방식으로 한 번만 실행
상태 업데이트는 prev 사용 정확한 누적이 가능함