리액트의 useState 훅은 리액트 프로젝트에서 화면에 렌더링되는 변경 가능한 데이터를 출력할 때 사용합니다.
useState 함수는 어떻게 작동하는지 알아봅시다.
const [number, setNumber] = useState<number>(0);
이와 같이 선언하여 사용할 수 있습니다.
console을 사용하여 찍어보겠습니다. 뭐가 나올까요?
console.log(useState<number>(0));

이런 형태의 배열을 리턴하는데, 우리는 useState 함수가 리턴하는 배열을 구조분해 할당해서 사용하는것 뿐입니다.
이제 화면에 보여질 값을 업데이트 하는 방법을 알아보겠습니다.
function Hooks() {
const [number, setNumber] = useState(0);
const onClick = () => {
setNumber(number + 1);
console.log(number, "1증가");
};
return (
<>
<div>{number}</div>
<div>
<button onClick={onClick}>+1</button>
</div>
</>
);
}
버튼을 클릭하여 숫자를 1씩 증가시키는 간단한 화면 구성입니다.
화면에 업데이트 되는 값과 콘솔에 찍히는 값이 같은가?
우리는 이부분을 집중해서 봐야합니다. 원하는 기댓값으로 작동하였나요?

잉? 화면에는 1.. 콘솔에는.. 0 ?
const onClick = () => {
setNumber(number + 1);
console.log(number, "1증가");
};
우리는 분명히 setNumber 함수를 사용하여 값을 업데이트한 후에 콘솔에 찍었습니다. 그런데 원하는 결과가 나오지 않았네요 ㅠㅠ..
❓왜그런걸까요
이 현상으로 미루어 보았을 때 우리는 setNumber 함수가 비동기로 작동한다는것을 알 수 있습니다.

리액트 공식문서에 따르면 useState 의 반환값 두 가지의 역할은 위와 같습니다.

또한, 리액트는 성능을위해 여러 업데이트 함수를 즉시 적용이 아닌 한꺼번에 모아서 한다고 합니다.
⭐️ 이제 답이 나왔죠?
리액트 프로젝트에서 관리하는 상태는 한두개가 아닙니다. 그 수가 많아지고 관리하는 상태가 단순한 값이 아니라면 어떨까요.. 그리고 그 상태가 업데이트 될때마다 동기적으로 화면을 리렌더링 한다면요? 셀 수 없이 많은 화면 리렌더링이 일어날겁니다. 성능저하로 이어지겠죠.
batch란?
리액트에서 배치(batch) 처리란 여러 개의 상태 업데이트를 하나의 렌더링 사이클로 묶어서 처리하는 것을 의미하며 성능을 최적화하고 불필요한 재렌더링을 방지하는 효과가 있습니다.
리액트는 상태 업데이트가 발생할 때마다 즉시 컴포넌트를 재렌더링하지 않고, 여러 업데이트를 모아서 한 번에 처리합니다.
- 이벤트 핸들러
- 상태 업데이트
- 비동기 코드
등을 배치 처리합니다.
const onClick = () => {
setNumber(number + 1);
if (number === 3) {
console.log(number);
alert("숫자가 3입니다!");
}
};
위 예제는 상태가 3일때 alert 을 띄워주는 간단한 예제입니다. 우리는 숫자가 2 -> 3이 될 때 저 메시지가 나오는것입니다.

하지만 아쉽게도 결과는 그렇지 않습니다. 상태가 3일 때 if 문에 걸리지 않기 때문입니다. 그렇다면 원하는 결과를 얻기 위해서는 동기적 처리가 필요합니다. 숫자가 3이 되는 순간에 메시지를 출력하기 위해서요.
useEffect 훅을 사용하여 상태업데이트의 후속처리(동기처리)를 할 수 있습니다.
const onClick = () => {
setNumber(number + 1);
};
useEffect(() => {
if (number === 3) {
console.log(number);
alert("숫자가 3입니다!");
}
}, [number]);
const onClick = () => {
setNumber((prev) => {
if (prev + 1 === 3) {
console.log(number);
alert("숫자가 3입니다!");
}
return prev + 1;
});
};
함수형 업데이트의 첫번째 인자는 이전 상태의 값을 보장합니다. 그럼 그걸 이용해서 컨트롤 할 수 있겠죠? 하지만 가독성이 떨어질거같아 자주 사용하진 않을것으로 생각되네요.
1번의 useEffect 는 라이프 사이클 메소드와 관련이 있지만 특정 상태의 변경을 감지하여 실행하는 후속처리 메소드로도 사용할 수 있습니다.

이렇게 처리하면 setState 함수의 후속처리를 동기적으로 할 수 있습니다.
const onClick = () => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
};
batch 처리는 여러가지 비동기 코드를 병합하여 처리한다고 하였습니다. 위 코드의 기댓값은 3씩 증가하는 것이지만 실상은 1씩밖에 증가하지 않습니다.
왜냐구요? 병합처리 되니까요~
그렇다면 batch처리를 피할 수 있는 방법이 있을까요?
const onClick = () => {
setNumber((prev) => prev + 1);
setNumber((prev) => prev + 1);
setNumber((prev) => prev + 1);
};
이렇게 콜백함수를 전달하여 상태를 업데이트하면 한 번에 3씩 증가합니다. 그치만 이 또한 batch를 피하는것은 아닙니다.
콜백함수형태의 상태 업데이트의 인자 prev 는 이전 상태를 보장합니다. 이 매커니즘 때문에 batch를 피하는것처럼 보여지지만 사실은 피할수는 없습니다.
상태 업데이트를 동기적으로 처리해야 하는 경우는 많지 않지만, 특정 상황에서는 동기적으로 상태를 업데이트하고 싶을 수 있습니다.
상태를 동기적으로 업데이트해야 하는 경우
즉시 반영이 필요한 UI 업데이트:
특정 사용자 인터랙션에 따라 즉시 UI를 업데이트해야 할 때 상태 업데이트를 동기적으로 처리해야 할 필요가 있을 수 있습니다. 예를 들어, 폼 검증 후 사용자 입력에 따라 즉시 피드백을 주는 경우입니다.
상태 변경 후 연속적인 작업:
상태 변경이 완료된 후 그 상태를 기반으로 추가 작업을 수행해야 하는 경우, 예를 들어 상태 변경 후 바로 다른 함수가 그 상태를 참조해야 하는 경우입니다.
동기적인 로직 실행 필요:
상태 변경 후 실행되는 로직이 상태의 최신 값을 필요로 하는 경우입니다.
useState의 비동기 업데이트 방식을 모른다면 고생할 수 있기 때문에 포스팅해봤습니다.