practical debugging note
React useEffect 요청이 두 번 보여서 잘못 고칠 뻔한 실수
개발 모드에서 useEffect 요청이 두 번 보이는 상황을 무조건 버그로 보고 우회하려다 StrictMode와 취소 처리를 다시 확인한 기록입니다.
빠른 요약
네트워크 탭만 보고 성급하게 판단했다 검색 필터 컴포넌트를 만들던 중 개발자 도구 Network 탭에 같은 요청이 두 번 찍혔다.
이 글에서 확인할 것
- 네트워크 탭만 보고 성급하게 판단했다
- 먼저 분리해서 본 것
- 수정한 코드 모양
- 억지 플래그보다 나았던 이유
- 다음에 같은 화면을 볼 때
네트워크 탭만 보고 성급하게 판단했다
검색 필터 컴포넌트를 만들던 중 개발자 도구 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 탭의 개수만 보고 결론 내리지 않는다.
마무리는 조금 허무했다. 버그를 고친다기보다, 내가 보고 있는 현상이 어떤 환경의 신호인지 이름 붙이는 일이 먼저였다.