fetch의 요청을 강제로 중단하기 (2. axios와 함께 사용)

eeensu·2025년 7월 15일
0

Javascript

목록 보기
38/41

axios와 함께 사용하는 법?

AbortController는 비동기 호출 라이브러리인 axios와 함께 사용될 수 있다! axios에서 AbortController를 사용하여 요청을 강제로 중단할 수 있으며, 이는 fetch와 유사하다.

axios 에서 제공하는 호출의 종료 방법에는 signalcancelToken 방법이 있었는데, 이중 signalAbortController 를 사용하는 방법이고, cancelToken 은 deprecated되었다.

아래 코드를 통해 살펴보자.

const DataFetcher = () => {
  const [data, setData] = useState(null);  // 서버에서 가져온 데이터를 저장하는 상태
  const [error, setError] = useState(null); // 에러 정보를 저장하는 상태
  const [loading, setLoading] = useState(true); // 로딩 상태

  useEffect(() => {
    const controller = new AbortController(); // AbortController 생성
    const signal = controller.signal; // signal 객체 가져오기

    // axios 요청
    axios
      .get('https://jsonplaceholder.typicode.com/posts', { signal })
      .then(response => {
        setData(response.data); // 데이터 설정
        setLoading(false); // 로딩 상태 종료
      })
      .catch(error => {
        if (error.name === 'CanceledError') {
          console.log('요청이 중단되었습니다.'); // 요청 취소 시 메시지 출력
        } else {
          setError(error); // 기타 에러 설정
          setLoading(false); // 로딩 상태 종료
        }
      });

    // 컴포넌트가 언마운트될 때 요청을 중단하는 클린업 함수
    return () => {
      controller.abort(); // 요청 중단
    };
  }, []); // 빈 의존성 배열로 첫 렌더링 때만 실행

  // 로딩, 에러, 데이터에 따라 화면 출력
  if (loading) return <p>데이터 로딩 중...</p>;
  if (error) return <p>에러 발생: {error.message}</p>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}


axios instance 에서 AbortController 를 정의하기

생각해보면, response 에서 config.abortController를 직접적으로 건드릴 필요는 보통 없다. AbortController 는 주로 요청을 취소하는 역할을 하고, 이 작업은 request 단계에서 진행되기 때문이다. 즉, 이미 취소가 된 후이므로 response에서 필요하지 않다.

import axios from 'axios'

const axiosInstance = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 5000,
})

axiosInstance.interceptors.request.use((config) => {
    const controller = new AbortController()

    config.signal = controller.signal

    // 요청이 취소되었을 때의 처리를 위해, config 객체에 controller를 저장
    config.abortController = controller

    return config
})

axiosInstance.interceptors.response.use(
    (response) => {
        console.log('Response received:', response)
        return response
    },
    (error) => {
        if (axios.isCancel(error)) {
            console.error('Request canceled:', error.message)
        } else {
            console.error('response error:', error)
        }

        return Promise.reject(error)
    }
)


위의 axios instance를 사용하여 호출한 컴포넌트에서의 활용

data를 호출하는 코드만 간단하게 살펴보자.

useEffect(() => {
  if (!url || !method) return

  const fetchData = async () => {
    try {
      const response = await axiosInstance[method]<T>(url)

      setData(response.data)
    } catch (err) {
      if (axios.isCancel(err)) {
        console.log('Request canceled:', err.message)
      } else {
        setError(err as Error)
        console.error(err)
      }
    } finally {
      setLoading(false)
    }
  }

  fetchData()

  return () => {
    if (axiosInstance.defaults.abortController) {
      axiosInstance.defaults.abortController.abort()
    }
  }
}, [method, url])

하지만 위의 방식엔 문제가 있다. 바로 cleanup 부분에서 abort하는 부분인데, 이때 abortaxiosInstance.defaults 는 인스턴스 전역에서 공유되는 속성이다. 그렇기에 이 인스턴스를 통해 취소하면 여러 위치에서 호출된 다른 요청들이 의도치 않게 취소될 수 있거나, 혹은 마지막 요청의 controller만 있을 수 있다.

앞서 request-interceptor 정의한 방식으로는 각 요청마다 독립적인 AbortController가 생성되지만, axiosInstance.defaults에 저장된 단일 abortController를 참조하여 요청을 취소하므로, 여러 요청이 있을 경우 이 abort() 호출로 인해 모든 요청이 취소될 수 있는 것이다.




그렇다면, 각 모든 api 요청에 AbortController 를 한땀한땀 추가해야하는 것일까?
이에 대한 대안은, 취소 요청에 대해 공통화된 유틸 기능을 만들면 좋을 것 같다는 생각이 들었다.

import { useEffect, useRef, useState } from 'react'
import axios, { AxiosRequestConfig } from 'axios'

const axiosInstance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
})

const useCancelableAxios<T = any>() {
  const controllerRef = useRef<AbortController | null>(null)
  const [data, setData] = useState<T | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const [loading, setLoading] = useState(false)

  const request = async (
    config: AxiosRequestConfig,
  ): Promise<void> => {
    controllerRef.current?.abort()
    const controller = new AbortController()
    controllerRef.current = controller

    setLoading(true)
    setError(null)

    try {
      const response = await axiosInstance.request<T>({
        ...config,
        signal: controller.signal,
      })

      setData(response.data)
    } catch (err: any) {
      if (axios.isCancel(err)) {
        console.log('Request canceled:', err.message)
      } else {
        setError(err)
      }
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    return () => {
      controllerRef.current?.abort()
    }
  }, [])

  return { data, error, loading, request }
}

요청 취소ㅓ 기능이 내장된 커스텀 axios hook이다. 이를 사용하려면 아래와 같다.

const { data, loading, error, request } = useCancelableAxios<MyData>()

useEffect(() => {
  request({ method: 'GET', url: '/my-endpoint' })
}, [])

앞으로, 모든 axios 요청에 이 훅의 request 를 사용하면, 요청에 관한 상태 로직이 훅이 공통으로 모여있으며, 언마운트시 AbortController 에 의해 요청이 자동으로 취소되고, 반복적인 코드도 제거된다.

profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글