Fetch 취소 · AbortController 완전 이해

AbortController·AbortSignal로 fetch를 취소하는 방법, AbortSignal.timeout()·any() 정적 메서드, React useEffect 클린업 패턴, 취소 가능한 비동기 함수 설계까지 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 Fetch API의 전반적인 사용법을 살펴봤습니다. 이번에는 진행 중인 요청을 취소하는 AbortControllerAbortSignal을 정리합니다. 타임아웃 처리, 검색 자동완성 debounce, React 컴포넌트 언마운트 시 요청 정리에 필수적입니다.


왜 취소가 필요한가

진행 중인 fetch를 취소하지 않으면 두 가지 문제가 발생합니다.

  1. 메모리 누수·상태 오염: 언마운트된 React 컴포넌트의 fetch가 완료되면 setState를 호출해 에러가 발생하거나 이전 응답이 새 컴포넌트를 덮어씁니다.
  2. 경쟁 상태(Race Condition): 검색창에서 빠르게 타이핑하면 여러 요청이 동시에 나가고, 나중에 보낸 요청이 먼저 돌아올 수 있습니다. 이전 요청을 취소해야 안전합니다.

AbortController 기본

const controller = new AbortController();
const { signal } = controller;

// signal 상태
console.log(signal.aborted); // false (초기)
console.log(signal.reason);  // undefined (초기)

// fetch에 signal 전달
const promise = fetch('/api/data', { signal });

// 요청 취소
controller.abort(); // 기본 reason: DOMException { name: 'AbortError' }
controller.abort('사용자 취소'); // 커스텀 reason

// 이후
console.log(signal.aborted); // true
console.log(signal.reason);  // '사용자 취소'

AbortController 구조와 흐름


AbortError 처리

async function fetchData(url) {
  const controller = new AbortController();
  const timerId = setTimeout(() => controller.abort('timeout'), 5000);

  try {
    const res = await fetch(url, { signal: controller.signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('요청 취소됨:', err.message, '이유:', controller.signal.reason);
      return null;
    }
    throw err; // 다른 에러는 상위로 전파
  } finally {
    clearTimeout(timerId);
  }
}

AbortError는 정상적인 취소이므로 사용자에게 에러로 보여주지 않습니다. err.name === 'AbortError'로 구분하세요.


AbortSignal.timeout() — 타임아웃 간소화

// 기존 방식
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);
fetch(url, { signal: controller.signal }).finally(() => clearTimeout(id));

// ✅ AbortSignal.timeout() — 한 줄로 해결 (Chrome 103+, Firefox 100+)
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });

타임아웃 시 발생하는 에러는 TimeoutError(err.name === 'TimeoutError')입니다. AbortError와 구분됩니다.


React useEffect 패턴

React에서의 취소 패턴

useEffect의 cleanup 함수에서 controller.abort()를 호출하면 컴포넌트 언마운트나 의존성 변경 시 자동으로 이전 요청을 취소합니다.

import { useState, useEffect } from 'react';

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!query) return;
    const controller = new AbortController();

    fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    })
      .then((r) => r.json())
      .then(setResults)
      .catch((err) => {
        if (err.name !== 'AbortError') console.error(err);
      });

    return () => controller.abort(); // query가 바뀌면 이전 요청 취소
  }, [query]);

  return <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>;
}

AbortSignal.any() — 여러 signal 합성

여러 신호 중 하나라도 abort되면 발동합니다.

const userController = new AbortController(); // 사용자가 수동 취소
const timeoutSignal = AbortSignal.timeout(10_000); // 10초 타임아웃

const combined = AbortSignal.any([userController.signal, timeoutSignal]);

const res = await fetch('/api/data', { signal: combined });
// 사용자 취소 또는 10초 타임아웃 중 먼저 발생하는 것으로 취소

signal.addEventListener — 취소 감지

signal의 abort 이벤트를 구독해 취소 시 자원을 정리할 수 있습니다.

async function streamWithCleanup(url, signal) {
  const ws = new WebSocket(url);

  signal.addEventListener('abort', () => {
    ws.close(1000, 'Aborted');
  });

  return new Promise((resolve, reject) => {
    ws.onmessage = (e) => resolve(e.data);
    ws.onerror = reject;
  });
}

취소 가능한 비동기 함수 설계

signal을 함수 인자로 받아 내부 비동기 작업에 전파하는 패턴입니다.

async function processItems(items, signal) {
  for (const item of items) {
    signal.throwIfAborted(); // abort됐으면 즉시 AbortError throw
    await processItem(item, signal); // 내부 fetch 등에도 signal 전달
  }
}

const controller = new AbortController();
processItems(largeList, controller.signal)
  .catch((err) => {
    if (err.name === 'AbortError') console.log('처리 중단');
    else throw err;
  });

// 3초 후 중단
setTimeout(() => controller.abort(), 3000);

signal.throwIfAborted()는 signal이 이미 abort된 경우 즉시 AbortError를 던집니다. 루프·재귀 함수에서 중간중간 체크하는 데 유용합니다.


정리

API용도
new AbortController()취소 컨트롤러 생성
controller.abort(reason)취소 신호 발송
controller.signalfetch 등에 전달할 신호
AbortSignal.timeout(ms)타임아웃 신호 생성
AbortSignal.any([...])복수 신호 합성
signal.throwIfAborted()루프 내 조기 중단 체크
signal.addEventListener('abort', fn)취소 시 정리 작업

지난 글: Fetch API 완전 이해


읽어주셔서 감사합니다. 😊