https://axios-http.com/kr/docs/instance
개발을 진행하면서 HTTP 요청을 다루게 되는데, 이때 많은 경우에 반복되는 패턴이나 코드를 발견할 수 있다.
예를 들자면,
이러한 반복적인 코드를 줄이고, 더 효과적으로 코드를 관리하기 위해 Aioxs 인스턴스와 인터셉터를 사용하는 방법에 대해 알아보자.
axios
의 instance는 기본적으로 axios
를 사용할 때 특정 설정을 미리 지정해놓은 별도의 “객체”를 생성해서 사용하는 것을 의미한다.
이렇게 생성된 instance는 axios.create()
메서드를 통해 만들어지며, 공통적으로 사용해야 할 설정 (예: 기본 URL, 헤더, 타임아웃 등)을 적용한 상태에서 API 호출을 할 수 있다.
먼저, 반복적으로 사용되는 Axios 설정을 추상화하기 위해 Axios 인스턴스를 생성한다. Axios 인스턴스를 주어진 설정으로 초기화된 새로운 Axios 요청을 만들어낸다.
이렇게 생성된 인스턴스는 설정이 미리 적용되어 있으므로, 각 요청에서 이 설정을 반복하지 않아도 된다.
import axios, { AxiosInstance } from 'axios';
const API_BASE_URL = "https://some-domain.com/api/";
const createInstance = () => {
const instance = axios.create({
baseURL: API_BASE_URL,
timeout: 2000,
});
return instance;
};
export const clientInstance = createInstance();
위의 코드에서는 기본 URL 및 타임아웃을 설정했다. 기본 URL은 모든 요청의 URL 앞에 추가되며, 타임아웃은 요청을 취소하는 데 걸리는 시간을 설정한다.
https://axios-http.com/kr/docs/interceptors
인터셉터는 요청이나 응답을 처리하기 전에 실행되는 함수다. 이를 이용하면 중복되는 코드를 줄일 수 있으며, 요청 또는 응답을 더 세밀하게 제어할 수 있다.
then
과 catch
로 넘어가기 전)에 인터셉터로 가로채서 원하는 작업들을 추가할 수 있다.const setInterceptors = (instance: AxiosInstance) => {
instance.interceptors.response.use(
(response) => response,
(error) => {
console.log('interceptor > error', error);
return Promise.reject(error);
}
);
instance.interceptors.request.use(
(config) => {
const token = '???'; // 토큰 주입 (프로젝트 정책마다 다름)
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error: AxiosError) => {
console.log('interceptor > error', error);
Promise.reject(error);
}
);
};
const createInstance = () => {
const instance = axios.create({
baseURL: API_BASE_URL,
timeout: 2000,
});
setInterceptors(instance);
return instance;
};
위의 코드에서는 요청 인터셉터와 응답 인터셉터를 설정했다.
intercepter를 활용해 요청을 가로채서 요청을 하기 전 header에 token(access, refresh) 를 담아서 api 요청을 보낸다.
import type {
AxiosInstance,
AxiosRequestConfig,
InternalAxiosRequestConfig,
} from 'axios';
import axios from 'axios';
import { authStorage } from '../../utils/storage/authStorage';
import { ERROR_STATUS } from '@/shared';
import { QueryClient } from '@tanstack/react-query';
const initInstance = (config: AxiosRequestConfig): AxiosInstance => {
const instance = axios.create({
timeout: 5000,
...config,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Cross-Control-Allow-Origin': '*',
...config.headers,
},
});
return instance;
};
export const BASE_URI = `https://sinitto.site`;
export const fetchInstance = initInstance({
baseURL: BASE_URI,
});
로그인 , 회원가입 시 localStorage에 accessToken과 refreshToken 보관.
accessToken
refreshToken
fetchInstance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = authStorage.accessToken.get();
if (accessToken !== undefined) {
config.headers['Content-Type'] = 'application/json';
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
fetchInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (
error.response?.status === ERROR_STATUS.ACCESS_TOKEN_EXPIRATION &&
!originalRequest._retry
) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
return Promise.reject(error);
}
const resp = await fetch(`${BASE_URI}/api/auth/refresh`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Cross-Control-Allow-Origin': '*',
Authorization: `Bearer ${refreshToken}`,
},
body: JSON.stringify({ refreshToken }),
});
if (resp.ok) {
console.log('토큰 재발급 성공');
const data = await resp.json();
authStorage.accessToken.set(data.accessToken);
authStorage.refreshToken.set(data.refreshToken);
return fetchInstance(originalRequest);
} else if (
resp.status === ERROR_STATUS.REFRESH_TOKEN_EXPIRATION ||
resp.status === ERROR_STATUS.INVALID_REFRESH_TOKEN
) {
console.log('토큰 재발급 실패');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
return Promise.reject(error);
}
return Promise.reject(error);
}
);
작성한 instance에 interceptor를 활용해 header에 token을 담았기 때문에 api 호출만을 위한 코드를 작성한다.
import { fetchInstance } from '@/shared';
export type AcceptedCallBackListResponse = {
callbackId: number;
seniorName: string;
postTime: string;
seniorId: number;
status: 'WAITING' | 'IN_PROGRESS' | 'COMPLETE' | 'PENDING_COMPLETE';
};
export const getAcceptedCallBackListPath = '/api/callbacks/sinitto/accepted';
export const getAcceptedCallBackList =
async (): Promise<AcceptedCallBackListResponse> => {
const response = await fetchInstance.get<AcceptedCallBackListResponse>(
getAcceptedCallBackListPath
);
return response.data;
};
useQuery
의 캐싱을 활용하기 위해useMutation
의 onSuccess, onError 등의 속성을 활용하기 위해이러한 이점을 활용하기 위해 react-query 라이브러리를 활용한 hook을 만들어서 사용한다.
import { getAcceptedCallBackList } from '../apis';
import { getAcceptedCallBackListPath } from '../apis/accepted-call-back-list.api';
import { AcceptedCallBackListResponse } from '../types';
import { useQuery } from '@tanstack/react-query';
const getAcceptedCallBackListQueryKey = [getAcceptedCallBackListPath];
export const useGetAcceptedCallBackList = () => {
return useQuery<AcceptedCallBackListResponse>({
queryKey: getAcceptedCallBackListQueryKey,
queryFn: () => getAcceptedCallBackList(),
});
};
언젠간 쿠키도 사용해야겠지...?