setState는 비동기일까

Published2025.03.11
Read Time8 min read

setState는 비동기일까?

🔹setState가 왜 바로 반영이 안되지?

React에서 setState를 사용하다 보면, 처음에 많은 프론트엔드 개발자들이 한 가지 의문을 가지게 된다.

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

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

console.log(count)에서는 업데이트된 값인 1이 출력되어야 할거같지만, 그게 아닌아니라 0이 출력됩니다.

코드만 봤을 때는 업데이트된 값인 1이 출력되는게 맞는 거 같은데 왜 이전 값인 0이 출력되는걸까? async await를 쓰지 않지만 setState가 비동기라 그런걸까?

결론부터 말하자면 setState는 비동기 처럼 동작하지만, 실제로는 동기함수이다. 이에 대해 자세히 살펴보자.

🔹 setState의 동작방식

setState 자체는 동기적으로 실행되지만, setState로 인한 상태업데이트와 렌더링은 비동기적으로 이루어진다.

왜 setState가 동기적인지는 우선 이따가 살펴보고, 우선 setState의 동작방식에 대해 알아보자.

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

const handleClick = () => {
  setCount(count + 1);
  console.log(count); // 출력값은 1이다.
};

아까 전 살펴봤던 코드이다. 왜 업데이트 된 값이 바로 반영이 안되었던 걸까?

이유는 React는 최적화를 위해 상태 변경을 비동기적으로 처리하고 있기 때문입니다.

배치 업데이트 (Batching)

React는 여러 개의 setState 호출을 하나로 비동기적으로 묶어서 처리하고, 이것을 배치(batch) 업데이트를 수행한다고합니다.
즉, 같은 이벤트 핸들러에서 여러 개의 setState가 호출되더라도 한 번의 렌더링에서 처리됩니다.

const handleClick = () => {
  setCount(10);
  setCount(count + 1);
  setCount(30);
  console.log("현재 count 값:", count);
};

즉 위의 3개의 setState는 한번에 함수 내 에서 하나씩 순서대로 실행되는 것이 아닌, 비동기적으로 한번에 처리가 되는 것 이다.

그렇다면 다음 코드를 보자.

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  console.log("현재 count 값:", count);
};

다음과 같은 경우 현재 count 값이 0으로 출력되는 것은 이제 알 것이다.

현재 count 값: 0  // 이전 값이 출력

그렇다면 최종적으로 count 값은 몇이 될까? `

setCount(count + 1)가 3번 호출되었지만, 최종적으로 count 값은 1만 증가한다.

이유는 역시 React는 동일한 이벤트 핸들러 내에서 여러 개의 setState배치 처리하여 하나로 합쳐버리기 때문이다. 즉,

setCount(count + 1);  // count + 1 → 0 + 1 → setCount(1)
setCount(count + 1);  // count + 1 → 0 + 1 → setCount(1)
setCount(count + 1);  // count + 1 → 0 + 1 → setCount(1)

  • setState는 바로 실행되지만, 이벤트 핸들러 내부에서는 count 값이 0으로 유지됨
  • 따라서 모든 setCount(count + 1)은 결국 setCount(1)과 동일

만일 의도한 바대로 count를 1씩증가시켜 3으로 만들고 싶다면,

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

이런 식으로 코드를 짜면, 배치 처리와 상관없이 이전 값을 기반으로 상태를 변경한다.

이렇게 setState는 React내에서 비동기적으로 상태 업데이트를 진행하는데, 왜 그럼에도 동기함수라고 하는걸까?

🔹 setState가 동기함수인 이유

setState의 상태 업데이트는 React내에서 배치처리를 통해 비동기적으로 이루어지지만, setState 자체는 호출 즉시 실행되므로 동기 함수이다.

즉, 예를들어서,

const handleClick = () => {
  console.log("Before setState:", count);
  setCount(count + 1);
  console.log("After setState:", count);
};

위의 setCount(count + 1); 함수 자체는 실행될 때, 실행되는 즉시 함수가 종료가 된다. setCount가 진짜 비동기 함수였다면, 비동기 작업(Promise 등)을 반환해야 하지만 그런 동작을 하지 않는 것이다.

다만 setCount가 실행되어 상태 변경을 요청하면, React가 이 요청을 현재 수행중인 이벤트가 핸들러가 끝날 때까지 기다린 후 배치처리를 수행하는 것이다.

setState가 비동기 함수 였다면,

const handleClick = async () => {
 console.log("Before setState:", count);
 await setCount(count + 1);  //  실제로는 이렇게 사용 불가능
 console.log("After setState:", count);
};

이런식으로 사용할 수 있어야 하지만, setState는 Promise를 반환하지 않으므로 await을 사용할 수 없다.

즉 setState는 React의 내부적인 상태 업데이트와 렌더링 방식으로 인해 비동기적으로 실행되는 것처럼 보이지만 엄밀히 말하면 동기 함수이다.

결론

setState를 통해 React가 상태를 업데이트하는 과정을 정리하면 다음과 같다.

  1. setState 호출 → 상태 변경 요청 (동기적으로 동작)
  2. React가 현재 실행 중인 이벤트 핸들러가 끝날 때까지 기다림
  3. 배치 처리를 수행하여 여러 개의 setState를 한꺼번에 처리(비동기적 최적화)
  4. 상태 업데이트 후 리렌더링을 트리거

setState는 동기적으로 실행되지만, React의 상태 변경과 렌더링 과정이 비동기적으로 최적화되어 실행된다.