[Axios] 토큰 갱신 + 디바운스 => promise array

Darcy Daeseok YU ·2023년 4월 8일
0

Axios inteceptor token 갱신 + debounce

레거시 프로젝트에 토큰 갱신 인터셉터를 수정하다....

한페이지 내에서 호출이 여러번 들어오는데 호출 1개당 토큰리프레시 1회가 호출된다...

낭비 ...

디바운스 적용 ... Promise 객체를 써야할듯하다.

24.05.04 타입스크립트 반영 및 일부 리팩토링


const onReqFulfilled = (response: AxiosResponse) => {
  return response
}

// 지금 토큰 갱신 중인지
let isRefreshing = false
// 여러개 호출 들어오는 경우 : 모두 토큰 갱신이 필요
const originalRequestsWaiting = [] as {
  url?: string
  resolve: (token: string) => void
  reject: (err: any) => void
}[]
const onReqRejected = (error: any) => {
  console.log(
    "%c interceptors.response ::: error : ",
    "background: black; color: crimson",
    error
  )

  if (axios.isAxiosError(error)) {
    const response = error.response as AxiosResponse<{
      data: any
      code: string
    }>

    if (
      response &&
      response.status === 401 &&
      response.data?.code === "JWT_TOKEN_ERR"
    ) {
      const old_accessToken = sessionStorage.getItem("accessToken")
      const old_refreshToken = sessionStorage.getItem("refreshToken")

      if (!old_refreshToken) return Promise.reject("재 로그인이 필요합니다.")

      const originalRequest = error.config as InternalAxiosRequestConfig & {
        _retry: boolean
      }

      // 지금 요청건으 첫 시도인지 체크 :: 재요청건이 아닌데 토큰이 리프레쉬 진행중인경우
      // 토큰 리프레시 중인경우 대기열에 추가함
      if (originalRequest && !originalRequest._retry) {
        if (isRefreshing)
          return new Promise<string>((resolve, reject) => {
            originalRequestsWaiting.push({
              url: originalRequest.url,
              resolve, // 리졸브는 newAccessToken을 넘겨준다.
              reject, // 리젝트는 Error 객체를 넘겨준다.
            })
          })
            .then((newToken) => {
              // === resolve(newAccessToken)
              originalRequest.headers.Authorization = `Bearer ${newToken}`
              return axiosWithToken(originalRequest)
            })
            .catch((err: any) => {
              // === resolve(error)
              return Promise.reject(err)
            })
      }

      const veryFirstOriginalRequest = originalRequest
      // 여기까지 온겨우 두변째 요청임. ::  재요청할 예정이므로
      originalRequest._retry = true
      // 토큰 리프레시 요청 진행
      isRefreshing = true

      // 리프레쉬 토큰 호출 부

      return new Promise((resolve, reject) => {
        refreshJWT(old_accessToken ?? "", old_refreshToken ?? "")
          .then(
            ({
              accessToken: new_accessToken,
              refreshToken: new_refreshToken,
            }) => {
              sessionStorage.setItem("accessToken", new_accessToken)
              sessionStorage.setItem("refreshToken", new_refreshToken)

              axiosWithToken.defaults.headers.common.Authorization = `Bearer ${new_accessToken}`

              // 대기열 요청 처리
              originalRequestsWaiting.forEach((reqPromise) => {
                reqPromise.resolve(new_accessToken)
              })

              // 성공했으니 대기열 비우기
              originalRequestsWaiting.slice(0)

              // 최초의 accessToken 만료를 만난 리퀘스트 처리
              resolve(axiosWithToken(veryFirstOriginalRequest))
            }
          )
          .catch((error: any) => {
            originalRequestsWaiting.forEach((reqPromise) => {
              reqPromise.reject(error)
            })

            // 최초의 accessToken 만료를 만난 리퀘스트 처리
            reject(error)
          })
          .finally(() => {
            isRefreshing = false
          })
      })
    }
  }
}
axiosWithToken.interceptors.response.use(onReqFulfilled, onReqRejected)

해결안


