Promise를 취소하는 방법

우동이·2024년 7월 23일
0

JavaScript

목록 보기
4/9
  • 자바스크립트는 Promise를 취소하는 API를 제공하지 않기 때문에 다른 방법으로 결과를 무시하는 방법을 소개합니다.

1. Promise.withResolvers()

  • Promise.withResolver는 2024년에 새로 등장했습니다.
  • 기존에는 Promise를 활용한다면 아래처럼 반드시 Resolve 함수와 Reject 함수를 인자로 받는 Callback 함수를 전달해야 했습니다.
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});
  • 하지만 Promise.withResolver를 이용한다면 아래처럼 Promise객체와 resolve, reject 함수가 포함된 객체를 반환합니다.
const { promise, resolve, reject } = Promise.withResolvers();
  • 아래와 같이 비동기 함수를 전달받아 실행과 취소를 할 수 있는 함수를 만들 수 있습니다.
const buildCancelablePromise = <T>(asyncFn: () => Promise<T>) => {
  let rejected = false;

  const { promise, resolve, reject } = Promise.withResolvers<T>();

  return {
    run: () => {
      if (!rejected) {
        asyncFn().then(resolve, reject);
      }

      return promise;
    },

    cancel: () => {
      rejected = true;
      reject(new Error("CanceledError"));
    },
  };
};
  • 콜백으로 전달한 비동기 함수는 실행되었으나 완료되기 전 미리 cancel를 통해 취소할 수 있습니다.
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));

const newPromise = buildCancelablePromise(async () => {
  await sleep(2000);
  return "resolve";
});

(async () => {
  try {
    const value = await newPromise.run();
    console.log("value: ", value);
  } catch (err) {
    console.log("err: ", err);
  }
})();

setTimeout(() => {
  // 프로미스 작업이 완료되기 전 cancel
  newPromise.cancel();
}, 500);

2. AbortController

  • AbortController는 비동기 요청을 중단할 수 있는 기능을 제공합니다.
  • 이 기능을 활용하여 아래처럼 중간에 요청을 취소할 수 있으며 cancel을 많이 호출해도 abort 이벤트가 한번만 동작한다는 특징을 이용해 최신 데이터를 가져올 때 자주 사용한다고 합니다. ( GET 요청에 용이 )
const buildCancelablePromise = <T>(
  requestFn: (signal: AbortSignal) => Promise<T>
) => {
  const abortController = new AbortController();

  return {
    run: () =>
      // 새로운 Promise를 반환
      new Promise<T>((resolve, reject) => 
        // 이전에 cancel 했으면 에러 발생             
        if (abortController.signal.aborted) {
          reject(new Error("CanceledError"));
          return;
        }
        // 전달 받은 비동기 함수에 signal를 전달하여 API를 감지할 수 있도록 하기
        requestFn(abortController.signal).then(resolve, reject);
      }),

    cancel: () => {
      // 비동기 요청 취소
      abortController.abort();
    },
  };
};

const newPromise = buildCancelablePromise(async (signal) => {
  return fetch("http://localhost:3000", { signal }).then((res) => res.data);
});
(async () => {
  try {
    const val = await newPromise.run();
    console.log("value: ", value);
  } catch (err) {
    console.log("err: ", err);
  }
})();
setTimeout(() => {
  newPromise.cancel();
}, 500);

3. React Hook으로 요청을 순차적으로 처리하는 기능 만들어보기

import { useCallback, useRef } from "react";

function useSequentialRequest<Args extends unknown[], Data>(
  requestFn: (signal: AbortSignal, ...args: Args) => Promise<Data>,
) {
  const requestFnRef = useRef(requestFn);
  const running = useRef(false);
  const abortController = useRef<AbortController | null>(null);

  return useCallback(
    async (...args: Args) => {
      // 이미 진행중인 API 요청이 있다면 이 요청을 취소하고 abortController를 초기화
      if (running.current) {
        abortController.current?.abort();
        abortController.current = null;
      }

      running.current = true;

      // 중복으로 AbortController를 만들지 않기 위해 ref를 이용
      const controller = abortController.current ?? new AbortController();
      abortController.current = controller;

      return requestFnRef.current(controller.signal, ...args).finally(() => {
        // finally를 활용해 API요청이 완료되었을 때 control이 같다면 지금 진행중인 API 요청이라는 것을 검증할 수 있습니다.
        if (controller === abortController.current) {
          running.current = false;
        }
      });
    },
    [requestFnRef],
  );
}
  • 활용하기
import React, { useState } from "react";
import "./App.css";
import { useSequentialRequest } from "./useSequentialRequest";

function App() {
  const [data, setData] = useState("");

  const fetchData = useSequentialRequest(
    async (signal: AbortSignal, query: string) =>
      fetch(`api url?query=${query}`, { signal }).then(
        (res) => res.data()
      )
  );

  const handleInput = async (value: string) => {
    try {
      const res = await fetchData(value);
      
      setData(res);
    } catch {
      console.log("error");
    }
  };

  return (
    <div>
       <input
          onChange={(e) => {
            handleInput(e.target.value);
          }}
        />
    </div>
  );
}

export default App;

참고

profile
아직 나는 취해있을 수 없다...

1개의 댓글

comment-user-thumbnail
2024년 7월 28일

항상 좋은 글 감사합니다 🔥

답글 달기

관련 채용 정보