빠른 흐름
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(url, { signal: controller.signal });
} catch (err) {
if (err.name === "AbortError") {
return;
}
throw err;
} finally {
clearTimeout(timeoutId);
}여기서는 결국 "누가 생명주기를 끊을 수 있는가"를 정리합니다.
타임아웃 구조
먼저 머릿속에 넣어둘 기본형은 아래입니다.
- 직접 취소 제어:
new AbortController() - 단일 타임아웃:
AbortSignal.timeout(ms) - 여러 조건 결합:
AbortSignal.any([...]) - abort 에러 분기:
if (err.name === "AbortError") - 타이머 정리:
clearTimeout(...)
단일 타임아웃
AbortSignal.timeout()
단일 요청 타임아웃은 이쪽이 제일 간단합니다.
await fetch(url, { signal: AbortSignal.timeout(2000) });즉 별도 컨트롤러가 필요 없는 단순 timeout 케이스라면 이 패턴이 기준입니다.
신호 결합
AbortSignal.any()
실전에서는 보통 취소 조건이 하나가 아닙니다.
- 사용자 취소
- 서버 종료
- 타임아웃
이럴 때 여러 signal을 합쳐 하나의 취소 조건으로 다루는 것이 깔끔합니다.
const signal = AbortSignal.any([
AbortSignal.timeout(3000),
controller.signal,
]);취소 이유를 같은 에러로 뭉개지 않는다
timeout, 사용자 취소, 상위 shutdown은 모두 abort로 연결할 수 있지만, 운영에서는 왜 취소됐는지 구분할 필요가 많습니다.
그래서 abort 자체를 정상 흐름으로 먼저 분리하고, 필요하면 signal.reason까지 같이 보는 편이 좋습니다.
abort를 건 쪽이 정리 책임도 가진다
직접 setTimeout으로 controller를 abort했다면, 성공/실패/취소 어느 경로든 타이머 정리를 같이 해야 합니다.
이 카드가 단순 취소 문법보다 생명주기 카드에 가까운 이유가 여기 있습니다.
언제 어떤 신호를 쓸까
체크포인트
- 네트워크 요청 timeout
- stream/pipeline cancel
- 장시간 작업의 외부 중단
- UI 취소 버튼과 timeout을 동시에 연결할 때
- 단일 timeout이면
AbortSignal.timeout - 직접 제어면
AbortController - 여러 조건 결합이면
AbortSignal.any - 취소와 실패는 분리 처리
- abort를 건 쪽이 타이머와 리소스 정리도 같이 담당
주의할 점
취소를 일반 에러처럼 전부 로깅하면 실제 장애와 정상 취소가 섞여서 운영 신호가 흐려집니다. Abort 계열 에러는 먼저 걸러내고, 그다음 진짜 실패만 로깅하는 편이 맞습니다.
참고 링크
2 sources