yja

[Offnal] interceptor pattern을 통한 토큰 재발급 본문

[App] React Native/Study

[Offnal] interceptor pattern을 통한 토큰 재발급

유진진 2025. 10. 12. 16:02

interceptor pattern을 통한 토큰 재발급은, 

API 요청이 실패했을 때 그 요청이나 응답을 가로채서 자동으로 새 토큰을 발급하고 재요청하는 것을 말한다. 

 

 

Axios 에서 interceptor 설정하기 

성공한 응답(response)은 그대로 반환하고, 실패한 응답(error)에 대해 토큰 재발급 로직을 실행한다. 

axios.interceptors.response.use(
  response => response,
  error => {
    // 응답 에러가 발생했을 때 여기서 처리
  }
)

 

 

 

토큰 재발급 로직 

사용자가 로그인을 하면 Access Token과 Refresh Token을 동시에 받는다. 

그리고 다음 API 요청을 할 때 헤더에 Access Token을 넣어 보내서 사용자를 인증하고 해당 API를 처리한다. 

만료 시간이 짧은 Access Token 으로, 만료되면 401 unanthorized 오류가 발생한다.

이 오류가 발생하면 interceptor가 가로채서 Refresh Token으로 새로운 Access Token을 자동 발급할 수 있다. 

 

Refresh Token 은 만료 기간이 길지만 언젠가는 만료된다. 이때는 서버가 새로운 Refresh Token을 함께 응답해준다. 

또는 너무 오래되었다면 다시 로그인이 필요할 수 있다. 

 

 

요청 인터셉터 

어떤 API 요청을 보내기전에 요청 Authorization 헤더에 accessToken을 붙여서 벡엔드로 요청을 보낸다. 

그러므로 로그인이 되어있는 상태에서만 다른 API 요청이 가능하도록 하는 것이다. 

api.interceptors.request.use(
  config => {
    const token = useAuthStore.getState().accessToken

    if (token) {
      config.headers.Authorization = token.startsWith('Bearer ')
        ? token
        : `Bearer ${token}`
    }
    return config
  },
  error => Promise.reject(error)
)

 

 

 

 

응답 인터셉터 

API 응답으로 401이나 403 에러나 나왔을 때 자동으로 토큰을 재발급하고 요청을 재시도한다. 

 

주요 변수 

let isRefreshing = false // 토큰 갱신 중인지 여부
let failedQueue: any[] = [] // 갱신 중에 들어온 요청들을 저장하는 큐

 

 

새 토큰 전달 함수 

토큰 재발급이 완료되면, 큐에 있던 요청들에게 새 토큰을 전달하고

실패 시 모든 요청을 에러로 종료시킨다. 

const processQueue = (error: any, token: string | null = null) => {
  failedQueue.forEach(prom => {
    if (token) {
      prom.resolve(token)
    } else {
      prom.reject(error)
    }
  })
  failedQueue = []
}

 

 

응답 인터셉터

1. 토큰 만료 시 

  • _retry 플래그는 무한 루프 방지로, 같은 요청을 재시도하지 않도록 한다. 
if (
  (error.response?.status === 401 || error.response?.status === 403) &&
  !originalRequest._retry
)

 

2. 이미 누군가 토큰을 갱신 중이면 나머지는 큐에 대기

if (isRefreshing) {
    return new Promise((resolve, reject) => {
      failedQueue.push({
        resolve: (token: string) => {
          originalRequest.headers.Authorization = `Bearer ${token}`
          resolve(api(originalRequest))
        },
        reject: (err: any) => reject(err),
      })
    })
  }

 

 

 

3. 토큰 재발급 API 요청

zustand store에 저장된 refreshToken을 가져와서, 그 값으로 새로운 accessToken 값을 받는다. 

`tokenReissue()`가 api 요청을 하는 함수이다.

이 함수의 응답으로 받은 accessToken 과 refreshToken을 store에 저장시킨다. 

 

대기 중이던 요청들(failedQueue)을 새 토큰으로 모두 재시도 한다. 

원래 실패했던 요청(originalRequest)도 새 토큰으로 다시 보낸다.

try {
    const refreshToken = useAuthStore.getState().refreshToken
    const data = await authService.tokenReissue(refreshToken!)
    console.log('/tokens/reissue 응답:', data)
    const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
      data

    // 새 토큰 저장
    useAuthStore.getState().setAccessToken(newAccessToken)
    useAuthStore.getState().setRefreshToken(newRefreshToken)

    // 큐 처리
    processQueue(null, newAccessToken)

    // 원래 요청 재시도
    originalRequest.headers.Authorization = `Bearer ${newAccessToken}`
    return api(originalRequest)
  } catch (err) {
    // ....
  }

 

 

 

토큰 재발급이 잘 이루어지는 지 테스트하기

토큰 재발급은 accessToken이 만료되었을 때에만 이루어지기 때문에, 직접 만료된 토큰을 넣어서 테스트해봐야한다. 

 

로그인 코드에서 로그인 후에 상태를 저장하는 코드는 이렇다.

// Zustand 상태에 로그인 정보 저장
  const { login } = useAuthStore.getState()
  login(
    {
      memberName: memberName,
      email: email,
      phoneNumber: '',
      profileImageUrl: profileImageUrl,
    },
    accessToken,
    refreshToken
  )

 

 

여기에서 테스트를 위해 실제 `accessToken` 값 대신 `'expired-token'`이라는 문자열을 저장한다. 

// Zustand 상태에 로그인 정보 저장
  const { login } = useAuthStore.getState()
  login(
    {
      memberName: memberName,
      email: email,
      phoneNumber: '',
      profileImageUrl: profileImageUrl,
    },
    'expired_token',
    refreshToken
  )

 

 

 

이렇게 만료된 accessToken을 저장시킨 후, 다른 API 요청을 시도하면

응답 인터셉터가 401 또는 403 응답을 가로채서 토큰 재발급 API 를 호출하게 된다. 

따라서 새로운 accessToken과 refreshToken 을 받게 되며 이 값을 다시 zustand 상태로 저장시키면 된다.