Next.js
React 상태가 빌드에서만 깨질 때
개발 환경에선 잘 돌아가는데 빌드 후에 상태가 꼬이는 경우가 있다. 보통 의존성 배열 실수나 클로저 문제다.
개발 중엔 정상인데 프로덕션 빌드에서만 상태가 깨진다. 이런 일이 생기면 빌드 최적화 때문에 발생한 의존성 관련 버그일 확률이 높다.
개발 모드에선 엄격한 검사가 많지만, 프로덕션 빌드는 코드를 최소화하고 변수를 재사용한다. 그래서 클로저나 참조 관계 문제가 숨어있다가 드러난다.
첫 번째: useEffect 의존성 배열을 확인한다
// 나쁜 예: 의존성이 빠져있음
function Counter({ initialValue }) {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(initialValue); // initialValue가 의존성에 없음!
}, []); // <- 이것이 문제
return <div>{count}</div>;
}
// 좋은 예: 의존성을 명시적으로
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
빌드 후엔 lint 경고를 무시했던 의존성 배열 버그가 더 자주 터진다. 개발 중엔 hot reload 때문에 의도치 않게 동작했지만, 빌드에선 최초 한 번만 실행된다.
두 번째: 상태 업데이트 함수 안의 클로저
// 문제: handleClick이 count의 오래된 값을 캡처함
function Timer() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log(count); // count는 고정된 값
setCount(count + 1);
};
return <button onClick={handleClick}>{count}</button>;
}
// 해결: 함수형 업데이트
const handleClick = () => {
setCount(prev => prev + 1);
};
특히 이벤트 리스너나 타이머 콜백에서 상태를 읽고 업데이트할 때 이런 문제가 생긴다.
세 번째: 불변성 위반
// 문제: 객체를 직접 수정함
function UserProfile({ user }) {
const handleUpdate = () => {
user.name = 'New Name'; // 직접 수정!
setUser(user); // 참조가 같아서 업데이트 감지 안 함
};
return <button onClick={handleUpdate}>Update</button>;
}
// 해결: 새 객체 만들기
const handleUpdate = () => {
setUser({ ...user, name: 'New Name' });
};
개발 환경에선 때때로 강제 re-render 때문에 작동했지만, 프로덕션 빌드에선 React가 변경을 감지하지 못한다.
확인하는 순서
npm run build후 실제로 동작하는지 로컬에서 먼저 테스트한다- DevTools에서 console 에러를 본다
- 상태가 예상과 다르게 나타나는 부분의 useEffect를 모두 확인한다
- eslint-plugin-react-hooks 경고를 다시 읽어본다
- 필요하면
console.log를 넣어서 상태 변화를 추적한다
React 상태 버그는 개발/프로덕션 환경 차이 때문에 생긴다. 빌드 후 테스트하는 습관이 중요하다.