개발 환경에서만 API가 두 번 호출돼서 StrictMode를 끄려다 알게 된 것
문제 발생
개발 서버에서만 목록 API가 두 번 호출됐습니다. 운영 빌드에서는 한 번이었습니다. StrictMode를 지우면 사라지길래 그렇게 넘어갈 뻔했습니다.
useEffect(() => {
fetch("/api/items").then(setItems); // 개발에서 두 번 실행된다
}, []);원인 분석
StrictMode는 개발 환경에서만 동작하는 검사 장치이고, 다음을 한 번 더 실행합니다.
- 컴포넌트 함수(순수하지 않은 렌더를 찾기 위해)
- effect의 설정 + 정리 사이클(정리를 빠뜨린 곳을 찾기 위해)
- ref 콜백의 설정 + 정리 사이클
- 그리고 deprecated API 사용을 검사합니다
공식 문서가 명시합니다 — 이 검사들은 전부 개발 전용이며 운영 빌드에는 영향을 주지 않습니다.
핵심은 두 번 실행이 버그를 만드는 것이 아니라 드러낸다는 점입니다. 순수한 함수는 여러 번 호출해도 결과가 같으므로, 결과가 달라진다면 그건 원래 순수하지 않았던 것입니다. effect도 마찬가지로, 설정과 정리가 짝을 이루면 두 번 실행돼도 최종 상태가 같아야 합니다. 위 코드는 정리가 없으므로 요청 두 개가 각각 살아남아 경쟁합니다.
실제로 이건 개발 전용 문제가 아닙니다. 사용자가 화면을 빠르게 오가면 운영에서도 같은 일이 벌어집니다 — StrictMode는 그 상황을 매번 재현해 줄 뿐입니다.
해결 방안
- effect에 정리 함수를 붙입니다. 요청을 실제로 끊으면 두 번 실행돼도 결과가 하나로 정리됩니다.
useEffect(() => {
const controller = new AbortController();
fetch("/api/items", { signal: controller.signal })
.then(setItems)
.catch((error) => { if (error.name !== "AbortError") throw error; });
return () => controller.abort();
}, []);- 구독·타이머·이벤트도 같은 규칙입니다. 설정한 것은 정리에서 정확히 되돌립니다. 되돌릴 수 없다면 그 자체가 설계 문제입니다.
- 렌더 중에 부수 효과를 두지 않습니다. 컴포넌트 본문에서 전역 변수를 바꾸거나 배열을 정렬하면 두 번 실행에서 결과가 달라집니다 — 이 저장소에서도 배열
sort()가 원본을 바꾸는 문제를 같은 맥락에서 다뤘습니다. useState·useMemo에 넘기는 함수도 순수해야 합니다. 이 함수들도 두 번 호출됩니다.- StrictMode를 끄지 않습니다. 끄면 문제가 사라지는 게 아니라 보이지 않게 됩니다. 두 번 호출이 신경 쓰인다면 정리 함수가 없다는 신호로 읽는 편이 낫습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.