발생한 문제
HookExam.jsx 파일에서는 input 입력 값을 받고 있다. input 값이 변경될 때 마다, customHook인 useForm()을 사용해서 formValue를 업데이트 시켜줄거다. useForm.jsx에는 customHook이 정의되어 있다.
// HookExam.jsx
import useInput from "./../hooks/useInput";
import useForm from "../hooks/useForm";
const HookExam = () => {
const [formValue, onFormChange] = useForm();
return (
<div>
<input name="name" value={formValue.name} onChange={onFormChange} />
<input name="age" value={formValue.age} onChange={onFormChange} />
</div>
);
};
export default HookExam;
// useForm.jsx
import { useState } from "react";
function useForm() {
const [formValue, setFormValue] = useState({});
const onFormChange = (e) => {
const newValue = { ...formValue, [e.target.name]: e.target.value };
setFormValue(newValue);
console.log("formValue", formValue);
};
return [formValue, onFormChange];
}
export default useForm;
의문점은 여기서 발생했다. 분명, 값이 onChange() 될때마다, setFormValue()를 하면 formValue의 값이 새로 업데이트 되어야 하는데, 한템포 늦게 변경되었다.

그치만, newValue는 콘솔에 값이 잘 변경되어서 출력되었다.

구글링을 해보니, 나와 비슷한 문제를 겪는 사람들이 있었다.
const [ number, setNumber ] = useState(0);
const handleClick = (i) {
setNumber(number + 1);
setNumber(number + 2);
setNumber(number + 3);
console.log("numberState: ", number) // 6이 아닌, 3 출력
}
문제의 원인
공식 문서에서 문제의 원인을 찾을 수 있었다.
https://ko.legacy.reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
State and Lifecycle – React
A JavaScript library for building user interfaces
ko.legacy.reactjs.org
https://github.com/facebook/react/issues/11527#issuecomment-360199710
리액트에서 상태를 변경하는 방법은 setState()가 대표적인 사례이다. 하지만, setState()가 될 때마다 리렌더링이 된다면, 매우 비효율적인 성능을 가지게 된다. 이를 해결하기 위해 리액트 배칭(Batching)이 나왔다. 즉, 배칭이란, 리액트의 상태 값을 일정한 주기로 처리하는 작업이다. 그래서 setState()를 했을 때, 값을 즉각적으로 업데이트 하는 것이 아니라 업데이트 하겠다고 예약이 된 후, 이후 일괄적으로 업데이트 되는 것이다. (리액트는 브라우저 이벤트가 끝날 시점에 state를 일괄적으로 업데이트 한다고 한다.)
해결 방법
1. useEffect()의 활용
상태가 변경된 이후 최신 값을 얻고 싶다면, useEffect()를 사용할 수 있다. useEffect()가 최신 값을 출력하는 이유는 렌더링과 상태 관리 구조 덕분인데, useEffect()는 렌더링이 완료된 후에 실행되기 때문이다.
import { useEffect, useState } from "react";
function useForm() {
const [formValue, setFormValue] = useState({});
const onFormChange = (e) => {
const newValue = { ...formValue, [e.target.name]: e.target.value };
setFormValue(newValue);
};
// 상태가 변경된 이후 최신 값 출력
useEffect(() => {
console.log("Updated formValue", formValue);
}, [formValue]);
return [formValue, onFormChange];
}
export default useForm;
2. setState()의 함수형 업데이트 사용
함수형 업데이트 방식으로 사용하면 상태 참조 문제를 방지할 수 있다. setState()는 단순히 새로운 값을 설정할 뿐만 아니라, 함수 형태의 콜백을 받아 이전 상태(prev)를 기반으로 새로운 상태를 계산할 수 있다.
아래의 코드는 이벤트 핸들러에서 상태 업데이트를 중첩 호출하는 경우이다. 여기서 number은 이전 렌더링 값을 참조하기 때문에 세 번의 상태 업데이트가 모두 0을 기반으로 계산된다. 따라서, 최종 값은 3이 출력된다.
const handleClick = () => {
setNumber(number + 1); // number는 이전 렌더링 값(고정된 값)을 참조
setNumber(number + 2);
setNumber(number + 3);
};
하지만 함수형 업데이트를 사용하면, 이전 상태 값(prev)를 기준으로 업데이트를 진행하기 때문에 6을 잘 출력할 수 있다.
const handleClick = () => {
setNumber((prev) => prev + 1); // 이전 상태 + 1
setNumber((prev) => prev + 2); // 이전 상태 + 2
setNumber((prev) => prev + 3); // 이전 상태 + 3
};
결국, setFormValue()도 함수형 업데이트 방식으로 사용하면 상태 참조 문제를 방지할 수 있다.
'React' 카테고리의 다른 글
| useCallback() 적용하기 (0) | 2025.02.05 |
|---|---|
| React query란 무엇일까? (0) | 2025.01.09 |
| 지연 초기화 (0) | 2025.01.06 |
| 최적화를 위한 useMemo(), memo(), useCallback() (2) | 2025.01.02 |
| Props와 이벤트 핸들러 (1) | 2024.12.26 |