본문 바로가기
C.W.K.
Stream
Lesson 05 of 07 · published

fetch 취소와 오래된 응답 막기

~11 min · fetch, abortcontroller, cancellation

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
요청을 시작할 수 있다면 취소할 수도 있어야 해. AbortController로 오래된 응답이 새 화면을 덮지 않게 만들어 보자.

요청마다 controller를 만들어

AbortController의 signal을 fetch에 넘기면 abort()로 진행 중인 요청을 멈출 수 있어. 컴포넌트가 사라지거나 query가 바뀔 때 cleanup에서 취소해.

오래된 응답이 뒤늦게 도착하는 문제를 막아

사용자가 A를 요청한 뒤 곧바로 B로 바꾸면 A 응답이 더 늦게 도착할 수 있어. 이전 요청을 취소하지 않으면 새 화면을 오래된 A가 덮어쓸 수 있지. Effect가 다시 실행되기 전에 이전 controller를 abort하면 마지막 요청만 상태에 반영돼.

AbortError는 예상된 종료야

취소된 fetch는 오류로 끝나지만 사용자에게 보여 줄 장애는 아니야. Catch에서 AbortError인지 확인해 조용히 끝내고, 네트워크 실패와 서버 오류만 실제 오류 상태로 다뤄.

사용자가 직접 멈출 수도 있어

스트리밍 채팅이나 큰 다운로드처럼 오래 걸리는 작업에는 Stop 버튼을 연결해. Controller를 ref에 보관하면 이벤트 핸들러에서 현재 요청을 취소할 수 있어.

요청의 수명은 화면의 수명과 맞춰. 결과를 사용할 컴포넌트나 query가 사라졌다면 요청도 남겨 두지 마.

취소 동사는 controller.abort()

Controller를 만들고 controller.signal을 fetch 옵션에 넘긴 뒤, 새 요청이 이전 요청을 대신하거나 컴포넌트가 사라질 때 controller.abort()를 호출해. Fetch는 AbortError로 끝나고 오래된 응답은 state에 들어오지 않아.

사용자 취소와 timeout을 함께 묶을 수 있어

AbortSignal.timeout(5000)은 5초 뒤 요청을 취소해. 사용자의 controller와 timeout을 모두 적용하려면 AbortSignal.any([controller.signal, AbortSignal.timeout(5000)])를 사용해. 명시적 취소는 AbortError, timeout은 TimeoutError로 구분할 수 있어.

Code

useEffect에서 fetch 수명과 AbortController 연결하기·tsx
import { useEffect, useState } from "react";

function useSearchResults(query: string) {
  const [results, setResults] = useState<string[]>([]);

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    const ctrl = new AbortController();
    fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: ctrl.signal })
      .then((r) => r.json())
      .then(setResults)
      .catch((e) => {
        // query가 바뀌면서 진행 중이던 요청이 취소되면 AbortError가 발생해.
        if (e.name !== "AbortError") throw e;
      });
    return () => ctrl.abort();
  }, [query]);

  return results;
}
Ref에 controller를 보관하는 이벤트 핸들러·tsx
import { useRef } from "react";

function useSave() {
  // 최신 controller를 보관하고 이전 요청은 취소해.
  const ctrlRef = useRef<AbortController | null>(null);

  const save = async (payload: object) => {
    // 진행 중인 이전 저장 요청을 취소해.
    ctrlRef.current?.abort();
    const ctrl = new AbortController();
    ctrlRef.current = ctrl;
    try {
      const r = await fetch("/api/save", {
        method: "POST",
        body: JSON.stringify(payload),
        signal: ctrl.signal,
      });
      return await r.json();
    } catch (e) {
      if ((e as Error).name === "AbortError") return null; // 대체됨
      throw e;
    }
  };

  return save;
}
AbortSignal.timeout으로 강제 취소 시점 정하기·ts
async function fetchWithTimeout<T>(url: string, ms: number): Promise<T> {
  const r = await fetch(url, { signal: AbortSignal.timeout(ms) });
  return r.json();
}

// AbortSignal.any — 여러 signal을 결합해 (사용자 취소 + timeout).
async function fetchUserCancellable<T>(
  url: string,
  userSignal: AbortSignal,
  ms: number
): Promise<T> {
  const r = await fetch(url, {
    signal: AbortSignal.any([userSignal, AbortSignal.timeout(ms)]),
  });
  return r.json();
}

External links

Exercise

useDebouncedValue를 거친 검색어로 결과를 요청하는 화면을 만들고, effect마다 새 AbortController를 연결해. 빠르게 세 검색어를 입력했을 때 이전 응답이 마지막 결과를 덮지 않는지 확인하고 Stop 버튼으로 현재 요청만 취소해. AbortSignal.any와 3초 timeout도 더해 느린 endpoint에서 timeout 오류가 구분되어 보이는지 시험해.
Hint
Cleanup에서는 이전 controller를 abort하고 catch에서는 AbortError와 실제 네트워크 오류를 나눠. 요청 수 감소, 오래된 결과 방지, 수동 취소, timeout 네 경로를 각각 확인해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.