Next.js
React useEffect에서 요청이 두 번 실행되는 현상을 잘못 고칠 뻔했다
개발 모드 StrictMode와 실제 버그를 구분하지 않고 우회 플래그로 감싸려다 올바른 취소 처리의 중요성을 깨달았다.
증상
검색 필터 컴포넌트를 만들던 중 개발자 도구 Network 탭에 같은 요청이 두 번 찍혔다. 처음에는 디바운스가 깨졌다고 생각해서 useRef로 억지 플래그를 만들 뻔했다.
나중에 확인해 보니 개발 모드의 StrictMode 영향도 섞여 있었다. 문제는 "두 번 보인다"와 "운영에서도 중복 저장된다"를 구분하지 않고 바로 코드를 비틀려고 했다는 점이다.
확인 순서
- 개발 모드에서만 두 번인지 확인한다.
- 요청이 조회인지, 저장/결제 같은 부작용인지 구분한다.
- 이전 요청이 늦게 도착했을 때 화면을 덮어쓰는지 본다.
- cleanup 함수가 있는지 확인한다.
조회 요청이라도 이전 응답이 나중에 도착하면 UI가 뒤집힐 수 있다.
수정 방법
처음 코드는 입력값이 바뀔 때마다 fetch만 호출했다.
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then(setItems);
}, [query]);
수정 후에는 취소 기준을 명확히 했다.
useEffect(() => {
const controller = new AbortController();
async function load() {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
const data = await res.json();
setItems(data.items);
}
load().catch((error) => {
if (error.name !== 'AbortError') {
console.error(error);
}
});
return () => controller.abort();
}, [query]);
억지 플래그를 피한 이유
didRun.current 같은 플래그로 두 번째 실행을 막으면 당장은 조용해 보인다. 하지만 의존성이 바뀌었을 때 다시 실행되어야 하는 정상 흐름까지 막을 수 있다. 나는 그 방식 대신 요청 취소와 멱등성을 먼저 확인하기로 했다.
다음 번에 마주치면
- 개발 모드와 운영 빌드를 구분해서 재현한다.
- 저장 API는 서버에서 중복 처리 방어를 둔다.
- 조회 API는 늦게 온 응답이 최신 상태를 덮지 않게 한다.
- Network 탭의 개수만 보고 결론 내리지 않는다.
결론은 조금 허무했다. 버그를 고친다기보다, 내가 보고 있는 현상이 어떤 환경의 신호인지 이름 붙이는 일이 먼저였다.