우리가 검색창에 글자를 입력할 때마다 서버에 요청을 보내는 자동완성 기능을 떠올려봅시다. 사용자가 “자바스크립트”를 검색하려고 “자”, “자바”, “자바스”를 빠르게 입력하면, 이전에 보낸 요청들은 더 이상 필요 없어집니다.

이처럼 비동기 작업을 시작한 뒤 중간에 취소해야 하는 상황은 실무에서 꽤 자주 발생하는데요, 자바스크립트에서는 AbortController를 통해 이런 비동기 작업을 깔끔하게 취소할 수 있습니다. 이번 글에서는 AbortController의 기본 구조부터 실무에서 활용하는 패턴까지 함께 살펴보겠습니다.
미리 요점만 콕 집어보면?
AbortController를 사용하려면 먼저 두 가지 개념을 알아야 합니다. 바로 AbortController 자체와, 여기서 파생되는 AbortSignal입니다.

AbortController를 생성하면, signal이라는 속성을 통해 AbortSignal 객체가 함께 노출됩니다. AbortController가 취소 명령을 내리는 쪽이라면, AbortSignal은 그 취소 신호를 전달받는 쪽입니다.
비유하자면, AbortController는 리모컨이고, AbortSignal은 TV에 달린 수신기라고 생각할 수 있습니다. 리모컨의 버튼을 누르면 수신기가 신호를 받아서 TV가 꺼지는 것처럼, controller.abort()를 호출하면 signal이 신호를 받아서 연결된 작업이 취소됩니다.
const controller = new AbortController();
const signal = controller.signal;
console.log(signal.aborted); // false
controller.abort(); // 취소 명령 전달
console.log(signal.aborted); // true
AbortController()로 컨트롤러를 생성하면, 자동으로 signal 속성에 AbortSignal 객체가 만들어집니다. 이 상태에서 abort()를 호출하면 signal의 aborted 속성이 true로 바뀌면서, 이 signal을 전달받은 비동기 작업이 중단됩니다.
그렇다면 취소 시점에 맞춰 정리 작업을 해야 할 때는 어떻게 할까요? signal의 abort 이벤트를 활용하면 됩니다.
const controller = new AbortController();
const signal = controller.signal;
signal.addEventListener("abort", () => {
console.log("작업이 취소되었습니다");
console.log("취소 사유:", signal.reason);
});
controller.abort("사용자가 페이지를 떠남");
// "작업이 취소되었습니다"
// "취소 사유: 사용자가 페이지를 떠남"
abort() 메서드에 인자를 전달하면 signal.reason으로 취소 사유를 확인할 수 있습니다. 참고로 reason에는 문자열뿐만 아니라 어떤 값이든 넣을 수 있어서, 에러 객체나 코드 값을 전달하는 식으로 활용할 수도 있습니다. reason을 생략하면 기본적으로 “AbortError” DOMException이 들어갑니다.
AbortController의 기본 구조를 알았으니, 이제 실제로 가장 많이 쓰이는 fetch 요청 취소에 적용해 보겠습니다.
AbortController가 가장 많이 쓰이는 곳은 fetch 요청입니다. fetch의 두 번째 인자에 signal을 전달하면, 해당 요청을 원하는 시점에 취소할 수 있습니다.
const controller = new AbortController();
fetch("https://api.example.com/users", {
signal: controller.signal, // signal 연결
})
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === "AbortError") {
console.log("요청이 취소되었습니다");
} else {
console.error("요청 실패:", err);
}
});
controller.abort();
abort()가 호출되면 fetch는 AbortError라는 특별한 에러를 발생시킵니다. catch 블록에서 err.name을 확인하면 사용자가 의도적으로 취소한 것인지, 네트워크 오류인지 구분할 수 있습니다.
그런데 실무에서 자동완성처럼 연속으로 요청을 보내는 경우, 단순히 이전 요청을 abort하는 것만으로는 부족할 수 있습니다. 네트워크 환경이나 캐시 상태에 따라 늦게 도착한 이전 응답이 최신 결과를 덮어쓰는 경우가 생기기도 하는데요, 이런 상황을 방지하려면 abort와 함께 마지막 요청만 반영하는 로직을 같이 사용하는 편이 안전합니다.
let currentController = null;
async function search(query) {
// 이전 요청이 있으면 취소
if (currentController) currentController.abort();
currentController = new AbortController();
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: currentController.signal,
});
const data = await res.json();
renderResults(data); // 마지막 요청 결과만 반영
} }
}
}
이렇게 하면 이전 요청을 가능한 빨리 취소할 수 있습니다. 위 코드에서 매번 new AbortController()로 새 컨트롤러를 만드는 이유는, 한 번 abort된 signal은 재사용할 수 없기 때문입니다. 이미 abort된 signal을 다시 fetch에 넘기면 요청이 즉시 reject되므로, 매 요청마다 새로운 AbortController를 생성해야 합니다.
다만 abort는 fetch 요청 자체를 중단시킬 뿐, UI 반영 로직까지 자동으로 막아주는 것은 아닙니다. 실무에서는 이미 처리 단계에 들어간 이전 응답이 뒤늦게 화면을 덮어쓰는 경우까지 방지하기 위해, abort와 함께 요청 ID를 비교하는 방식으로 마지막 요청만 반영하도록 만드는 것이 더 안전합니다.
서버 응답이 너무 오래 걸릴 때 자동으로 요청을 취소하고 싶다면, AbortSignal.timeout()을 활용할 수 있습니다.
// 5초 안에 응답이 없으면 자동 취소
fetch("https://api.example.com/data", {
signal: AbortSignal.timeout(5000),
})
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === "TimeoutError") {
console.log("요청 시간이 초과되었습니다");
}
});
AbortSignal.timeout(5000)은 5초 후에 자동으로 취소 신호를 보내는 signal을 생성합니다. 기존에는 setTimeout과 abort()를 조합해야 했지만, 이 정적 메서드를 사용하면 한 줄로 간결하게 타임아웃을 설정할 수 있습니다.
스펙상 timeout으로 인한 취소는 TimeoutError로 구분할 수 있게 되어 있지만, fetch에서 최종적으로 어떤 에러 이름이 보이는지 런타임에 따라 차이가 있을 수 있습니다. 그래서 에러를 더 정확히 구분하고 싶다면 err.name과 함께 signal.reason까지 같이 확인하는 편이 안전합니다.
AbortController는 fetch 외에도 다양한 Web API에서 활용할 수 있습니다. 이벤트 리스너 관리부터 여러 비동기 작업을 한꺼번에 제어하는 패턴까지 살펴보겠습니다.
AbortController는 fetch뿐만 아니라 이벤트 리스너 관리에도 활용할 수 있습니다. addEventListener의 세 번째 인자에 signal을 전달하면, abort() 호출 시 이벤트 리스너가 자동으로 해제됩니다.
const controller = new AbortController();
document.addEventListener(
"click",
() => {
console.log("클릭!");
},
{ signal: controller.signal } // signal 전달
);
// 이벤트 리스너 자동 해제
controller.abort();
기존에는 removeEventListener로 직접 이벤트를 해제해야 했고, 이때 동일한 함수 참조를 유지해야 하는 번거로움이 있었습니다. signal을 활용하면 이런 불편함 없이 깔끔하게 이벤트를 정리할 수 있습니다.
특히 하나의 controller로 여러 이벤트 리스너를 한 번에 해제할 수도 있습니다.
const controller = new AbortController();
const { signal } = controller;
window.addEventListener("resize", handleResize, { signal });
window.addEventListener("scroll", handleScroll, { signal });
document.addEventListener("keydown", handleKeydown, { signal });
// 세 개의 이벤트 리스너를 한 번에 해제
controller.abort();
컴포넌트 언마운트나 페이지 전환 시점에 controller 하나만 abort하면 관련된 리스너를 한꺼번에 정리할 수 있어, 이벤트 해제 누락으로 인한 리소스 낭비를 줄이는 데 유리합니다.
같은 원리로, 하나의 signal을 여러 fetch 요청에 전달하면 한 번의 abort()로 모든 요청을 동시에 취소할 수 있습니다.

