React를 공부하다 보면 자주 듣게 되는 말이 있다.
바로 "setState는 비동기다!"
그런데 정말 그럴까?
이 질문에 제대로 답하기 위해서는 setState의 동작 원리를 정확히 이해할 필요가 있다.
아래에서 하나하나 살펴보자.
setState란?setState는 React에서 컴포넌트의 상태(state)를 변경할 수 있도록 도와주는 함수다.
상태가 변경되면 해당 컴포넌트는 자동으로 리렌더링되며, UI가 새 상태에 맞게 갱신된다.
setState는 동기일까? 비동기일까?console.log("1");
setState(100);
console.log("2");
위 코드를 실행해보면 "1"이 출력된 직후, 지연 없이 "2"가 출력된다.
setState가 호출되었음에도 다음 줄의 코드 실행이 멈추지 않기 때문에,
setState 함수 자체는 동기적으로 실행되는 함수임을 알 수 있다.
setState(count + 1);
console.log(count); // 이전 값이 출력됨
setState가 상태를 바로 바꾸는 것이 아니라,1. setState 호출
2. 변경 요청이 큐에 저장됨
3. React가 한꺼번에 처리(batch update)
4. 컴포넌트를 리렌더링
setState는 실행은 동기, 하지만 효과는 비동기적으로 나타남React는 성능 최적화를 위해 여러 개의 상태 변경을 한 번의 렌더링으로 처리한다.
이를 Batching(배치 처리) 이라고 하며, 렌더링 비용을 최소화할 수 있는 매우 중요한 최적화 전략이다.
setState(count + 1);
setState(count + 1); // 결과적으로 +1만 반영됨!
setState를 연달아 호출해도 둘 다 같은 이전 count 값을 참조하게 된다.React는 이런 문제를 막기 위해 함수형 업데이트(functional update) 방식을 제공한다.
setCount(prev => prev + 1);
setCount(prev => prev + 1);
prev는 함수 인자로 들어오는 가장 최신 상태값이다.prev는 키워드인가요?아니요!
prev는 단순한 매개변수(parameter) 이름일 뿐이다.
다음 코드도 완전히 동일하게 동작한다:
setCount(before => before + 1);
setCount(anyName => anyName + 1);
prev => prev + 1)는 setState가 실행될 때까지 "저장된 함수" 상태로 남아 있음setCount(count + 1))은 타이밍 문제로 오류가 생길 수 있다.이전에는 이벤트 핸들러 내부에서만 setState가 배치 처리됐지만,
React 18부터는 Promise, setTimeout, async/await 등 모든 비동기 함수 안에서도 자동으로 batching이 적용된다.
useEffect(() => {
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
}, 1000);
}, []);
| 항목 | React 17 | React 18 |
|---|---|---|
| 결과 값 | count = 1 | count = 2 |
| Batching 적용 여부 | ❌ (setTimeout 내에서는 안 됨) | ✅ (모든 비동기 컨텍스트에서 적용) |
| 리렌더링 횟수 | 2번 | 1번 |
| 상태 누적 | ❌ (덮어쓰기 발생) | ✅ (정확하게 누적됨) |
setState는 동기 함수지만, 상태는 비동기적으로 반영된다."
setState는 자바스크립트 상으로는 동기 함수이지만, 상태 반영은 비동기적으로 이루어집니다. 이는 React가 여러 상태 업데이트를 한 번에 처리하는 batching 구조 때문이며, 이로 인해 성능 최적화가 가능합니다. 상태값에 의존할 때는 클로저 기반의 함수형 업데이트를 통해 정확한 상태 변경을 보장할 수 있습니다."