Next.js
React 상태 문제를 빠르게 진단하는 방법
React 컴포넌트에서 상태가 업데이트되지 않거나 의도치 않게 변할 때, 원인을 찾는 체계적인 접근법을 정리했다.
React 상태 문제는 눈에 띄는 에러 없이도 발생한다. 화면에는 보이는데 상태는 업데이트되지 않거나, 반대로 모든 상태가 초기화되는 식이다.
첫 번째: 상태 변경이 실제로 일어나는가
React DevTools의 "Highlight updates when components render" 옵션을 켜고, 상태를 변경해본다. 만약 컴포넌트가 다시 렌더링되지 않으면, setState 콜 자체가 제대로 안 된 것이다.
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('Before:', count);
setCount(count + 1);
console.log('After:', count); // 여기는 여전히 이전 값이다
};
State closure를 의심해보자. 콜백이나 이벤트 핸들러 안에서 상태를 참조할 때, 클로저에 갇혀 있을 수 있다.
두 번째: 의존성 배열 확인
useEffect에서 의존성을 잘못 설정했을 가능성이 크다.
useEffect(() => {
// 상태나 props가 바뀔 때마다 실행
fetchData();
}, [userId, API_KEY]); // 의존성을 명시적으로
의존성 배열이 없으면 매번 렌더링마다 실행되고, 빈 배열이면 마운트할 때만 실행된다. 특정 값이 바뀔 때만 실행하려면 그 값을 배열에 넣어야 한다.
세 번째: 상태 불변성
객체나 배열 상태를 직접 수정하면, React는 상태가 바뀐 줄 모른다.
// 잘못된 방식
const [items, setItems] = useState([]);
items.push(newItem); // 직접 수정
setItems(items); // 이건 변경 감지 안 됨
// 올바른 방식
setItems([...items, newItem]);
ES2023의 structuredClone이나 Immer 같은 라이브러리를 쓰면 불변성 관리가 더 쉬워진다.
네 번째: 마운트/언마운트 순서
부모가 자식 컴포넌트를 조건부로 렌더링할 때, 자식의 상태가 초기화될 수 있다.
function Parent() {
const [show, setShow] = useState(true);
return (
<>
<button onClick={() => setShow(!show)}>Toggle</button>
{show && <Child />} {/* 끝나면 state가 버려진다 */}
</>
);
}
Child 컴포넌트가 DOM에서 제거되면 상태도 초기화된다. 상태를 유지해야 한다면 Parent에서 관리하거나, key를 명시해서 DOM 재사용을 제어한다.
다섯 번째: 프로덕션 빌드 확인
로컬에서는 정상인데 배포 후에만 문제가 나는 경우도 있다.
npm run build
npm run start
StrictMode 때문에 로컬 개발에서는 effect가 두 번 실행되는데, 프로덕션 빌드에서는 한 번만 된다. 이 차이로 인해 초기화 로직이 제대로 안 될 수 있다.
마지막: 상태 업데이트 로직 추적
상태가 언제 어디서 바뀌는지 알 수 없으면, 모든 setState 호출 전에 로그를 남기거나 debugger를 세운다.
const [state, setState] = useState(initialValue);
const updateState = (newValue) => {
console.log('State update:', { from: state, to: newValue, stack: new Error().stack });
setState(newValue);
};
React DevTools의 Profiler 탭에서도 어떤 컴포넌트가 언제 렌더링되는지 정확히 볼 수 있다. 상태 변경과 렌더링 시점을 비교하면, 대부분의 문제는 금방 찾을 수 있다.