
React 애플리케이션에서 사용자 인터페이스는 상태(state)에 따라 변화한다. 이 상태를 업데이트하는 것은 React 개발의 가장 기본적인 부분 중 하나이다. React에서 상태를 업데이트하는 가장 기본적인 방법은 useState 훅과 함께 제공되는 상태 설정 함수(setter function)를 사용하는 것이다.
상태 변수를 설정하는 것은 즉시 변수를 바꾸는 것이 아닙니다. 대신, 새로운 렌더링을 큐에 추가하는 것입니다. React는 이 렌더링 요청을 받아 다음 렌더링 주기에서 상태를 업데이트하고 UI를 다시 그리게 됩니다.
하지만 때때로 다음 렌더링이 발생하기 전에 하나의 상태 값에 대해 여러 작업을 수행하고 싶을 때가 있다. 특히 동일한 상태 변수에 대해 동일한 이벤트 핸들러 내에서 연속적으로 여러 번 업데이트를 시도할 때, 예상과 다르게 동작할 수 있다.
예를 들어, 카운터 컴포넌트에서 "+3" 버튼을 클릭했을 때 카운터 값을 3 증가시키고 싶다고 상상해보자. 가장 직관적으로 생각할 수 있는 방법은 setNumber(number + 1) 코드를 이벤트 핸들러 내에서 세 번 호출하는 것이다.
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
카운터의 초기 값이 0이라면, 위 코드는 setNumber(0 + 1), setNumber(0 + 1), setNumber(0 + 1)로 실행될 것이고, 여러분은 당연히 카운터가 3이 될 것이라 예상할 것이다. 하지만 실제로는 카운터가 단 1만 증가하는 것을 보게 될 수 있다.
왜 이런 현상이 발생할까? React는 상태 업데이트를 처리하는 방식을 통해 성능을 최적화하고 예상치 못한 불일치를 방지한다. 이러한 동작은 배치(Batching)라고 불리는 React의 핵심 메커니즘과 관련이 깊다.
React에서 useState 등으로 상태를 설정하는 것은 값을 즉시 바꾸는 것이 아니라, 다음 렌더링을 새로 요청하는 것이다. 하지만 React는 여러 상태 업데이트 요청을 똑똑하게 묶어서 처리합니다. 이것이 배치(Batching)이다. React는 이벤트 핸들러 안의 모든 코드가 다 실행될 때까지 기다렸다가, 발생한 모든 상태 업데이트 요청을 하나로 모아 한 번에 처리한다.
배치를 하는 주된 이유:
1. 성능 향상: 불필요한 리렌더링을 여러 번 하지 않고 한 번에 처리하여 앱 속도를 빠르게 한다.
2. 일관성 유지: 일부 상태만 업데이트된 어색하거나 '덜 끝난' 화면이 노출되는 것을 막아준다.
이는 마치 식당 웨이터가 주문받는 방식과 비슷하다. 웨이터는 손님이 첫 메뉴를 말하자마자 주방으로 달려가는 대신, 모든 주문을 다 받고 나서 한 번에 주방에 전달한다. React도 마찬가지로, 이벤트 핸들러 코드가 다 끝날 때까지 기다렸다가 상태 업데이트들을 모아서 처리한다. 따라서 setNumber() 호출이 여러 번 있더라도, 실제 화면 다시 그리기(리렌더링)는 이벤트 핸들러 완료 후에 딱 한 번만 일어난다.

여기서 중요한 것은 각 렌더링 시점의 상태 값은 고정된 '스냅샷(Snapshot)'이라는 개념이다. 이벤트 핸들러가 실행될 때, 그 안에서 number와 같은 상태 변수가 참조하는 값은 이벤트 발생 시점의 변하지 않는 그 값이다. 이것이 setNumber(number + 1)를 세 번 호출해도, 이벤트 핸들러 안의 number는 계속 초기 값(예: 0)을 참조하기 때문에 값이 1밖에 늘지 않는 이유이다.
요약:
- 상태 설정은 다음 렌더링을 요청하는 것이다.
- React는 이벤트 핸들러 완료 후 상태 업데이트를 배치(Batching)하여 한 번에 처리한다. 이는 성능과 일관성을 위한 것이다.
- 각 렌더링의 상태 값은 고정된 스냅샷이다.
하지만 배치가 항상 일어나는 것은 아니다. React는 사용자의 의도된 여러 독립적인 이벤트(예: 버튼을 여러 번 클릭하는 것)에 대해서는 배치를 하지 않는다. 각 클릭 이벤트는 별도로 처리된다. React는 안전하다고 판단될 때만 배치를 수행한다.
동일한 이벤트 핸들러 내에서 동일 상태를 다음 렌더링 전에 여러 번 업데이트하고자 한다면, 단순 값(예: number + 1) 대신 이전 상태 값을 기반으로 다음 상태를 계산하는 '함수'를 전달해야 한다. 이것을 업데이터 함수(updater function)라고 부르며 (예: setNumber(n => n + 1)), React에게 '현재 상태에 대해 이러이러한 작업을 해 달라'고 요청하는 방식이다.
업데이터 함수를 사용하면:
1. React는 전달받은 함수를 이벤트 핸들러 완료 후 처리될 대기열(queue)에 추가한다.
2.다음 렌더링 시 React는 이 대기열을 순서대로 실행하며 최종 업데이트된 상태 값을 계산한다.
예를 들어 setNumber(n => n + 1)를 세 번 호출하면, 대기열에 세 개의 함수가 추가되고, 다음 렌더링 때 React는 초기 상태 값(예: 0)부터 시작해 각 함수의 결과를 다음 함수의 입력으로 사용하여 순차적으로 계산한다 (0 -> 1 -> 2 -> 최종 3).
요약:
- 독립적인 여러 이벤트는 배치되지 않는다.
- 동일 이벤트 내에서 같은 상태를 여러 번 업데이트하려면 업데이터 함수
setNumber(prev => prev + ...)를 사용해야 한다. 이 함수들은 대기열에 추가되어 다음 렌더링 시 순차적으로 처리된다.
React에서 useState의 상태 설정자(setter)에 업데이터 함수를 전달할 때, 이 함수가 이전 상태 값을 인자로 받게 된다. 이 인자의 이름을 정하는 데에는 몇 가지 일반적인 네이밍 컨벤션이 있으며, 이는 코드의 가독성을 높이는 데 기여한다.
상태 변수의 첫 글자(들) 사용: 해당 상태 변수의 이름을 줄여서 첫 글자나 두 글자를 인자로 사용하는 것이 흔하다.
전체 상태 변수 이름 사용 (선택 사항): 코드를 더 명시적으로 작성하고자 할 경우, 상태 변수 전체 이름을 인자로 사용할 수도 있다.
prev 접두사 사용 (선택 사항): 이전 상태 값임을 명확히 나타내기 위해 prev 접두사를 사용하는 것도 일반적인 컨벤션이다.