이번에 새로 프로젝트를 하나 진행하면서 Axios의 모듈화가 프로젝트 전반에 큰 영향을 미친다는 것을 알게되었다.
프로젝트의 규모가 크기도 했고, 사용됐던 API의 개수가 60개 정도였기 때문에, Axios 사용에 있어서 모듈화는 필수였다.
비록 API의 개수가 적거나, 프로젝트의 규모가 작다고 해도, Axios의 모듈화는 앞으로 진행될 수도 있는 사이드 프로젝트나 개인 프로젝트에 있어서 필수적으로 도입해야겠다라고 생각했기 때문에, 다시 한 번 정리하면서, 이 글을 읽는 다른 개발자 분들에게도 도움이 됐으면 하는 바람이다.
Axios란?
Axios는 HTTP 비동기 통신을 위한 라이브러리이다.
쉽게 말해 프론트엔드와 백엔드 간의 원활한 통신을 위해 사용된다.
기존에 Axios를 사용했던 방법
Axios 모듈화를 도입하기 전 기존에 내가 Axios를 사용했던 방법을 보기 위해 이전에 학교 친구들과 진행했었던 프로젝트의 일부 코드를 가져왔다.
import axios from "axios";
axios.get(`http://localhost:8080/${memberId}`, {
headers: {
Authorization: accessToken,
},
})
.then((res) => {
console.log(res.data);
})
.catch((err) => console.log(err.response));
위의 코드는 axios를 사용하여 API 주소인 url과 헤더를 config로 추가하여 요청을 보내고 있다.
성공 핸들링을 then 내에서, 에러 핸들링을 catch 내에서 진행한다.
만약 해당 프로젝트에도 60개 이상의 API가 존재한다고 가정했을경우 API 요청을 할 때 마다 반복적으로 axios 인스턴스를 생성하여 API url과 config로 헤더를 넣어주어야하고, 헤더에 포함한 accessToken 같은 경우에는 만료가 되기 때문에 에러 핸들링 부분에서 반복적으로 accessToken을 재발급하는 로직이 포함되어야 한다. 딱봐도 비효율적이다.
Axios instance
Axios instance를 통해 직접 config를 지정한다면, 반복적인 코드를 줄일 수 있다.
import { CONFIG } from '@config';
import axios from 'axios';
const instance = axios.create({
baseURL: CONFIG.API_BASE_URL,
});
위의 코드는 이번에 진행했던 프로젝트에서 사용했던 Axios 인스턴스다. config 파일 내에서 프로젝트 url을 관리하고 있다.
파일 경로는 다음과 같다. apis > _axios > instance.ts
새로운 axios 인스턴스를 생성하면서 config를 지정해 줄 수 있다. baseURL을 프로젝트 url로 지정했기 때문에 생성한 인스턴스를 사용하는 경우 반복적인 url 작성을 피할 수 있게 됐다.
Axios interceptor
then과 catch로 Promise 응답이 처리되기 전에 interceptor로 특정 로직을 실행시킬 수 있다.
instance.interceptors.request.use
요청이 수행되기 전에 특정 로직을 수행한다.
instance.interceptors.request.use(
(config) => {
const token = getToken();
config.headers = {
...config.headers,
Authorization: token.accessToken,
};
return config;
},
(error) => {
Promise.reject(error);
},
);
위와 같이 요청전에 생성한 인스턴스의 config 설정을 할 수 있다. 이번에 진행한 프로젝트에서는 JWT를 사용하여 대부분의 API 요청에 토큰이 필요했기 때문에 API 요청 전에 config 헤더에 토큰을 담아 주었다. 오류가 발생한 경우에는 요청을 거절하도록 한다. 이렇게 interceptor.request를 통해 요청 수행전 토큰을 담도록해서 기존 axios 인스턴스 config에 반복적으로 헤더에 토큰을 담아주도록 코드를 작성하지 않아도 된다.
instance.interceptors.response.use
요청에 대한 응답을 처리 한다.
const refreshToken = async () => {
const refreshToken = getToken().refreshToken;
const response = await axios.post(`${CONFIG.API_BASE_URL}/members/token`, {
refreshToken,
});
return response.data;
};
instance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const { config: originalRequest, response } = error;
const { status, data } = response;
const isUnAuthError = status === 401;
const isNotFoundError = status === 404;
const isDuplicateError = status === 409;
if (isNotFoundError) {
return Promise.reject(error.response.data);
}
if (isDuplicateError) {
return Promise.reject(error.response.data);
}
if (isUnAuthError) {
if (data?.code === 'TOKEN_INVALID') {
if (CONFIG.ENV === 'development') {
alert('세션이 만료되었습니다. 다시 로그인해 주시기 바랍니다.');
window.location.href = `${CONFIG.LOCAL}/auth/login`;
} else if (CONFIG.ENV === 'production') {
alert('세션이 만료되었습니다. 다시 로그인해 주시기 바랍니다.');
window.location.href = `${CONFIG.DOMAIN}/auth/login`;
}
return;
}
if (data?.code === 'TOKEN_EXPIRED') {
const { accessToken } = await refreshToken();
setToken({
accessToken,
refreshToken: getToken().refreshToken,
roles: getToken().roles,
});
return instance.request(originalRequest);
}
return Promise.reject(error.response.data);
}
},
);
정상적인 2xx 범위의 응답이라면 response를 리턴하도록하고, 에러가 발생한 경우에는 해당 에러 status와 code에 맞게 에러를 핸들링 한다.
위의 코드에서 알 수 있듯이 기존에 API를 설계할 때 상태코드에 따른 로직 설계가 유연하게 이루어질 수 있도록 백엔드 개발자와 함께 상태코드에 관해서 회의를 진행했었다.
status가 권한없음(isUnAuthError)이라면, 토큰이 유효하지 않은 경우(TOKEN_INVALID), 토큰이 만료된 경우(TOKEN_EXPIRED)로 구분해서 에러 핸들링을 한다.
- 토큰이 유효하지 않은 경우(TOKEN_INVAILD)에는 로그인 페이지로 이동시킨다.
- 토큰이 만료된 경우에는 refreshToken() 함수를 통해 리턴받은 새로운 accessToken을 저장하도록 하여 중단된 요청을 발급받은 새로운 토큰으로 재요청한다.
이외의 페이지 찾지 못함(isNotFoundError), 중복 처리(isDuplicateError) 에러는 요청이 거절되도록 처리한다.
instance 사용하기
직접 config를 지정하고 interceptor를 연결한 인스턴스를 사용해보자.
이번에 진행했던 프로젝트에서는 API를 파일별로 클래스 내부에서 관리했다.
아래 코드의 파일 경로는 다음과 같다. apis > auth > authApi.ts
import instance from '@apis/_axios/instance';
export class AuthApi {
...
async getMemberProfile(): Promise<Member> {
const { data } = await instance.get(`/members/me`);
return data;
}
...
}
const authApi = new AuthApi();
export default authApi;
사용자 지정 instance를 통해 config로 넣어준 baseURL을 통해 반복적인 url 작성을 피하게 됐고, config 헤더에 담아주어야하는 토큰을 interceptor에서 담아 요청하도록 했고, API 요청에 대한 응답을 먼저 interceptor에서 처리하도록 했다.
확실히 기존에 axios를 사용하던 방식보다 깔끔해졌을 뿐만 아니라, 유지 보수에도 큰 장점을 가지는 것을 알 수 있다.
'개발' 카테고리의 다른 글
[개발] 카카오 로그인 구현하기 (0) | 2023.03.27 |
---|---|
Yarn-berry + NextJS(typescript) 보일러플레이트 만들기 (0) | 2023.03.05 |
[개발] React-query를 활용한 낙관적 업데이트 (0) | 2023.02.28 |
[개발] React-query를 활용한 데이터 필터링 (0) | 2023.02.25 |
[개발] React-query를 활용한 custom hook(ft. typescript) (0) | 2023.02.22 |