// 지금 토큰 갱신 중인지
let isRefreshing = false;
// 여러개 호출 들어오는 경우 : 모두 토큰 갱신이 필요
let originalRequests = [];
apiClient.interceptors.response.use(
  function (response) {
    return response;
  },
  async error => {
    console.log(
      '%c interceptors.response ::: error : ',
      'background: black; color: crimson',
      error,
    );

    // axios 사용하기 때문 에러도 AxiosError 객체
    if (axios.isAxiosError(error)) {
      if (
        error.response.status === 401 &&
        error.response.data.code === AuthErrCodes.TOKEN_VERIFY_ERROR
      ) {
        const accessToken = sessionStorage.getItem('accessToken');
        const refreshToken = sessionStorage.getItem('refreshToken');

        if (!refreshToken)
          return Promise.reject('재 로그인이 필요합니다.[No Refresh Token]');

        const originalRequest = error.config;
        // 요청건이 처음 시도인지 체크
        if (!originalRequest._retry) {
          if (isRefreshing) {
            return new Promise(function (resolve, reject) {
              originalRequests.push({
                url: originalRequest.url,
                resolve,
                reject,
              });
            })
              .then(token => {
                originalRequest.headers['Authorization'] = 'Bearer ' + token;
                return axios(originalRequest);
              })
              .catch(err => {
                return Promise.reject(err);
              });
          }

          originalRequest._retry = true;
          isRefreshing = true;

          return new Promise(function (resolve, reject) {
            renewAuthTokens({ accessToken, refreshToken })
              .then(({ accessToken, refreshToken }) => {
                sessionStorage.setItem('accessToken', accessToken);
                sessionStorage.setItem('refreshToken', refreshToken);

                apiClient.defaults.headers.common['Authorization'] =
                  'Bearer ' + accessToken;
                originalRequest.headers['Authorization'] =
                  'Bearer ' + accessToken;

                originalRequests.forEach(originalRequestPromise => {
                  originalRequestPromise.resolve(accessToken);
                  console.log('처리됨 ::: ', originalRequestPromise.url);
                });

                resolve(axios(originalRequest));
              })
              .catch(err => {
                originalRequests.forEach(originalRequestPromise => {
                  originalRequestPromise.reject(err);
                });

                // 리덕스 예제
                // store.dispatch(showMessage({ message: 'Expired Token' }));

                reject(err);
              })
              .then(() => {
                isRefreshing = false;
              });
          });
        }
      }

디바운스는 위에 로직하고 섞어써야할듯하다. => 조금 미뤄둔다.
originalRequest =[] 에 promise객체 써주고 originalRequest._try 체크 해주면 될듯하다.


인터셉터 외부에 변수를 정의 
let timer = null;
let originalRequests = [];

apiClient.interceptors.response.use(
  function (response) {
    return response;
  },
  async error => {
    console.log(
      '%c인터셉터 에러 ==== ',
      'background: lightgreen; color: white',
      error,
    );

    // axios 사용하기 때문 에러도 AxiosError 객체
    if (axios.isAxiosError(error)) {
      if (
        error.response.status === 401 &&
        error.response.data.code === AuthErrCodes.TOKEN_VERIFY_ERROR
      ) {
        const authTokens = JSON.parse(
          sessionStorage.getItem('recoil-persist'),
        )?.auth;

        if (!authTokens.refreshToken)
          return Promise.reject('refreshToken을 불러올 수 없습니다.');

        // 디아운스 토큰 요청 한번에 요청 && 처리
        // 0.5s이내 토큰 리프레시 들어오면 요청중이던 토큰 리프레시 스탑 후 재실행
        const originalRequest = error.config;
        originalRequests.push(originalRequest);

        if (timer) {
          clearTimeout(timer);
        }

        timer = setTimeout(async () => {
          // token refresh 요청
          const data = await renewAuthTokens(authTokens);

          sessionStorage.setItem(
            'tokens',
            JSON.stringify({
              accessToken: data.accessToken,
              refreshToken: data.refreshToken,
            }),
          );

          setAxiosApiAuthToken(data.accessToken);

          // 토큰 에러 처리 후 요청 다시 진행
          originalRequests.map(async originalRequest => {
            originalRequest.headers[
              'Authorization'
            ] = `Bearer ${data.accessToken}`;

            //axios.reqeust(originalRequest) 쓰면 어레이 마지막 요청만 호출됨
            return apiClient(originalRequest);
          });

          //array비우기
          originalRequests.splice(0);
        }, 500);

        return;
      }

      if (error.status === 500) {
        console.log('%c error 500====', 'background-color: yellow');
        return alert(error.message);
      }
      console.log('response error', error);

      return Promise.reject(error);
    }
  },
);
profile
React, React-Native https://darcyu83.netlify.app/

0개의 댓글