발단:
웹 개발을 하면서 계속해서 사용하게 될 axios 라이브러리에 대해, 효율적으로 모듈화하여 사용하는 방법을 제대로 이해하고 싶었다.
내용:
기존에 사용하던 방식
- src/api/index.js 파일에 path를 객체로 저장함
- 서버로 요청을 보낼 각 컴포넌트에서 axios를 임포트해서 try catch문으로 사용함
// src/api/index.js
const URL = 'http://localhost:8080/api';
const USERS = '/user';
const DIARY = '/diary';
const api = {
user: {
signUp: () => URL + USERS + '/sign-up',
findUserId: (email) => URL + USERS + `/find-id/${email}`,
},
diary: {
postDiary: () => URL + DIARY,
deleteDiary: (diaryId) => URL + DIARY + `/${diaryId}`,
},
};
export default api;
// api 요청을 보낼 곳
import axios from 'axios';
import api from '../../../api/api';
// 일기장 삭제 요청
const fetchDeleteDiary = async () => {
try {
await axios.delete(api.diary.getDiaryInfo(diaryId));
navigate('/diary', { state: { filter: false } });
} catch (err) {
console.error(err);
}
};
내가 하고 싶었던 것
- axios instance를 사용하고 싶었음
- try catch문을 매번 쓰고 싶지 않았음
구현과정
1단계
일단 src/apis/index.js 에 axios instance를 생성했다.
만들려는 서비스 중 일부 페이지는 로그인 하지 않은 유저도 접근 가능하기 때문에, 구분을 위해 defaultInstance와 authInstance로 나누어 만들었다. (아직 로그인 구현 전이라 나중을 위해 일단 구분해둠)
// src/apis/index.js
import axios from "axios";
const BASE_URL = 'http://localhost:8080/api'
const accessToken = "test";
// accessToken이 필요 없는 요청시
const axiosApi = (url, options) => {
const instance = axios.create({ baseURL: url, ...options });
return instance;
};
// accessToken이 필요한 요청시
const axiosAuthApi = (url, options) => {
const instance = axios.create({
baseURL: url,
headers: { Authorization: accessToken },
...options,
});
return instance;
};
export const defaultInstance = axiosApi(BASE_URL);
export const authInstance = axiosAuthApi(BASE_URL);
src/apis/api.js 파일에 기존 방식대로 path들을 모아주었다. 기존과 차이점은 base url을 매번 적지 않아도 되게 되었다는 것.
// src/apis/api.js
const USERS = "/member";
const api = {
user: {
login: (type) => `/auth/login/${type}`,
resister: () => USERS + "/resister",
},
};
export default api;
axios 요청은 만든 인스턴스를 임포트한 뒤 try catch문으로 사용하게 되었다.
// api 요청을 보내는 곳
import { defaultInstance } from "../../../apis";
import api from "../../../apis/api";
const kakaoLogin = async (code) => {
try {
const res = await defaultInstance.get(api.user.login("kakao"), {
params: { code: code },
});
if (res.data.accessToken) navigate("/");
else
navigate("/signup", {
state: { email: res.data.email, type: res.data.type },
});
} catch (err) {
console.error(err);
}
인스턴스를 사용한다는 것 말고는 바뀐 건 없다.
2단계
만든 axios instance에 interceptor를 적용했다.
interceptor는 공식문서에 아래와 같이 설명되어 있다.
then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있습니다.
// 요청 인터셉터 추가하기
axios.interceptors.request.use(function (config) {
// 요청이 전달되기 전에 작업 수행
return config;
}, function (error) {
// 요청 오류가 있는 작업 수행
return Promise.reject(error);
});
// 응답 인터셉터 추가하기
axios.interceptors.response.use(function (response) {
// 2xx 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
// 응답 데이터가 있는 작업 수행
return response;
}, function (error) {
// 2xx 외의 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
// 응답 오류가 있는 작업 수행
return Promise.reject(error);
});
즉 request 또는 response 요청 전에 intercept해서 먼저 실행시킬 코드를 작성할 수 있다.
즉 try catch문을 대신할 수 있다!
만든 axios instance를 리턴하기 전에 interceptor를 걸어서 response.data를 콘솔에 출력 후 리턴하고, 에러 발생시 에러를 콘솔에 찍도록 했다.
response가 아니라 response.data를 리턴한 건 매번 .data 치기 귀찮아서...ㅎㅎ
// src/apis/index.js
// accessToken이 필요 없는 요청시
const axiosApi = (url, options) => {
const instance = axios.create({ baseURL: url, ...options });
// 성공시 콘솔에 response.data 출력
// 에러시 콘솔에 에러 출력
instance.interceptors.response.use(
(response) => {
console.log(response.data);
return response.data;
},
(error) => {
console.error(error);
}
);
return instance;
};
그럼 실제 요청하는 부분의 코드는 이렇게 바뀐다.
// api 요청을 보내는 곳
import { defaultInstance } from "../../../apis";
import api from "../../../apis/api";
const kakaoLogin = async (code) => {
const res = await defaultInstance.get(api.user.login("kakao"), {
params: { code: code },
});
if (res.accessToken) navigate("/");
else
navigate("/signup", {
state: { email: res.email, type: res.type },
});
}
3단계
그런데 이렇게 해서 프론트 팀원에게 설명을 해 주려니까, try catch문이 없어진 건 매우 좋은데 매번 defaultInstance를 임포트해서 사용해야 하나 싶어졌다. 어차피 api path 때문에 api.js를 임포트할 건데 그냥 거기서 요청을 보내면 안 되려나?
그래서 src/apis/api.js에서 모든 axios 요청을 보낼 수 있도록 바꿨다.
// src/apis/api.js
import { defaultInstance } from ".";
const api = {
user: {
login: (type, params) => defaultInstance.get(`/auth/login/${type}`, params),
},
};
export default api;
// api 요청을 보내는 곳
import api from "../../../apis/api";
const kakaoLogin = async (code) => {
const res = await api.user.login("kakao", { params: { code: code } });
if (res.accessToken) navigate("/");
else
navigate("/signup", {
state: { email: res.email, type: res.type },
});
};
아주 간단해졌다! 마음에 든다!
이제 로그인 구현을 하면서 defaultInstance랑 authInstance에서 동일하게 작성될 코드들을 어떻게 하나로 합칠 수 있을지 생각해 보고, 마음에 들게 만들어 봐야겠다.
끝.