Web API에서 AbortController를 파헤쳐보자.

Gyeop
8 min readJun 6, 2021

--

아!!! 내가 날린 fetch 요청 취소하고 싶다!

브라우저에서 비동기 요청은 필수 불가결한 존재가 되었다. AJAX는 웹에서 비동기적으로 페이지의 일부분을 다시 렌더링하여, 보다 좋은 사용자 경험을 위해 사용된다고 설명하지만, 사실상 SPA를 이용하여 많이 개발하는 요즘 시대에는 꼭 필요한 존재가 되었다.

fetch를 이용해서 충분히 처리할 수도 있지만, 프론트엔드 생태계에서는 보다 개발의 편의성과 속도 향상을 위해 axiossuperagent 와 같은 라이브러리를 종종 이용하곤 한다. 또한 리액트를 주로 사용한다면, 오히려 react-queryswr과 같은 hooks를 위한 다양한 라이브러리도 많이 생겨나고 있다.

그런데, 이렇게 많은 생태계가 발전함과 동시에 자주 사용하면서 생긴 하나의 의문점이 있다면, “왜 비동기 처리를 취소할 수는 없는가?” 에 대한 부분이었다.

본인 같은 경우는 대부분 리액트 프레임워크로 사내에서 개발을 하고 있는데, 유저가 특정 스크린에 접근하여 특정 데이터에 요청을 보낸 이후, 해당 스크린에서 이탈해버리면 브라우저에 알려 줄 방법이 없으므로, 비동기 요청이 완료된 이후에야 클라이언트 측의 상태를 파악하여 렌더링 되는 로직들을 캔슬시키는 방법을 사용하고 있었다. (특히 redux-saga를 이용한다면 generator funtion에서 중간에 특정 조건을 만족하면 해당 태스크를 캔슬하는 코드를 자주 작성하게 된다.)

그래서 취소 어떻게 해요?

위에서 적었다시피, 비동기 요청을 중간에 캔슬됨을 감지하고 네트워크상에서 중단시켜버리면 이후의 불필요한 로직은 깔끔하게 제거할 수 있을 텐데, 왜 이러한 부분을 구현할 수는 없는 걸까? 라는 의문은 계속되었다.

그렇게 찾게 된 AbortController.

뭔가 최신스펙인가 싶어서 찾아보니, 2015년부터 해당 기능 구현에 대해 굉장히 많은 논의가 있었고, 정식적으로는 2017년 9월 경에 채택되었다고 한다. 이후에 브라우저 바로 지원되기 시작했다. (브라우저에서 언제부터 지원되기 시작했는지에 대해서는 Can I use 에서 Date relative를 눌러 쉽게 확인할 수 있다.)

예제코드를 간단히 구현해보자.

const controller = new AbortController();controller; // AbortController {signal: {aborted: false, onabort: null}}

AbortController 를 이용해 생성한 인스턴스에는 프로토타입에 abort 라는 함수를 물려받는다. 해당 함수를 실행하면, 컨트롤러 내부의 aborted 의 value가 true로 변경된다.

뿐만 아니라 AbortControllersignal은 내부적으로 이벤트 핸들러를 가지고있기 때문에, 이벤트를 추가할 수 있다(자세한 스펙은 여기). AbortController를 이용하려면 fetch를 이용할 때 두번째 파라미터의 option에 추가해주면 된다.

위의 글을 바탕으로 실제로 실행할 수 있는 간단한 코드.

const controller = new AbortController();
const signal = controller.signal;
signal.addEventListener("abort", () => {
console.log("has cancelled!");
});
const options = { signal, method: "GET" };fetch("https://httpbin.org/get", options)
.then(res => {
if (res.ok) {
return res.json();
} else {
throw new Error(res);
}
})
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error);
});
// 비동기 요청이 브라우저의 Web API에서 처리중일테니, 바로 호출해본다.
controller.abort();
취소된 네트워크 요청

실제로 위의 코드를 브라우저에서 실행하면, fetchcatch 체이닝에서 다음과 같은 에러가 잡힌다. DOMException: The user aborted a request.

사용해보면서 간단히 정리한 부분

