AbortController — fetch, 이벤트 리스너, 타이머 취소 통합
AbortController는 비동기 작업을 취소 할 수 있는 표준 API입니다. fetch 요청 취소가 가장 대표적이지만, 이벤트 리스너 제거, 타이머 취소, 심지어 여러 작업을 한 번에 취소하는 데도 활용할 수 있습니다.
기본 사용법
// 1. 컨트롤러 생성
const controller = new AbortController();
const signal = controller.signal;
// 2. signal을 전달
fetch("/api/data", { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === "AbortError") {
console.log("요청이 취소되었습니다.");
} else {
console.error("에러:", err);
}
});
// 3. 취소
controller.abort();
fetch 취소 — 가장 흔한 활용
React에서의 cleanup
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await fetch("/api/users", {
signal: controller.signal,
});
const data = await response.json();
setUsers(data);
} catch (err) {
if (err.name !== "AbortError") {
setError(err);
}
}
}
fetchData();
// 컴포넌트 언마운트 시 취소
return () => controller.abort();
}, []);
검색 자동완성 — 이전 요청 취소
let currentController = null;
async function search(query) {
// 이전 요청 취소
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: currentController.signal,
});
return await response.json();
} catch (err) {
if (err.name === "AbortError") {
return null; // 취소된 요청은 무시
}
throw err;
}
}
타임아웃 패턴
AbortSignal.timeout (모던 브라우저)
// 5초 후 자동 취소
try {
const response = await fetch("/api/slow", {
signal: AbortSignal.timeout(5000),
});
const data = await response.json();
} catch (err) {
if (err.name === "TimeoutError") {
console.log("5초 타임아웃!");
}
}
수동 타임아웃 구현
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, {
...options,
signal: controller.signal,
}).finally(() => clearTimeout(timeoutId));
}
이벤트 리스너 자동 제거
addEventListener의 signal 옵션으로 이벤트 리스너를 자동 제거할 수 있습니다.
const controller = new AbortController();
// signal로 등록한 이벤트 리스너
window.addEventListener("resize", handleResize, {
signal: controller.signal,
});
window.addEventListener("scroll", handleScroll, {
signal: controller.signal,
});
document.addEventListener("click", handleClick, {
signal: controller.signal,
});
// 한 번에 모든 리스너 제거!
controller.abort();
// removeEventListener를 각각 호출할 필요 없음
실전 — 모달 열기/닫기
function openModal() {
const controller = new AbortController();
const { signal } = controller;
// 모달 관련 이벤트 등록
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeModal();
}, { signal });
overlay.addEventListener("click", closeModal, { signal });
function closeModal() {
modal.remove();
controller.abort(); // 모든 이벤트 리스너 자동 제거
}
}
AbortSignal.any — 여러 신호 조합
// 타임아웃 또는 사용자 취소 중 먼저 발생하는 것
const userController = new AbortController();
const signal = AbortSignal.any([
AbortSignal.timeout(10000), // 10초 타임아웃
userController.signal, // 사용자 취소 버튼
]);
fetch("/api/data", { signal });
// 취소 버튼 클릭 시
cancelButton.onclick = () => userController.abort();
abort 이유 전달
const controller = new AbortController();
controller.signal.addEventListener("abort", () => {
console.log("취소 이유:", controller.signal.reason);
});
// 이유를 전달하며 취소
controller.abort("사용자가 페이지를 떠남");
// "취소 이유: 사용자가 페이지를 떠남"
// AbortSignal.timeout은 자동으로 TimeoutError를 이유로 설정
커스텀 비동기 작업 취소
// AbortSignal을 지원하는 커스텀 함수
function delay(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
// 이미 취소된 경우
if (signal?.aborted) {
reject(new DOMException("Aborted", "AbortError"));
return;
}
const timeoutId = setTimeout(resolve, ms);
// 취소 시 타이머 정리
signal?.addEventListener("abort", () => {
clearTimeout(timeoutId);
reject(new DOMException("Aborted", "AbortError"));
});
});
}
// 사용
const controller = new AbortController();
delay(5000, { signal: controller.signal })
.then(() => console.log("5초 경과"))
.catch((err) => {
if (err.name === "AbortError") console.log("취소됨");
});
// 2초 후 취소
setTimeout(() => controller.abort(), 2000);
주의사항
// 1. 한번 abort()하면 재사용 불가
const controller = new AbortController();
controller.abort();
console.log(controller.signal.aborted); // true — 영구적
// 새 요청에는 새 컨트롤러가 필요
const newController = new AbortController();
// 2. AbortError 구분
try {
await fetch(url, { signal });
} catch (err) {
if (err.name === "AbortError") {
// 의도된 취소 — 에러로 처리하지 않음
return;
}
// 진짜 에러
throw err;
}
활용 요약
| 패턴 | 설명 |
|---|---|
| fetch 취소 | 컴포넌트 언마운트, 검색 자동완성 |
| 타임아웃 | AbortSignal.timeout(ms) |
| 이벤트 리스너 정리 | addEventListener({ signal }) |
| 여러 작업 일괄 취소 | 하나의 controller로 여러 작업 관리 |
| 조건부 취소 | AbortSignal.any([...signals]) |
**기억하기 **: AbortController는 "비동기 작업의 취소 버튼"입니다. fetch뿐 아니라 이벤트 리스너, 타이머, 커스텀 비동기 작업까지 통합 관리할 수 있습니다. React의 useEffect cleanup에서 fetch를 취소하는 패턴은 실무 필수입니다.
댓글 로딩 중...