AbortController는 웹 요청을 취소할 수 있게 해주는 기능이다. 보통 웹에서 요청을 일단 보내면 이후에 필요 없어져도 취소할 방법이 없어서 그냥 요청은 그대로 두고 응답받은 내용을 사용 안 하는 식으로만 구현했다. 간단한 HTTP 요청을 응답이 꽤 빠르기 때문에 괜찮을 수도 있지만 무거운 요청의 경우는 불필요한 네트워크 트래픽을 낭비하게 되거나 연결을 차지하고 있으므로 취소하는 것이 좋다.
아직 실험적 기능이지만 현재 IE를 제외한 모든 메이저 브라우저에서 지원하고 있고 IE는 지난 15일 지원이 완전히 종료되었기 때문에 사용하기가 훨씬 수월해진 상태이다.
AbortController 사용방법
AbortController는 new AbortController()
를 사용해서 생성해서 사용하고 생성된 인스턴스에서 signal
프로퍼티로 AbortSignal
을 받아서 fetch()
같은 DOM 요청과 통신할 수 있다.
const controller = new AbortController();
const signal = controller.signal;
signal.addEventListener('abort', () => {
console.log('Aborted? ', signal.aborted);
})
setTimeout(() => {
controller.abort();
}, 100);
controller.abort()
를 실행하면 controller.signal
이 abort
이벤트를 발생시키고 이 이벤트가 발생했다는 의미로 controller.signal.aborted
가 true
가 된다. 이를 실행하면 다음과 같이 출력된다.
브라우저에서 AbortController
로 fetch()
취소하기
fetch()
로 httpbin을 통해 5초 걸리는 요청을 보내는 예제를 작성했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Abort Controller Demo</title>
</head>
<body>
<script>
const URL = 'https://httpbin.org/delay/5';
fetch(URL)
.then((res) => {
console.log(`Received: ${res.status}`);
}).catch((e) => {
console.error(e);
});
</script>
</body>
</html>
당연히 5초 뒤에 응답 로그가 출력된다.
이 응답을 받으려면 5초가 걸리지만 이후 비즈니스 로직에 따라 이 요청을 받을 필요가 없어졌다면AbortController를 이용해서 취소할 수 있다.
간단한 GET 요청은 fetch(resource)
처럼 간단하게 URL을 지정해서 사용하지만 fetch(resource, init)
처럼 두 번째 파라미터로 초기화 옵션을 줄 수 있다. 이 파라미터 문서를 보면 signal
을 옵션으로 받을 수 있는 걸 알 수 있다.
An AbortSignal object instance; allows you to communicate with a fetch request and abort it if desired via an AbortController.
즉, AbortSignal
의 인스턴스를 받고 AbortController
를 이용해서 원할 때 fetch
요청을 취소할 수 있다.(위 예제에서 <script>
부분만 작성했다.)
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;
});
앞에서 AbortController
를 살펴본 것처럼 5초 걸리는 GET 요청을 fetch()
로 보낸 후 2초 후에 controller.abort()
를 실행해서 fetch()
를 취소시켰다. fetch()
가 취소되면 AbortError
라는 DOMException
을 던지기 때문에 취소된 오류와 다른 오류를 구분해서 처리할 수 있다.
위 화면을 보면 https://httpbin.org/delay/5
요청이 2초 후에 취소된 것을 볼 수 있다.
AbortSignal.timeout
위에서 예제를 위해 특정 시간 뒤에 abort()
가 호출되도록 작성했지만, 요청에 타임아웃을 지정하려면 AbortSignal.timeout
를 사용하는 게 더 간편하다. 다만, 문서의 호환 표처럼 아직은 Firefox에서만 제대로 지원하고 있고 Chrome은 103버전에서 지원할 예정이다.(현재 테스트할 때는 Chrome 102이다.)
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;
});
이렇게 하면 앞의 예제와 동일하게 동작한다. 다만 문서에 따르면 이 경우에는 AbortError
대신 TimeoutError
이 발생해야 하는데 내가 테스트했을 때는 AbortError
가 발생했다. Chrome 105에서 테스트해도 동일한 걸 보면 Firefox의 문제는 아닌 것 같은데 왜 그런지는 아직 찾지 못했다.
어쨌든 이 기능을 브라우저에서 쓰려면 다른 브라우저도 지원할 때까지 좀 더 기다려야 할 것 같다.
Node.js Stream에서 AbortController 사용하기
AbortController
가 fetch
에만 사용하도록 만들어 진 것이 아니라 AbortController
를 지원하는 곳이면 어디든지 사용할 수 있다. 대표적으로 Node.js의 Stream
이 있다.
간단히 파일을 스트림으로 복사하는 예제를 만들어 보자. 약간 큰 파일이 필요해서 mkfile -n 1g 1GB.file
명령어로 1GB 용량의 파일을 생성했고 이를 읽어서 output.file
파일로 복사하는 예제이다.
// stream-file.js
const { createReadStream, createWriteStream } = require('node:fs');
const input = createReadStream('./1GB.file');
const output = createWriteStream('./output.file');
input.pipe(output);
output.on('error', (err) => {
console.error(err);
});
output.on('finish', () => {
console.log('Finished');
});
위 파일을 실행하면 아래처럼 Finished
가 출력되고 1GB의 output.file
이 생성된 것을 볼 수 있다.
$ node stream-file.js
Finished
여기에 AbortController를 적용해보자. Node.js에서도 15.4.0부터 AbortController를 전역으로 제공하기 때문에 그대로 사용할 수 있다.
// stream-abortcontroller.js
const { createReadStream, createWriteStream } = require('node:fs');
const { addAbortSignal } = require('node:stream');
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 300);
const input = createReadStream('./1GB.file');
const output = addAbortSignal(
controller.signal,
createWriteStream('./output.file')
);
input.pipe(output);
output.on('error', (err) => {
if (err.name === 'AbortError') {
console.error('Aborted: ', err);
return;
}
throw err;
});
output.on('finish', () => {
console.log('Finished');
});
AbortController
의 사용 방법은 같지만 fetch
처럼 stream
에 시그널을 전달해 주어야 하므로 stream
에서는 stream.addAbortSignal(signal, stream)
를 제공한다. 여기서는 새로운 AbortController
인스턴스를 생성해서 300ms 후에 abort()
가 호출되도록 했고 addAbortSignal
를 이용해서 이 컨트롤러의 시그널과 createWriteStream('./output.file')
를 지정해서 해당 스트림에 시그널이 추가되도록 했다.
$ node stream-abortcontroller.js
Aborted: AbortError: The operation was aborted
at AbortSignal.onAbort (node:internal/streams/add-abort-signal:37:20)
at AbortSignal.[nodejs.internal.kHybridDispatch] (node:internal/event_target:643:20)
at AbortSignal.dispatchEvent (node:internal/event_target:585:26)
at abortSignal (node:internal/abort_controller:284:10)
at AbortController.abort (node:internal/abort_controller:315:5)
at Timeout._onTimeout (/Users/outsider/Dropbox/projects/temp/abort-controller/stream.js:6:16)
at listOnTimeout (node:internal/timers:559:17)
at processTimers (node:internal/timers:502:7) {
code: 'ABORT_ERR'
}
이를 실행하면 Abort가 잘 실행된 것을 확인할 수 있다.
Node.js은 16.14.0부터 AbortSignal.timeout(delay)
를 지원하기 때문에 이 버전 이상이라면 타임아웃만을 위해서는 앞에서 본 것처럼 AbortController
를 생성할 필요 없이 AbortSignal.timeout()
을 사용할 수 있다.
// stream-timeout.js
// 생략
const output = addAbortSignal(
AbortSignal.timeout(300),
createWriteStream('./output.file')
);
// 생략
앞뒤 부분은 똑같아서 생략했지만, 이 코드도 똑같이 동작한다.
$ node stream-timeout.js
Aborted: AbortError: The operation was aborted
at AbortSignal.onAbort (node:internal/streams/add-abort-signal:37:20)
at AbortSignal.[nodejs.internal.kHybridDispatch] (node:internal/event_target:643:20)
at AbortSignal.dispatchEvent (node:internal/event_target:585:26)
at abortSignal (node:internal/abort_controller:284:10)
at Timeout._onTimeout (node:internal/abort_controller:112:7)
at listOnTimeout (node:internal/timers:559:17)
at processTimers (node:internal/timers:502:7) {
code: 'ABORT_ERR'
}
Comments