이번 글에서는 useState가 비동기적으로 동작하는 원리와 올바른 사용 패턴에 대해 다뤄보겠다.


먼저 비동기적이라는 것은 setState를 호출한 직후에 상태가 즉시 반영되지 않음을 의미한다.

const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1);
  console.log(count); // 여전히 setCount 이전 값인 0이 출력됨
};

위 코드에서 handleClick을 호출하면 setCount(count + 1)이 실행되지만, console.log(count)는 여전히 이전 값인 0을 출력한다.


useState가 비동기적으로 동작하는 원리

이러한 비동기성이 발생하는 이유는 렌더링 성능을 최적화하기 위해 여러 상태 업데이트를 모아서(batch) 한 번에 처리하기 때문이다.

const [count, setCount] = useState(0);

// 클릭 시 호출
const handleClick = () => {
  setCount(count + 1);     
  console.log(count);      
};
  1. setCount(count + 1) 실행됨 → 변경 요청이 리액트 내부 큐에 들어감
  2. React는 바로 리렌더링하지 않고 현재 실행 중인 이벤트 핸들러가 끝날 때까지 기다림
  3. 모든 업데이트 요청이 끝난 후 리렌더링 스케줄링됨
  4. 컴포넌트가 리렌더되고 그때 최신 count 값으로 다시 렌더링

즉, setCount는 바로 값을 바꾸는 게 아니라 다음 렌더 사이클을 예약하는 역할을 한다.

더보기

렌더링이 트리거되는 대표적인 상황과 리렌더링 발생 시점은 다음과 같다.

상황 리렌더링 발생 시점
✅ 동기 이벤트 핸들러 내부 (onClick, onChange, 등) 이벤트 핸들러 실행이 끝난 직후
⏱️ setTimeout, Promise.then, fetch 등 비동기 콜백 내부 콜백 실행 중에 즉시 리렌더링 가능
🔄 React 내부 스케줄링 (React 18 이상, Concurrent Mode) React가 판단한 적절한 시점 (우선순위 기반)

최신 상태 값을 활용하는 올바른 패턴

업데이트된 상태 값을 사용하고 싶을 때 권장되는 사용 패턴은 다음과 같다.

1. useEffect 사용

useEffect(() => {
  console.log("count 변경됨:", count); // 업데이트된 값 반영
}, [count]);

2. 함수형 업데이트 사용

상태가 이전 값에 의존할 경우, setCount(prev => prev + 1)처럼 이전 값을 기반으로 업데이트하는 것이 안전하다.

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

이 방법은 현재 상태 값을 정확히 기반으로 업데이트하기 때문에, 배치 처리와 관계없이 예상대로 동작한다.

// 잘못된 예시
setCount(count + 1);
setCount(count + 1); // 두 번 호출해도 count는 1만 증가함

위 코드는 count가 여전히 이전 값을 기준으로 두 번 계산되기 때문에, 결과적으로 +1만 적용된다.

아래와 같이 이전 값을 기반으로 업데이트하는 방식을 사용하면 의도한 대로 동작한다.

// 올바른 예시
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 결과적으로 +2 됨

const [value, setValue] = useState(0);

const handleClick = () => {
  setValue(value + 1);
  console.log("immediate:", value); // 이전 값
  setTimeout(() => {
    console.log("delayed:", value); // 여전히 이전 값
  }, 0);
};

이 경우에도 setTimeout 안의 value는 여전히 클로저에 캡처된 이전 값이므로, 의도한 것처럼 바뀌지 않는다. 리렌더 이후의 값을 알고 싶다면 useEffect를 사용해야 한다.

요약

  • useState는 비동기적이다.

  • 상태 변경은 즉시 반영되지 않고, 렌더링 주기에 맞춰 처리됨

  • useEffect, 함수형 업데이트를 사용하여 변경 이후의 값을 안전하게 다룰 수 있다.


참고 자료