블로그 목록
frontend

AbortSignal.timeout()으로 fetch 타임아웃 걸기 — AbortController 수동 관리 그만

fetch에는 타임아웃 옵션이 없다. 네트워크가 느리거나 서버가 응답을 안 주면 요청이 그냥 매달려 있는다. 로딩 스피너가 30초씩 돌아가는 화면을 만들고 싶지 않으면 직접 끊어줘야 한다.

예전엔 AbortController를 만들고 setTimeout으로 취소를 걸었다.

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch('/api/data', { signal: controller.signal });
  return await res.json();
} finally {
  clearTimeout(timer); // 이걸 빼먹으면 타이머가 남는다
}

동작은 하는데 컨트롤러 만들고, 타이머 걸고, finally에서 clearTimeout까지 챙겨야 한다. 이 정리를 빼먹으면 요청이 성공해도 타이머가 5초 동안 살아 있다.

AbortSignal.timeout()

AbortSignal.timeout(ms)는 지정한 시간 뒤에 자동으로 abort되는 signal을 바로 만들어준다. 컨트롤러도, 타이머 정리도 필요 없다.

try {
  const res = await fetch('/api/data', {
    signal: AbortSignal.timeout(5000),
  });
  return await res.json();
} catch (err) {
  if (err.name === 'TimeoutError') {
    console.log('5초 안에 응답이 없었음');
  }
  throw err;
}

타임아웃으로 끊기면 err.name이 TimeoutError로 온다. 사용자가 직접 취소한 AbortError와 구분되기 때문에, "시간 초과"와 "취소됨"을 다른 메시지로 보여줄 수 있다.

AbortSignal.any()로 신호 합치기

타임아웃도 걸고 싶은데, 컴포넌트가 언마운트되면 사용자 취소도 걸고 싶을 때가 있다. 이럴 때 AbortSignal.any()로 여러 signal을 하나로 묶는다. 둘 중 하나라도 abort되면 요청이 끊긴다.

function fetchWithTimeout(url, { signal, timeout = 5000 } = {}) {
  const signals = [AbortSignal.timeout(timeout)];
  if (signal) signals.push(signal);

  return fetch(url, { signal: AbortSignal.any(signals) });
}

React에서 쓰면 이런 모양이 된다.

useEffect(() => {
  const controller = new AbortController();
  fetchWithTimeout('/api/data', { signal: controller.signal })
    .then((res) => res.json())
    .then(setData)
    .catch((err) => {
      if (err.name !== 'AbortError') setError(err);
    });
  return () => controller.abort();
}, []);

언마운트되면 controller.abort()가 걸리고, 5초가 지나면 타임아웃이 걸린다. 두 신호를 따로 관리할 필요 없이 any()가 알아서 먼저 도착한 쪽으로 끊는다.

정리

AbortSignal.timeout()과 AbortSignal.any()는 지금 모든 최신 브라우저와 Node 18+에서 쓸 수 있다(Baseline). fetch 타임아웃 유틸을 직접 만들거나 axios의 timeout 옵션을 쓰려고 라이브러리를 붙이던 걸, 이제 플랫폼 기능만으로 끝낼 수 있다. setTimeout/clearTimeout 짝을 손으로 맞추다 타이머를 흘리는 실수도 자연스럽게 사라진다.