본문 바로가기

Study

웹 요청 중단 시키기 (ft. AbortController)

최근 QA 테스트 진행 중, QA 팀원분의 다음과 같은 요청이 있었다.

api를 사용하는 컴포넌트의 네트워크 실패/지연 케이스를 재현해보고 싶은데 방법이 없을까요?

로컬 환경에서는 이를 쉽게 재현할 수 있겠지만, 개발 내용을 공유하기 위해선 개발 서버로 배포가 필요했고 특정 api만을 원할때마다 실패 혹은 지연을 일으킬 수 있는 방법이 떠오르지 않아 당시엔 '그건 좀 힘들 것 같아요 ㅜㅜ' 라고 답변을 드렸다.

이후, 같은 질문을 받는다면 좀 더 괜찮은 답변을 드리고 싶었고 몇가지 방법을 찾아냈다.

 

  • Chrome 개발자 도구 > 네트워크 탭 > fetch/XHR 탭 > 원하는 api 요청 block 

  • Chorme 개발자 도구 > 설정 > 제한 > 커스텀 네트워크 속도/지연 프로필 설정 (네트워크 탭에서 제공하는 preset 네트워크 설정인 slow 3g, fast 3g가 충분히 느리지 않다고 생각들때 사용)

 

위 방법 외에도 javascript 에서도 웹 요청을 programatically 하게 중단시킬 수 있는 AbortController 라는 interface를 사용하는 방법이 있다.

Interface

interface AbortController {
  constructor();

  // AbortSignal 타입의 signal 객체
  [SameObject] readonly attribute AbortSignal signal; 

  // signal과 연관된 DOM 요청을 취소시킨다
  // reason이 undefined 라면 DOMException 타입의 "AbortError" 로 초기화
  undefined abort(optional any reason); 
};
// EventTarget interface를 상속한다
interface AbortSignal : EventTarget {

  // 이미 aborted 된 signal을 반환한다.
  [NewObject] static AbortSignal abort(optional any reason);
  // 매개변수로 넘겨준 시간 후에 abort 가 발생하는 AbortSignal 객체를 반환한다
  [Exposed=(Window,Worker), NewObject] static AbortSignal timeout([EnforceRange] unsigned long long milliseconds);

  // aborted 유무 판단 flag
  readonly attribute boolean aborted;
  // signal's abort reason
  readonly attribute any reason;
  // signal이 aborted 됐으면 reason을 throw 한다.
  undefined throwIfAborted();

  // event type이 abort인 onabort event handler를 가진다
  attribute EventHandler onabort;
};

Usage

fetch/axios 중단 시키기 (axios는 v0.22 부터 지원가능하다고 한다)

// ref: https://developer.mozilla.org/ko/docs/Web/API/AbortSignal

const controller = new AbortController();
const signal = controller.signal;

const downloadBtn = document.querySelector('.download');
const abortBtn = document.querySelector('.abort');

downloadBtn.addEventListener('click', fetchVideo);

abortBtn.addEventListener('click', function() {
  controller.abort();
  console.log('Download aborted');
});

function fetchVideo() {
  ...
  // with fetch
  fetch(url, {signal}).then(function(response) {
    ...
  }).catch(function(e) {
    reports.textContent = 'Download error: ' + e.message;
  })
  ...
  // with axios
  axios.get(url, {signal}).then(function(response) {
  	...
  });
  ...
}

timeout 발생시키는 signal 생성 (꽤 최신 브라우저들만 이를 지원한다)

// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout

try {
  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
  const result = await res.blob();
  // …
} catch (err) {
  if (err.name === "TimeoutError") {
    console.error("Timeout: It took more than 5 seconds to get the result!");
  } else if (err.name === "AbortError") {
    console.error("Fetch aborted by user action (browser stop button, closing tab, etc.");
  } else if (err.name === "TypeError") {
    console.error("AbortSignal.timeout() method is not supported");
  } else {
    // A network error, or some other problem.
    console.error(`Error: type: ${err.name}, message: ${err.message}`);
  }
}

AbortSignal interface를 signal로 받는 요청에도 사용 가능하다.

fs readFile 메서드 abort 하기

// ref: https://blog.logrocket.com/complete-guide-abortcontroller-node-js/

const fs = require("node:fs");

const controller = new AbortController();
const { signal } = controller;

fs.readFile("data.txt", { signal, encoding: "utf8" }, (error, data) => {
  if (error) {
    if (error.name === "AbortError") {
      console.log("Read file process aborted");
    } else {
      console.error(error);
    }
    return;
  }
  console.log(data);
});

controller.abort();

AbortSignal interface는 내부적으로 event type이 abort인 onabort event handler를 가지고 있기 때문에

abortable 한 api를 커스텀하게 만들수도 있다.

// ref: https://blog.logrocket.com/complete-guide-abortcontroller-node-js/

const customAbortableApi = (options: {signal?: AbortSignal}) => {
  const { signal } = options;

  if (signal?.aborted === true) {
    throw new Error(signal.reason);
  }

  const abortEventListener = () => {
    // Abort API from here
  };
  
  if (signal) {
    signal.addEventListener("abort", abortEventListener, { once: true });
  }
  try {
    // Run some asynchronous code
    if (signal?.aborted === true) {
      throw new Error(signal.reason);
    }
    // Run more asynchronous code
  } finally {
    if (signal) {
      signal.removeEventListener("abort", abortEventListener);
    }
  }
};

 

참고

https://developer.mozilla.org/ko/docs/Web/API/AbortSignal

https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout

https://dom.spec.whatwg.org/#interface-abortcontroller

https://stackoverflow.com/questions/38329209/how-to-cancel-abort-ajax-request-in-axios

https://blog.logrocket.com/complete-guide-abortcontroller-node-js/