setState 직후 값이 바로 바뀌지 않는 이유
이번 글에서는 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);
};
- setCount(count + 1) 실행됨 → 변경 요청이 리액트 내부 큐에 들어감
- React는 바로 리렌더링하지 않고 현재 실행 중인 이벤트 핸들러가 끝날 때까지 기다림
- 모든 업데이트 요청이 끝난 후 리렌더링 스케줄링됨
- 컴포넌트가 리렌더되고 그때 최신 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, 함수형 업데이트를 사용하여 변경 이후의 값을 안전하게 다룰 수 있다.
