🧩 구현해야 할 로직
저번 포스팅에서 말한 것처럼 인증이 필요한 api 요청(리뷰 작성, 댓글 달기 등등) 시 accessToken이 만료되었을 경우 토큰을 자동 갱신해서 응답을 받는 로직을 구현해야 한다. 그림으로 좀 더 디테일하게 어떤 과정들을 구현해야 하는지 살펴보자.
위 그림에서 보는 것처럼 토큰 갱신 후 처음에 요청했던 작업을 다시 재요청할 수 있도록 해야 한다. 그리고 당연히 유저는 이 과정을 느낄 수 없어야 한다.
🌱 첫번째 버전: 직접 함수 구현
맨 처음에는 이 로직을 아래와 같이 withTokenRefresh
라는 함수로 직접 구현했다.
// /util/api.ts
// api 함수 타입 정의
type ApiFunction<T, R> = (data: T) => Promise<R>;
/**
- 토큰 만료 시 토큰 갱신 후 실패한 요청 재시도
- @param apiFunction 요청 함수
- @param data 요청 시 사용할 데이터. 요청 시 사용할 데이터가 없을 경우 아무것도 넣지 않아도 된다.
- @returns 요청 함수
*/
export const withTokenRefresh = <T = void, R = void>( // 요청 함수 내의 타입 정의. 각 타입의 기본값을 void로 설정
apiFunction: ApiFunction<T, R>
): ApiFunction<T, R> => {
return async (data: T): Promise<R> => {
try {
return await apiFunction(data);
} catch (error) {
const { response } = error as AxiosError;
// 토큰 만료 시 토큰 갱신
if (response?.status === 401) {
try {
await refreshKakaoAccessToken();
return await apiFunction(data);
} catch (refreshError) {
console.error("토큰 갱신 실패:", refreshError);
throw refreshError;
}
}
throw error; // refreshKakaoAccessToken, apiFunction 모두 실패 시 에러 전파
}
};
};
/**
- 리뷰 업로드 함수
- withCredentials: true 옵션으로 쿠키를 전송해 사용자 인증
- @param formData 전송할 리뷰 정보
*/
export const uploadReview = withTokenRefresh(
async (formData: FormData): Promise<void> => {
const response = await api.post(`/review`, formData, {
withCredentials: true,
headers: {
"Content-Type": "multipart/form-data",
},
});
console.log(response.data);
}
);
하지만 이런 방식은 다음과 같은 문제가 있었다.
- 매번 api 요청 코드를 이 함수로 감싸야 함
- 함수를 정의할 때 인자가 하나만 있다고 정의했기 때문에 여러 개의 데이터를 인자에 넣어야 할 경우 객체로 감싸야 제대로 동작함. 유연함이 부족함.
무엇보다 함수의 초반 부분이 타입 정의로 떡칠이 되어 있어 제대로 작동하긴 해도 개인적으로 매우 마음에 들지 않았다.
🛠️ 리팩토링 버전: Axios 인터셉터 적용하기
그러던 중 Axios의 기능 중 하나인 인터셉터에 대해 알게 되었다. 간단히 말하자면 api 요청, 또는 응답을 받을 시 고정적으로 해야할 작업을 정해놓을 수 있는 기능인데, 이를 통해 재사용성, 유지보수성을 향상시킬 수 있다. 지금 나에게 딱 맞는 기능이라 바로 적용해봤다.
인터셉터 | Axios Docs
인터셉터 then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있습니다. axios.interceptors.request.use(function (config) { return config; }, function (error) { return Promise.reject(error); }); axios.interceptors.response.use(f
axios-http.com
📌 용도에 따라 인스턴스 분리
// axios 인스턴스 생성
// 일반 인스턴스
export const genaralApiClient: AxiosInstance = axios.create({
baseURL: API_URL,
});
/**
- 인증용 인스턴스
- withCredentials: true 옵션으로 쿠키를 전송해 사용자 인증
*/
export const authApiClient: AxiosInstance = axios.create({
baseURL: API_URL,
withCredentials: true,
});
먼저 원래 쓰던 axios 인스턴스를 generalApiClient
, authApiClient
2개로 분리했다. 인증이 필요한 api 요청 시 매번 withCredentials
옵션을 true
로 설정해야 하는 번거로움을 없앨 수 있고, 인증이 필요한 api에 따로 위의 로직을 적용할 수 있기 때문에 이렇게 했다.
📌 authApiClient
에 인터셉터 설정하기
/**
- 카카오 액세스 토큰 갱신 함수
*/
export const refreshKakaoAccessToken = async (): Promise<void> => {
try {
// 인터셉터로 인한 무한 루프 방지를 위해 generalApiClient 사용
const response = await genaralApiClient.post(
`/auth/kakao/refresh`,
{},
{
withCredentials: true,
}
);
console.log(response.data);
} catch (error) {
console.error("카카오 액세스 토큰 갱신 실패:", error);
throw error;
}
};
// 토큰 만료 시 토큰 갱신 후 실패한 요청 재시도
authApiClient.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
if (error.response.status === 401) {
try {
await refreshKakaoAccessToken();
return authApiClient(error.response.config);
} catch (refreshError) {
// refreshToken이 만료된 경우이므로 강제 로그아웃 처리
console.error("토큰 갱신 실패:", refreshError);
alert("다시 로그인해주세요.");
await persistor.purge(); // redux-persist가 관리하는 모든 상태 초기화
window.location.href = "/";
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
그리고 authApiClient
에 토큰 갱신 로직이 적용된 인터셉터를 추가했다. 여기서 accessToken 갱신 요청 함수인 refreshKakaoAccessToken
의 경우 authApiClient
를 사용하면 문제가 발생한다. 코드를 조금만 봐도 알 수 있겠지만, 재수없게 refreshToken
이 없는 상태일 때 인증이 필요한 api 요청을 할 경우 계속해서 authApiClient
의 인터셉터를 작동시켜 다음과 같이 무한 401 에러 루프에 빠진다는 것을 알 수 있다.
이런 끔찍한 결과를 방지하기 위해서 refreshKakaoAccessToken
함수는 generalApiClient
를 사용해 작성했고, 이 때문에 withCredentials
옵션도 따로 설정했다.
🏁 마무리
이렇게 해서 UX를 위한 자동 토큰 갱신 로직 적용까지 마쳤다. 다음 포스팅에서는 카카오 서버에서 가져온 사용자 정보를 redux-toolkit으로 관리하도록 하는 작업과 로그아웃 기능 구현에 대해 다루겠다.
'프로젝트 > Re|view' 카테고리의 다른 글
[Re|view] 7. FSD 구조 적용 실패기 (0) | 2025.01.28 |
---|---|
[Re|view] 6. 카카오 로그인 적용기 - Redux Toolkit으로 사용자 정보 관리하기 & 로그아웃 (1) | 2024.09.04 |
[Re|view] 4. 카카오 로그인 적용기 - 쿠키에 토큰 저장하기 (5) | 2024.09.03 |
[Re|view] 3. 깃허브 unverified 이슈 해결 과정 (0) | 2024.05.10 |
[Re|view] 2. 프로젝트 진행 방식 정립 (0) | 2024.05.10 |