removeEventListener를 일일이 달아주다가 너무 귀찮아서 한 번에 지울 수 있는 방법을 검색해서 AbortController라는 것을 알게 되었다. 처음 본 것은 아니고 `AbortSignal`이라고 예전에 react-query와 axios를 함께 쓰면서 signal이라는 전달인자로 axios request header에 넘겨서 사용했던 기억이 있다.
/* Component.ts */
export const getCommentQuery = (id: string): UseQueryOptions<AxiosResponse<Ires>, AxiosError | Error> => ({
queryKey: ['comment', parseInt(id, 10)],
/**
* TIL: 쿼리 취소: 쿼리 취소는 요청 중인 쿼리를 취소하는 방법입니다. axios v0.22.0+ 버전을 사용하신다면 아래 코드와 같이 작성해서 axios api method header에 넣습니다.
* 쿼리가 비활성화(언마운트 등) 되면 자동으로 쿼리를 취소합니다.
* @see {@link https://beomy.github.io/tech/react/tanstack-query-v4/#%EC%BF%BC%EB%A6%AC-%EC%B7%A8%EC%86%8C}
*/
queryFn: async ({ signal }) => await getComment(id, signal),
onSuccess: () => {
console.log('렌더링');
},
...
/* api.ts */
export const getComment = async (id: string, signal?: AbortSignal) => {
try {
const response = instance.get<Ires>(`/comments/${id}`, {
// * Pass the signal to `axios`
signal,
});
return response;
} catch (error: unknown) {
if (isAxiosError(error)) {
...

mdn에 들어가보면 실험적인 기능이라고 하지만 실험적인 기능이라기에는 모든 브라우저와 호환성 체크 표시가 잘 되어 있어서 다양한 활용 예시를 보고자 더 공부하고 기록하게 되었다.

1. AbortController는 무엇일까?
1) AbortController란
- `AbortController`는 하나 이상의 웹 요청을 중단할 수 있는 인터페이스이다.
- 대표적으로 `fetch API`, `DOM Event`를 특정 시점에 중단시킬 수 있다.
2) AbortController API
(1) new AbortController()
- 웹 요청을 중단할 수 있는, `AbortController` 인터페이스를 생성한다.
const controller = new AbortController();
(2) AbortController.signal
- 요청을 취소할 수 있는 `AbortSignal` 객체 인터페이스를 반환한다.
const controller = new AbortController();
const signal = controller.signal;
- 웹 요청에 `AbortSignal` 객체의 `signal` 값을 인자로 넘기면, 요청을 취소할 수 있는 상태가 된다.
const controller = new AbortController();
const signal = controller.signal;
// fetch 요청을 취소할 수 있는 상태
const response = await fetch(url, { signal }); // signal를 넘겨줌
- dom event에도, `signal`을 인자로 넘기면 이벤트를 취소할 수 있는 상태가 된다.
const controller = new AbortController();
const signal = controller.signal;
// dom event를 취소할 수 있는 상태
button.addEventListener('click', onClick, { signal }); // signal를 넘겨줌
(3) AbortController.abort()
- `abort()` 를 사용하면, `signal`이 할당된 웹 요청을 취소할 수 있다.
const controller = new AbortController();
controller.abort();
- 아래 코드는 `abort()`를 호출해 버튼의 클릭 이벤트를 취소한 예시이다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
button {
padding: 10px;
}
</style>
<script type="module">
const controller = new AbortController();
const signal = controller.signal;
const onClick = function () {
console.log('aborted');
};
button.addEventListener('click', onClick, { signal }); // singal이 인자로 할당됨
controller.abort(); // 호출시, button의 click 이벤트를 취소. abort의 위치는 리스너 전후든 상관없다.
</script>
</head>
<body>
<button id="button">버튼</button>
</body>
</html>
(4) abortSignal.aborted
- `aborted`는 `abort()`가 실행되어 요청이 취소되었는지 여부를 알 수 있다.(`Boolean`)
const controller = new AbortController();
const abortSignal = controller.signal;
const isAborted = abortSignal.aborted;
- 아래 코드는 `aborted` 여부에 따라 서로 다른 콘솔을 출력하는 예시이다.
const controller = new AbortController();
const signal = controller.signal;
// ...
if (signal.aborted) console.log('요청 취소'); // (1)
else console.log('요청 취소되지 않음!'); // (2)
- 만약 `abort()`가 실행되어 요청이 취소되었다면, `aborted`는 `true`이므로 ‘요청 취소’를 출력한다. - (1)
- 그러나 요청이 취소되지 않았다면 ‘요청 취소되지 않음!’을 출력한다. - (2)
(5) abort event
event type이 `abort`다.
abort이벤트는 `abort()`가 실행되어 요청이 중단된 경우 발생한다.
// 이벤트 접근 방법1
addEventListener('abort', (event) => { ... });
// 이벤트 접근 방법2
onabort = (event) => { ... };
3) AbortController 활용법
`AbortController`를 어떤 상황에서 활용하면 좋을까? 여러 예시를 보도록 하자.
- 우선 `new AbortController`를 사용해 `AbortController` 객체를 만든다.
const controller = new AbortController();
- 그 다음으로 중요한 건, 사과 이모티콘을 요청하는 비동기 함수인 `requestAppleIcon()`이다.
(1) 비동기 처리 중간에 멈추기
- 비동기의 경우 중간에 호출을 멈추기 어렵다.
- 하지만 어떤 비동기 동작을 요청했는데 너무 오랜 시간이 걸려 취소해야한다면, 방법이 없을까?
- 이때 바로 AbortController를 사용하는 것이다!
- AbortController를 사용하면, 비동기 처리를 중간에 멈출 수 있다.
- 아래 예시에는 사과 이모티콘을 요청하는 버튼과 요청을 취소하는 버튼이 있다.
- 사과 요청하기 버튼은 사과 이모티콘을 화면에 띄우는 비동기 함수를 호출한다.
- 사과 이모티콘의 경우 요청 후 2초가 지나야 화면에 나타나는데, 취소 버튼을 누르면 비동기 처리를 중간에 취소할 수 있다.
See the Pen AbortController by Olimjo (@OLIMJO) on CodePen.
비동기 취소 코드는 어떻게 구성되어 있을까? 하나씩 살펴보자
- 우선 `new AbortController`를 사용해 `AbortController` 객체를 만든다.
const controller = new AbortController();
- 그 다음으로 중요한 건, 사과 이모티콘을 요청하는 비동기 함수인 `requestAppleIcon()`이다.
/* abort.js */
const result = document.querySelector('.result');
let timeoutKey;
let intervalKey;
let abortEventHandler;
let controller = new AbortController();
let signal = controller.signal;
function turnoffTimer() {
clearTimeout(timeoutKey);
clearInterval(intervalKey);
}
function requestAppleIcon() {
return new Promise((resolve, reject) => {
if (!signal.aborted) {
timeoutKey = setTimeout(() => {
/**
* 참고로 sto 콜백은 MicroTask Queue에 등록되어 0ms더라도
* 아래 이벤트 핸들러 등록보다 늦게 실행된다.
*/
clearInterval(intervalKey);
resolve('🍎 🍎 🍎 🍎'); // (1)
}, 2000);
}
// if문 통과해서 resolve 되더라도 아래 이벤트 리스너는 등록된다.(리스너가 쌓이게 된다.)
abortEventHandler = () => {
turnoffTimer();
reject(new DOMException('Aborted', 'AbortError'));
};
signal.addEventListener('abort', abortEventHandler); // (2)
});
}
- `signal.aborted`가 `false`라면, 정상적으로 sto 설정에 따라 사과 이모티콘을 반환한다.(resolve) - (1)
- `signal.aborted`가 `true`라면 `abort`이벤트를 리스닝하고 있다가 이벤트 핸들러를 실행한다.(reject) - (2)
- `requestFunc()`은 사과 요청하기 버튼 클릭시 실행하는 함수이다.
async function requestFunc() {
try {
// listener가 쌓이지 않게 여기서 제거해준다.
signal.removeEventListener('abort', abortEventHandler);
const timer = new Date().getTime();
intervalKey = setInterval(() => {
const thisTime = new Date().getTime();
const timeGap = Math.floor((thisTime - timer) / 1000);
result.innerText = timeGap;
}, 1000);
const data = await requestAppleIcon();
console.log(data); // 🍎 🍎 🍎 🍎
result.innerText = data; // (3)
} catch (error) {
console.log(error); // DOMException: Aborted
console.log(error.message); // Aborted
clearInterval(intervalKey);
if (error.message === 'Aborted') result.innerText = '중단되어 출력 불가 🙅♂️'; // (4)
}
}
- `requestAppleIcon()`에서 signal을 listen하고 있으므로 어딘가에서 `abort`이벤트를 만들면 중단시킬 수 있게 된다.
- fulfilled 되면 사과를 뱉어준다. - (3)
- 실행 중단 요청이 발생할 경우, 에러 메시지가 텍스트 노드에 삽입된다. - (4)
- `abortFunc()`은 요청 중단하기 버튼 클릭시 실행하는 함수이다.
function abortFunc() {
controller.abort(); // (5)
/**
* catch 문보다 아래 코드들이 먼저 실행된다.
*/
// 여기서 리스너 제거해도 상관없다. 어차피 리무버보다 이전에 등록한 리스너의 이벤트 핸들러가 먼저 발동된다.
signal.removeEventListener('abort', abortEventHandler);
// 'abort' 이벤트 발생 후 signal 삭제되었으므로 새로 생성해야 한다.
controller = new AbortController();
signal = controller.signal;
}
- `controller.abort()`를 실행하며 `abort`이벤트를 발생시켜 요청을 중단시킬 수 있다. - (5)
(2) dom 이벤트 한 번만 발생시키기
- dom 이벤트의 경우, addEventListener 메소드에 `once` 옵션을 `true`로 지정하면 이벤트를 한 번만 실행시킬 수 있다.
el.addEventListener('click', function() {
alert('안녕하세요!!');
}, { once : true });
그런데 `AbortController`를 사용해서도 dom이벤트를 한 번만 실행시킬 수 있다!
const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;
button.addEventListener('click', onClick, { signal }); // (1)
function onClick() {
console.log('clicked!');
controller.abort(); // (2)
}
- 방법은 간단한데, 우선 한 번만 실행하고 싶은 이벤트에 `signal`을 인자로 넘긴다. - (1)
- 그 다음 이벤트 발생시 `controller.abort()`를 실행하면 이벤트를 한 번만 발동시킬 수 있다. - (2)
(3) 이벤트 한 번에 제거하기, removeEventListener의 대안
- 이벤트 리스너의 경우, `removeEventListener`를 하지 않으면 이벤트 리스너가 요소보다 오래 남아 메모리 누수가 발생한다.
- 그렇기에 우리는 이벤트 리스너를 `removeEventListener`를 사용해 제거해줘야 한다.
- 그런데 여러 개의 이벤트 리스너가 있는 경우, 일일이 다 `removeEventListener`로 제거해야하나?
- 만약 `AbortController`를 몰랐다면 아래와 같이 제거해야 했을 것이다.
// 이벤트 리스너 등록
btn1.addEventListener('click', onClick);
btn2.addEventListener('click', onClick);
btn3.addEventListener('click', onClick);
// 이벤트 리스너 제거
window.addEventListener('beforeunload', () => {
btn1.removeEventListener('click', onClick);
btn2.removeEventListener('click', onClick);
btn3.removeEventListener('click', onClick);
};
)
- 하지만 `AbortController`를 사용하면, `abort()`를 호출하여 한 번에 이벤트 리스너를 제거할 수 있다!
// AbortController 객체 가져오기
const controller = new AbortController();
const { signal } = controller;
// signal인자를 넘기기
btn1.addEventListener('click', onClick, { signal });
btn2.addEventListener('click', onClick, { signal });
btn3.addEventListener('click', onClick, { signal });
// abort() 호출해서 이벤트 리스너 제거하기
window.addEventListener('beforeunload', () => {
controller.abort();
};
)
2. removeEventListener 사용하지 않고 listener 제거할 수 있는 방법 3가지
1. Using the once Option of addEventListener
2. Using the signal Option of addEventListener
3. Clone the element
위에서 다룬 `AbortSignals`나 addEventListener 옵션인 `once`설정을 통해 제거할 수도 있지만, 코드가 내 소스 코드가 아닌데 그 코드가 이벤트 리스너를 추가하고 있다면? 제 3자 스크립트가 내가 설정한 이벤트와 얽혀있고 그 얽혀있는 상태를 유지해야 한다면?
아래 방법(element cloning)을 고려해보도록 하자.
DOM 요소를 클론하게 되면, 이전에 등록된 모든 이벤트 핸들러들을 지워준다.
DOM 요소를 클론할 때 HTML 속성에 있는 on* 프로퍼티에 할당한 이벤트 핸들러들은 유실되지 않고 클론되면서 보존이 된다.
const button = document.querySelector('button);
// replace the element with a copy of itself
// and nuke all the event listeners
button.replaceWith(button.cloneNode(true));
<!-- This event handler will be copied when you cloned the node -->
<button onclick="console.log('Still here!')"`>click me</button>
브라우저 호환성
mdn 문서가면 실험적인 기능이라는 문구가 반겨줄 것이다.
근데 호환성 확인해보면 전부 적용 가능한 것 같다. 무난하게 사용할 수 있을 것 같다.

부록
1. AbortSignal.timeout 활용하기(feat. DOMException)
특정 시간 뒤에 `abort()`가 호출되도록 하려면 `AbortSignal.timeout`을 사용하는 게 더 간편하다.
활용 예시를 보도록 하자.
먼저, AbortSignal.timeout를 사용해서 간편화 하기 전의 코드이다.
httpbin이라는 simple http request & response service 사이트에서 5초 지연되는 응답을 받는 코드이다.
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 2_000);
const URL = 'https://httpbin.org/delay/5';
fetch(URL, { signal: controller.signal })
.then((res) => {
console.log(`Received: ${res.status}`);
}).catch((err) => {
if (err.name === 'AbortError') {
console.error('Aborted: ', err);
return;
}
throw err;
});
`setTimeout`을 통해 요청을 기다리는 시간이 2초가 지나면 더 이상 기다리지 않고 abort 하도록 한다.
이 때 `fetch()`가 취소되면 `AbortError`라는 `DOMException`을 던지기 때문에 취소된 오류와 다른 오류를 구분해서 처리할 수 있다.
🔗DOMException
- 문법
`new DOMException(message, name);` 자세한 건 링크 참고.
fetch() 에서 AbortSignal.timeout으로 더 간편하게
`AbortSignal.timeout`을 활용하면 `AbortController`를 생성할 필요도 없이 간단히 타임아웃을 지정할 수 있다.
const URL = 'https://httpbin.org/delay/5';
fetch(URL, { signal: AbortSignal.timeout(2_000) })
.then((res) => {
console.log(`Received: ${res.status}`);
}).catch((err) => {
if (err.name === 'AbortError') {
console.error('Aborted: ', err);
return;
}
throw err;
});
Axios는 더더 쉽다.
`timeout` 에 시간을 지정하면 된다.(단위 ms)
interface CustomInstance extends AxiosInstance {
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse<TcustomResponseFormat>>;
};
}
let refreshtoken;
let accesstoken;
const axiosToken: CustomInstance = axios.create({
baseURL: process.env.REACT_APP_SERVER_URL,
timeout: 1500, // ⭐ 여기!에 시간 지정하면 된다.
timeoutErrorMessage:
'Request Timeout over 1.5 seconds. check your refreshToken.',
withCredentials: true,
});
axiosToken.interceptors.request.use(
config => {
const { method, url } = config;
logOnDev(`🚀 [API] ${method?.toUpperCase()} ${url} | Request`);
...
레퍼런스
1. 참고한 블로그
2. https://css-tricks.com/using-abortcontroller-as-an-alternative-for-removing-event-listeners/
3. https://developer.mozilla.org/ko/docs/Web/API/AbortController