const controller = new AbortController();
const { signal } = controller;
// 여러 요청에 같은 signal 전달
const userReq = fetch("/api/user", { signal });
const postReq = fetch("/api/posts", { signal });
const commentReq = fetch("/api/comments", { signal });
// 페이지 이동 시 모든 요청을 한 번에 취소
controller.abort();
또한 여러 개의 취소 조건을 하나로 합쳐야 할 때는 AbortSignal.any()를 활용할 수 있습니다.
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
// 사용자가 취소하거나, 5초가 지나면 취소
const signal = AbortSignal.any([
userController.signal,
timeoutSignal,
]);
fetch("/api/data", { signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => console.log("취소됨:", err.message));
AbortSignal.any()는 전달받은 signal 중 하나라도 abort되면 취소 신호를 보내며, 이때 가장 먼저 abort된 signal의 reason이 최종 reason으로 전달됩니다. 위 코드에서는 “사용자가 직접 취소”하거나 “5초 타임아웃”이 발생하면 요청이 취소됩니다. 다만, AbortSignal.any()는 비교적 최신 API라 일부 구형 환경에서는 호환성 확인이 필요할 수 있습니다.
이번 글에서는 AbortController의 기본 구조부터 fetch 취소, 이벤트 리스너 관리, 그리고 여러 작업을 한 번에 취소하는 패턴까지 살펴봤습니다.비동기 작업에서 “시작”하는 방법은 많이 다루지만, “취소”하는 방법은 상대적으로 덜 주목받는 경우가 많습니다. 하지만 불필요한 요청을 제때 취소하지 않으면 메모리 누수나 예상치 못한 상태 업데이트로 이어질 수 있기 때문에, 비동기 작업의 취소는 시작만큼이나 중요한 부분입니다. AbortController의 사용법이 그리 어렵지 않은 만큼, 다음 프로젝트에서 한번 적용해 보시면 좋겠습니다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.