프로젝트

[Axios] interceptors 적용 - 토큰 재발급

dev-hpk 2024. 12. 10. 15:08

저번 포스팅에서 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 없이 구형 브라우저에서도 더 나은 호환성을 제공한다.