저번 포스팅에서 retryFetch라는 메서드를 만들어서 요청을 보내고, response의 status를 확인해 토큰을 재발급했다.
const retryFetch = async (
url: string,
options: RequestInit
): Promise<Response> => {
const response = await fetch(url, options);
if (response.status === 401) {
// status === 401 : Unauthorized 토큰 갱신
const refreshResponse = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/auth/refresh-token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
refreshToken: localStorage.getItem("refreshToken"),
}),
}
);
if (refreshResponse.ok) {
const { accessToken, refreshToken } = await refreshResponse.json();
// localStorage에 갱신된 토큰 저장
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
// requset 재요청
const newOptions = {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${accessToken}`,
},
};
return await fetch(url, newOptions);
} else {
throw new Error("토큰 갱신 실패. 다시 로그인하세요.");
}
}
return response;
};
이 방법은 일단 서버에 request를 보내고 response로 401 status가 내려올 때 같은 요청을 다시 서버로 보낸다.
즉, reponse를 모두 받아야 한다는 것이다. 만약 아래와 같은 경우가 있다면 요청이 실패했음에도 오랜 시간이 걸려서 비효율적일 것이다.
- 실패한 요청이라도 서버에서 응답 데이터를 모두 전송하는 경우
- 실패한 요청에 대해 처리하는 코드가 있는 경우
이런 문제를 해결하기 위해서는 어떻게 해야할까?
생각해 보면 답은 매우 간단했다. 서버에서 응답이 돌아올 때 데이터를 모두 받지 않고, status만 확인해서 실패하면 요청을 취소하거나 다시 요청하는 것이다.
찾아보니 fetch에는 응답을 미리 확인하는 기능이 없다고 한다... 그럼 어떻게 하지?
axios!!!!!!!!!!!
axios는 요청/응답을 가로채는 인터셉터(interceptor) 기능이 내장되어 있다고 한다.
이를 통해 요청이나 응답 자체를 취소하거나 특정 상태 코드에 따라 에러를 처리하는 것이 가능하다.
사용법
// 요청 인터셉터
axios.interceptors.request.use((config) => {
// 요청이 전달되기 전, 요청(config)에 대한 설정 작업
return config;
});
// 응답 인터셉터
axios.interceptors.response.use(
// 정상 응답 처리 (이 작업 이후 .then()으로 이어진다)
(response) => {
return response;
},
// 에러 처리 (이 작업 이후 .catch()로 이어진다)
async (error) => {
// 응답이 error일 때 처리할 작업
return Promise.reject(error);
}
);
retryFetch를 axios.interceptor로 수정해 보자.
1. axios instance 생성
import axios from "axios";
// Axios 인스턴스 생성
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_SERVER_URL,
headers: {
"Content-Type": "application/json",
},
});
2. 요청/응답 인터셉터(interceptop) 생성
// 요청 인터셉터
axiosInstance.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 응답 인터셉터
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (
error.response &&
error.response.status === 401 &&
!originalRequest._retry
) {
originalRequest._retry = true; // 중복 요청 방지 플래그
try {
// 토큰 갱신 요청
const refreshResponse = await axios.post(
`${process.env.NEXT_PUBLIC_SERVER_URL}/auth/refresh-token`,
{
refreshToken: localStorage.getItem("refreshToken"),
},
{
headers: {
"Content-Type": "application/json",
},
}
);
if (refreshResponse.status === 200) {
const { accessToken, refreshToken } = refreshResponse.data;
// 갱신된 토큰 저장
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
// 갱신된 토큰으로 요청 헤더 업데이트
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
// 원래 요청 재시도
return axiosInstance(originalRequest);
}
} catch (refreshError) {
console.error("토큰 갱신 실패:", refreshError);
// 갱신 실패 시 사용자에게 로그아웃 요청 등 처리
throw new Error("토큰 갱신 실패. 다시 로그인하세요.");
}
}
return Promise.reject(error);
}
);
3. 기존 retryFetch를 axios로 수정
const uploadImage = async (file: string) => {
const formData = new FormData();
formData.append("image", file);
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_UPLOAD_IMAGE_URL}`,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);
return response.data.url; // 서버에서 반환된 이미지 URL
} catch (error) {
console.error("이미지 업로드 중 오류 발생:", error);
return null;
}
};
const postArticle = async ({ title, content, image }: FormInputInterface) => {
let imageUrl = null;
if (image) {
// 이미지 업로드 후 URL 받기
imageUrl = await uploadImage(image[0]);
}
const data = {
image: imageUrl || "https://example.com/...",
content,
title,
};
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_SERVER_URL}/articles`,
data,
{
headers: {
"Content-Type": "application/json",
},
}
);
return response.data.id; // 게시글 ID 반환
} catch (error) {
console.error("게시글 추가 실패:", error);
throw new Error("게시글 추가 실패");
}
};
const postArticleComment = async ({
id,
content,
}: PostCommentInterface): Promise<ArticleInquiryInterface> => {
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_SERVER_URL}/articles/${id}/comments`,
{ content },
{
headers: {
"Content-Type": "application/json",
},
}
);
return response.data; // 댓글 데이터 반환
} catch (error) {
console.error("댓글 추가 실패:", error);
throw new Error("댓글 추가 실패");
}
};
결과는
status가 201로 떨어지면 토큰을 재발급하도록 잘 동작한다.
axios로 수정 후 생긴 이점을 정리해 봤다.
- 기존 fetch 코드는 요청을 보낼 때마다 설정하던 헤더의 Authorization을 , axios로 수정 후 인터셉터를 통해 자동으로 적용했다. 이를 통해 개별 API 호출에서 반복적으로 토큰 관련 코드를 작성하지 않아 코드가 간결해졌다.
- 서버의 응답을 가로채 status를 확인 후 401이면 토큰 재발급 로직을 실행한다. 즉, 불필요한 서버 데이터를 받을 필요가 없고, 에러 처리를 할 필요가 없어 사용자 경험을 개선한다.
느낀 점
지금까지 프로젝트를 진행하면서 fetch가 더 손에 익어 fetch를 사용했는데, 앞으로는 axios를 사용해야겠다.
axios를 찾아보며 사용해야 하는 이유를 정리해 봤다.
- 인터셉터 기능을 제공하여 토큰 갱신, 에러 처리 등의 로직을 쉽게 관리할 수 있다.
- JSON 자동 직렬화 및 역직렬화를 기본 제공한다. (fetch는 직접 처리 필요)
- axios.create를 통해 instance를 생성해 공통 헤더를 설정하거나 기본값을 지정하기 쉽고, 이를 모든 요청에 자동으로 적용할 수 있다.
- axios는 HTTP 상태 코드에 따라 에러를 자동으로 처리하고, 오류 응답 본문까지 쉽게 접근 가능하다. (fetch는 항상 요청을 성공을 간주해 response.status === 200 같은 상태 코드 확인 필요)
- axios는 기본적으로 polyfill 없이 구형 브라우저에서도 더 나은 호환성을 제공한다.
'프로젝트' 카테고리의 다른 글
[JWT] Refresh Token 적용 - 401 Unauthorized 해결 (4) | 2024.12.08 |
---|---|
[Fandom-K] SCSS 추가 및 기초 설정 (0) | 2024.10.26 |
[Fandom-K] 프로젝트 생성 & 초기 설정 (0) | 2024.10.25 |