AbortController
는 비동기 호출 라이브러리인 axios
와 함께 사용될 수 있다! axios
에서 AbortController
를 사용하여 요청을 강제로 중단할 수 있으며, 이는 fetch
와 유사하다.
axios
에서 제공하는 호출의 종료 방법에는 signal
과 cancelToken
방법이 있었는데, 이중 signal
이 AbortController
를 사용하는 방법이고, 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>;
}
생각해보면, 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)
}
)
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
하는 부분인데, 이때 abort
를 axiosInstance.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
에 의해 요청이 자동으로 취소되고, 반복적인 코드도 제거된다.