← 전체 글로 돌아가기

Next.js

React 탭 전환에서 useEffect가 예전 데이터를 덮어쓴 실수 정리

빠른 탭 전환 중 이전 요청이 늦게 도착해 화면을 덮어쓴 문제를 고친 기록.

빠르게 클릭할 때만 틀렸다

통계 화면의 일·주·월 탭은 천천히 누르면 정상이었다. 하지만 일 탭 뒤 바로 월 탭을 누르면 늦게 온 일 요청이 월 화면을 덮었다. 의존성 배열에 range를 넣어도 이미 시작한 비동기 요청은 멈추지 않는다는 점을 놓쳤다.

cleanup에서 이전 요청 취소

effect 실행마다 컨트롤러를 새로 만들고 cleanup에서 취소했다. ref 하나를 공유하면 새 요청까지 취소할 수 있어 이 위치가 중요했다.

useEffect(() => {
  const controller = new AbortController();
  async function load() {
    setLoading(true);
    try {
      const res = await fetch(`/api/stats?range=${range}`, { signal: controller.signal });
      if (!res.ok) throw new Error('failed');
      setStats(await res.json());
    } catch (e) {
      if ((e as Error).name !== 'AbortError') setError('다시 시도해 주세요.');
    } finally {
      if (!controller.signal.aborted) setLoading(false);
    }
  }
  load();
  return () => controller.abort();
}, [range]);

처음에는 finally에서 무조건 로딩을 껐다. 취소된 이전 요청이 새 요청의 로딩 표시를 꺼 버려서 aborted 조건을 추가했다.

재현과 마무리

개발자 도구에서 Slow 3G를 고르고 탭을 연속 클릭했다. 정상 네트워크에서는 숨은 문제가 바로 보였다.

  • 응답 도착 순서는 보장되지 않는다.
  • AbortError는 사용자 오류로 표시하지 않는다.
  • 느린 네트워크에서도 빠르게 전환해 본다.

탭이 단순해 보여도 요청이 겹치면 경쟁 상태가 된다.