예제코드는 간단하게 작성했지만, 실제로 어떻게 사용될 수 있을지 여러가지 방면으로 실험해보았다.

  1. 참고로 fetch 요청이 끝나고 response를 받고난 이후에, 첫번째 then 체이닝에서 호출해도 같은 오류가 발생한다. 결국 해당 체이닝 클로저 내부에서 컨트롤러의 abort 메서드를 호출하는 순간 에러를 반환하여 catch 구문에서 잡힌다고 볼 수 있다.
  2. 서버에서 오류를 받은 것인지, 또는 중간에 의도적으로 끊은지 알기 위해서는 에러의 네임으로 판단할 수 있다. catch 구간에서 error.nameAbortError이면 중간에 컨트롤러로 인해 의도적으로 캔슬된 에러임을 판단할 수 있다.
  3. 요청이 완료된 이후에 controller.abort()를 실행해도 상관없다. then 체이닝이나 tryCatch스코프에서 이미 벗어났으므로 잡히지 않을 것이고, 만약 이벤트를 걸어놨다면, 구독하고 있던 콜백함수만이 실행된다.
  4. 걸어놓은 이벤트의 콜백함수는 최초 1회만 실행된다. 최초 1회 실행 후, 이후에 계속 controller.abort()를 실행해도 이벤트 리스너에 걸어놓은 콜백은 실행되지 않는다.
  5. fetch를 하기 전에 먼저 controller.abort()를 실행하면, 네트워크 요청 자체를 하지 않고 에러를 반환한다.
  6. 하나의 fetch에 꼭 하나의 컨트롤러가 1:1로 매핑될 필요는 없다. 연속적으로 요청을 보내야하는 경우가 있다면, 하나의 컨트롤러로 관리해도 상관없어 보인다. 특히 Promise.all같은 경우는 하나의 컨트롤러로 배열 내부의 모든 비동기 처리를 취소할 수 있다.
const controller = new AbortController();
const signal = controller.signal;
signal.addEventListener("abort", () => {
console.log("has cancelled!");
});
const options = { signal, method: "GET" };const getDatas = async () => {
const fetchArray = [
fetch("https://httpbin.org/get", options),
fetch("https://httpbin.org/get", options),
fetch("https://httpbin.org/get", options),
fetch("https://httpbin.org/get", options),
fetch("https://httpbin.org/get", options),
];
try {
const datas = await Promise.all(fetchArray);
console.log(datas);
} catch (error) {
console.log(error);
}
};
getDatas();controller.abort();

그래서 이거 쓰나요?

FYI : 잘 몰라요.

물론 아직까지 해당기능이 많이 보편화되었는지에 대한 확인이 어려워서 찾아봤는데, fetch를 대신하는 많은 라이브러리들이 해당 기능을 기반으로 구현하지 않았을까 하는 의심에 axios에서 제공하는 cancel 기능을 찾아보니, tc39의 스테이지1에 올라왔던 proposal-cancelable-promises 를 기반으로 구현했다고 한다. (물론 tc39에서 반려되어서, 이후 관리되지 않는 것으로 보인다.)

참고로 proposal-cancelable-promises는 아래 구문을 지원하려 했다고 한다.(wow)

try {}
catch cancel (reason) {

}
catch (err) {
}
finally {
}

결국 잘 사용되고 있는지는 잘 모르겠다.
혹시 잘 사용하고 있는 곳이 있다면 알려주시면 감사하겠습니다.(__)

삽질 후의 결론

뭔가 보편적으로 사용되는 API인지 파악하기가 어렵고(레퍼런스의 부재..), Web API이다보니 node환경에서 지원하지 못하는 부분(Node 15버전부터 지원되는데, 이걸 서비스에 사용하는 곳이…)과 IE에서도 지원되지 않는 점이 어쩌면 사용하지 못하는 큰 걸림돌이 아닐까 생각한다. 물론 IE를 위한 abortcontroller-polyfill 이 존재하기도 한다. 물론 크로스 브라우징을 생각하면 고통받으며 사용 및 유지보수하는 부분에 대해서는 해당 Web API를 사용할 개발자의 판단에 맡긴다.

참고자료

[Google Developers - Abortable fetch]

--

--