Axios를 이용한 JWT Refresh 자동화(with. debounce)

유영석·2022년 2월 25일
9

화면에서 페이지가 전환될 때 각각의 컴포넌트들은 생각보다 많은 네트워크 요청을 보낸다.
만약 Token이 만료되어있다면, 각각의 컴포넌트 요청에서 토큰을 갱신하고 기존 네트워크 요청에 연결해야하는데...
적어도 Token Refresh 요청만이라도 통합할수 없을까?

Axios를 이용해서 이 문제를 해결해보고자 한다. 내가 생각한 아이디어는 간단하다. 일반적인 요청은 아래와 같이 동작한다. 각각의 Reqeust마다 적절한 Response를 받는다.

이때 Axios instance는 모두 같은 토큰을 가지고 있기 때문에 만료된 토큰으로 요청한다면 이렇게 동작할 것이다. 근데 이때 동작하는 Refresh Token함수는 1) 서버로부터 새로운 토큰을 받아오고 2) 새로 받아온 토큰을 local storage에 저장하는 등 생각보다 많은 역할을 수행해야한다. 때문에 여기서 Debounce를 이용해 같은 동작을 한번만 수행하도록 개선했다.

Axios 요청에서 Error를 묶으려면 Axios Interceptor가 필요하다.

Axios Interceptor
then이나 catch로 처리되기 전에 요청이나 응답을 가로챌 수 있습니다.

따라서 Axios Interceptor와 debounce(lodash)를 통해서 기능을 구현했다.

전체 코드는 다음과 같다

import debounce from 'lodash/debounce';
import axios, { AxiosInstance, AxiosError } from 'axios';

const AUTHORIZATION = 'Authorization';

class OAuth {
  ... 
  private initInterceptor(axiosInstance: AxiosInstance) {
    // refresh token 요청을 위한 API 정의
    const getRefreshToken = async (refreshToken: string) => {
      try {
        const credential: Credential = {
          client_id: OAuthConfig.clientId,
          client_secret: OAuthConfig.clientSecret,
          grant_type: GrantType.REFRESH_TOKEN,
          refresh_token: refreshToken,
        };

        const formData = new FormData();
        for (const [key, value] of Object.entries(credential))
          formData.append(key, value);

        const { data: newToken } = await axios.post<Token>(
          '/user/oauth/token',
          formData,
          {
            baseURL: this.apiBaseUrl,
            headers: { 'content-type': 'application/x-www-form-urlencoded' },
          }
        );

        this.setAuthorization(newToken);

        return newToken;
      } catch (error) {
        // refresh token 이 만료된 경우
        this.logout();
      }
    };

    const getRefreshTokenWithSchedulerResponse = debounce(
      getRefreshToken,
      200, // 200ms 화면전환에 의한 Component요청이 생길 수있는 최대시간을 적어주면 좋을 것 같다.
      { leading: true, trailing: false } // Option 설정 중요
    );

    axiosInstance.interceptors.response.use(null, (error: AxiosError) => {
      if (!error.response) return error;

      // need refresh token: 리프래쉬가 필요한 경우 417 에러를 사용하기로 back-end 논의하여 결정함
      if (error.response.status === 417) {
        const refresh = async (resolve: typeof Promise.resolve) => {
          try {
            // 이 함수를 통하여 같은 토큰 요청이 여러번 가는것을 방지한다.
            const newToken: Token = await getRefreshTokenWithSchedulerResponse(
              this.token.refresh_token
            );

            error.config.headers[AUTHORIZATION] 
              = `${newToken.token_type} ${newToken.access_token}`;

            // 토큰을 갱신하여 새로운 config와 함께 기존 request를 다시 실행한다.
            const data = await axios.request(error.config);

            return resolve(Promise.resolve(data));
          } catch {
            return resolve(Promise.reject(error));
          }
        };

        return new Promise(refresh);
      }

      return Promise.reject(error);
    });
  }
  ...
}

FE에서 DDoS공격 하지 않기!?

debounce통해 적어도 Token expired 의한 요청이라도 O(n + n)에서 O(n + 1)로 줄일 수 있을 것이라 생각한다. 2n이나 n+1이나 그렇게 큰 차이가 안난다고 말할지도 모르겠지만, 작은 곳에서부터 디테일을 살려가야한다고 생각한다. 작은 것부터 잡아가면서 프로젝트의 디테일을 살려보자 :)

profile
ENFP FE 개발자 :)

2개의 댓글

comment-user-thumbnail
2022년 2월 25일

👍

1개의 답글