Axios Interceptor로 Access Token 자동 갱신하기 (Refresh Token + Queue 처리)

짜장킴·2026년 1월 28일

실무

목록 보기
6/8

request 인터셉터

  1. 토큰을 어디에 넣을지
  2. 헤더 형식
    Request Interceptor: Access Token 자동 첨부

Access Token은 SessionStorage에 저장해두고, 모든 요청에 자동으로 붙인다.

api.interceptors.request.use(
  (config) => {
    if (typeof window !== "undefined") {
      const accessToken = SessionStorage.getItem("accessToken");

      if (accessToken) {
        config.headers.Authorization = `Bearer ${accessToken}`;
      }
    }

    return config;
  },
  (error) => Promise.reject(error),
);

response 인터셉터

  • “언제 재발급할지” 규칙만 맞추면 됨
  1. 어떤 상태 코드에서 refresh 할지
  2. refresh API가 어떤 응답을 주는지
  3. refresh 실패 시 어떻게 처리할지

Access Token이 만료되면 보통 401 Unauthorized 가 떨어진다.
이때 refresh API를 호출해서 새 access token을 받아오고, 실패했던 요청을 다시 실행한다.
주의: 동시에 여러 요청이 401로 터질 수 있으므로, refresh 요청을 한 번만 보내고 나머지는 대기시켜야 한다. (큐 처리)
별도 인스턴스 만드는거 권장 -> url이 달라도 같은 axios 인스턴스 탈 수 있기 때문에

  • isRefreshing : 현재 refresh 요청이 진행 중인지 여부
  • failedQueue : refresh가 끝날 때까지 대기할 요청들의 Promise resolve/reject 모음
type FailedQueueItem = {
  resolve: (token: string) => void;
  reject: (err: unknown) => void;
};

let isRefreshing = false;
let failedQueue: FailedQueueItem[] = [];

const processQueue = (error: unknown, token: string | null) => {
  failedQueue.forEach((p) => (token ? p.resolve(token) : p.reject(error)));
  failedQueue = [];
};

api.interceptors.response.use(
  (res) => res,
  async (error) => {
    const originalRequest = error.config as any;

    // 1) 토큰 만료 조건: 보통 401
    const isExpired = error.response?.status === 401;

    // 이미 retry한 요청이면 무한루프 방지
    if (!isExpired || originalRequest._retry) {
      return Promise.reject(error);
    }

    // 2) 이미 refresh 중이면 큐에 대기
    if (isRefreshing) {
      return new Promise((resolve, reject) => {
        failedQueue.push({
          resolve: (newToken) => {
            originalRequest.headers.Authorization = `Bearer ${newToken}`;
            resolve(api(originalRequest));
          },
          reject,
        });
      });
    }

    originalRequest._retry = true;
    isRefreshing = true;

    try {
      // 3) 여기서 /token/access 호출 = refresh api 호출
      // refresh token은 httpOnly 쿠키로 자동 전송됨
      const res = await refreshApi.post("/token/access");

      // 4) 서버 응답 구조에 맞게 accessToken 추출
      // { data: { accessToken: "..." } } 또는 { data: { data: { accessToken } } } 등
      const newAccessToken = res.data?.accessToken;

      if (!newAccessToken)
        throw new Error("No accessToken in refresh response");

      // 5) 새 토큰 저장
      sessionStorage.setItem("accessToken", newAccessToken);

      // 대기 중 요청들 처리
      processQueue(null, newAccessToken);

      // 6) refresh를 수행한 요청(A) 재시도
      originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
      return api(originalRequest);
    } catch (refreshError) {
      // refresh 실패 → 모든 요청 실패 처리 + 로그아웃
      processQueue(refreshError, null);
      sessionStorage.removeItem("accessToken");
      window.location.href = "/login";
      return Promise.reject(refreshError);
    } finally {
      isRefreshing = false;
    }
  },
);

실행 과정

여러 API 요청(A, B, C)이 동시에 나갔는데
accessToken이 만료된 상태로 가정

1️⃣ A 요청

  • A → 401
  • isRefreshing = false
  • isRefreshing = true로 변경
  • /token/access 호출 (refresh 시작)

2️⃣ B 요청

  • B → 401
  • 이미 isRefreshing === true
  • 아래 코드로 진입
if (isRefreshing) {
  return new Promise((resolve, reject) => {
    failedQueue.push({
      resolve: (newToken) => {
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        resolve(api(originalRequest)); // B 재요청 예약
      },
      reject,
    });
  });
}
  • 새로운 Promise를 생성
  • resolve / reject를 큐에 저장
  • B는 Promise pending 상태로 대기

3️⃣ refresh 성공 (A 흐름 복귀)

processQueue(null, newAccessToken);

const processQueue = (error: unknown, token: string | null) => {
  failedQueue.forEach((p) => (token ? p.resolve(token) : p.reject(error)));
  failedQueue = [];
};
  • 큐에 쌓인 B, C 요청들의 resolve(newToken) 실행
  • 내부에서:
    - Authorization 헤더를 새 토큰으로 교체
    - api(originalRequest) 실행 → B 재요청

전체 흐름 요약

1. A 요청 → 401
2. isRefreshing = true
3. /token/access 호출

   (그 사이)

4. B 요청 → 401
5. failedQueue에 B의 재요청 함수 push
6. B는 Promise pending 상태로 대기

   (refresh 성공)

7. 새 토큰 저장
8. processQueue 실행
   → B의 resolve(newToken) 실행
   → B 재요청 시작
9. A도 자기 자신 재요청
10. isRefreshing = false
profile
프론트엔드 취준생입니다.

0개의 댓글