“이전 상태 값을 기반으로 업데이트할 때, 우리는 setCount에 새로운 값을 직접 넘기는 대신, 함수를 넘겨줄 수 있습니다.”
댄은 마야의 ItemCounter 컴포넌트 코드를 수정하기 시작했다. 그의 손가락이 handleAddTwo 함수를 향했다.
const handleAddTwo = () => {
setCount(c => c + 1);
setCount(c => c + 1);
};
“setCount에 이렇게 함수를 전달하면, 리액트는 이 함수를 나중에 실행할 때, 가장 최신의 상태 값을 인자(여기서는 c라고 이름 붙였습니다)로 넘겨주겠다고 약속합니다. 우리는 이 함수를 ‘업데이터 함수(updater function)’라고 부르죠.”
마야는 숨을 죽이고 그의 설명을 들었다.
댄이 상황을 단계별로 풀어 설명했다.
“버튼을 클릭하면, 리액트는 두 개의 업데이터 함수를 실행 대기열(queue)에 넣습니다. [c => c + 1, c => c + 1] 이렇게요.”
“그리고 다음 렌더링을 준비하면서, 리액트는 이 대기열을 순서대로 처리합니다.”
- “첫 번째 함수를 꺼내 실행합니다. 이때 리액트는 현재 상태 값인 0을 인자
c로 넘겨줍니다.c + 1은 1을 반환하고, 상태는 1로 업데이트될 예정입니다.” - “두 번째 함수를 꺼내 실행합니다. 이때 리액트는 방금 계산된, 가장 최신의 상태 값인 1을 인자
c로 넘겨줍니다.c + 1은 2를 반환하고, 상태는 2로 최종 업데이트될 예정입니다.”
모든 업데이트 계산이 끝나면, 리액트는 최종 결과인 2를 가지고 컴포넌트를 리렌더링한다.
마야는 코드를 실행했다.
버튼을 누르자, 화면의 숫자가 정확히 ‘2’로 바뀌었다.
그녀의 얼굴에 환한 미소가 피어올랐다.
“이게 바로 함수형 업데이트(Functional Updates)입니다.”
댄이 결론지었다.
“이 방식을 사용하면, 렌더링 시점의 count 값(스냅샷)에 의존하지 않고, 리액트가 관리하는 가장 최신 상태를 기반으로 안전하게 다음 상태를 계산할 수 있습니다. 상태 업데이트가 여러 번 연속해서 일어나는 복잡한 경우에 아주 유용하죠.”
이 작은 깨달음은 마야에게, 그리고 훅을 처음 접하는 모든 개발자에게 중요한 멘탈 모델의 전환을 의미했다.
set 함수는 단순히 값을 설정하는 명령이 아니었다. 그것은 리액트와 소통하는 정교한 인터페이스였다.
setCount(newValue): “상태를 이 값으로 만들어줘.”
setCount(updaterFn): “현재 상태를 가지고 이 계산을 한 뒤, 그 결과로 상태를 만들어줘.”
이것은 훅을 ‘정확하게’ 사용하기 위한 첫 번째 관문이었다.
비동기적으로 동작하는 상태 업데이트의 본질을 이해하고, 그에 맞는 올바른 도구(함수형 업데이트)를 사용하는 것.
댄은 마야에게 격려의 말을 건네고 자리로 돌아왔다.
그는 useState의 이 미묘한 동작이 앞으로 수많은 개발자들을 혼란에 빠뜨릴 것임을 알고 있었다. 하지만 동시에, 이 함정을 극복하는 과정 자체가 개발자들이 리액트의 비동기 렌더링 철학을 더 깊이 이해하게 만드는 중요한 학습 경험이 될 것이라고 믿었다.
훅의 세계는 겉보기에는 단순했지만, 그 안에는 이처럼 깊이 이해하고 숙달해야 할 중요한 개념들이 숨어 있었다. useState의 함정을 넘어선 개발자들 앞에는, 이제 useEffect라는 더 거대하고 복잡한 산이 기다리고 있었다.


