Programming/Client

여러 번 호출 가능한 abortController 만들기 (feat. useRef)

Jiwoo 2024. 6. 7. 17:12

문제 상황

대량 청구서를 발송 중에 취소하는 로직이 필요했습니다.
그래서 fetch 비동기 통신을 취소하는 abortController를 사용했습니다.


🚨 주의사항
abortController는 프론트의 요청만 취소하는 것임을 알아야 합니다.
요청 이후에 백엔드에서 일어나는 일은 제어할 수 없습니다.
예를 들어 청구서 발송을 클릭하고, 곧바로 취소를 눌러서 요청을 취소했다하더라도 백엔드에서는 발송이 시작된 상태이고 이것을 취소할 수는 없습니다.
저는 위와 같은 이유로 해당 로직을 작성했지만, 사용하지는 못했습니다 :(

해결 방법

메일을 발송하는 로직을 훅으로 만들어서 사용하는 컴포넌트에서 호출하도록 했습니다.

import AbortController from "abort-controller";

export function useSendInvoice(emails: React.Key[]) {
  // sendInvoice 함수 실행 시 새로 생성하나 리렌더링은 막기 위해 useRef 사용
  // 새로 생성하는 이유는 한 번 취소하면 계속 {aborted: true} (취소상태)로 호출되기 때문임
  const abortControllerRef: MutableRefObject<AbortController | null> = useRef(null);
  const url = `${process.env.NEXT_PUBLIC_API_ENDPOINT}/billing/invoice/send/mail`;

  async function sendInvoice() {
    abortControllerRef.current = new AbortController();

    try {
      const res = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${localStorage.getItem("loginToken")}`,
        },
        body: JSON.stringify({ emails }),
        signal: abortControllerRef.current.signal as AbortSignal,
      });

      const { resultCode, result } = await res.json();

      if (res.status !== 200 || resultCode !== "SUCCESS") {
        return { success: false, errorType: "unknown error" };
      }
      return { success: true, data: result };
    } catch (err) {
      const errorObj = err as Error;
      if (errorObj.name === "AbortError") {
        return { success: false, errorType: "aborted" };
      }
      return { success: false, errorType: "unknown error" };
    }
  }

  // fetch 요청만 중간에 취소되고 메일은 다 발송되서 사용 안 함
  function cancelSend() {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
  }

  return { sendInvoice, cancelSend };
}

useRef를 사용한 이유
취소한 이후에 sendInvoice를 호출하면 {aborted: true}(취소상태)로 호출되는 문제가 발생했습니다.
useRef를 사용하여 sendInvoice를 호출할 때마다 abortControllerRef.current sendInvoice를 새로 생성하나, 리렌더링은 되지 않도록 처리했습니다.