useDispatch() at axios interceptor not work

Maliethy·2022년 2월 4일
0

react error

목록 보기
6/7

1. issue

프론트단에서 JWT 보안상 안정성을 강화하기위해 accessToken은 redux에, refreshToken은 cookies에 보관하는 방식으로 axios.interceptor를 수정하는 과정에서 문제가 발생했다. 아래 코드와 같이
axios intercepter 안에서 dispatch를 가져와 사용하려고 하니 정상적으로 작동하지 않았다.

import { useAppDispatch } from '@redux/store/configureStore';


 axiosApiInstance.interceptors.response.use(
    (response) => {
      return response;
    },
    async function (error) {
    const dispatch = useAppDispatch();//이 코드 이후부터 코드들이 작동하지 않았다.
    
    
    
    }

2. solution

Hooks, such as useDispatch(), can only be used in the body of a function component. You cannot use useDispatch in an interceptor. If you need to dispatch an action in an interceptor, you will need to have a reference to the store object, and call store.dispatch(/ action /)
https://stackoverflow.com/questions/60643890/redux-dispatch-not-work-when-use-in-axios-interceptors

useDispatch hook은 함수 component 안에서만 사용할 수 있다. interceptor 안에서는 사용할 수 없어서 에러가 난 것이다.
사용해본 결과 다음과 같이 configureStore에서 바로 가져온 store 객체에 있는 dispatch는 작동이 안된다.

import { store } from '@redux/store/configureStore';

 axiosApiInstance.interceptors.response.use(
    (response) => {
      return response;
      },
    async function (error) {
       store.dispatch(userSlice.actions.setAccessToken(newAccessToken));
      },
    );
export const PostConfirmUserInfo = createAsyncThunk<PostUpdateMyInfoReponse, GetMyInfoRequest>(
  'Web/ConfirmUserInfo',
  async (data, { getState, rejectWithValue, dispatch })/ => {
  
    const response = await setupAxios(getState(), dispatch).post(`/Web/ConfirmUserInfo`, data);/dispatch매개변수를 가져와 setupAxios 함수의 인수로 넣어서 사용한다. 
    return response.data as PostUpdateMyInfoReponse;
  },
);

전체코드는 다음과 같다.

import reactCookies from 'react-cookies';
import axios from 'axios';
import setToken from '@utils/setToken';
import { userSlice } from '../reducers/user';
import { message } from 'antd';
import router from 'next/router';

function setupAxios(state) {
  const baseURL = process.env.NODE_ENV === 'development' ? '/' : 'https://example.co.kr/'; //개발모드일 때 proxy서버로 요청보내고 배포모드일 때 실서버로 요청보내기


  const axiosApiInstance = axios.create({
    baseURL: baseURL,
    withCredentials: true,
  });

  const axiosApiRefreshToken = axios.create({
    baseURL: baseURL,
    withCredentials: true,
  });

  axiosApiInstance.interceptors.request.use(
    (config) => {
      const accessToken = state.user.accessToken;
      // console.log('accessToken', accessToken);
      config.headers = {
        Authorization: `Bearer ${accessToken}`,
        Accept: 'application/json',
      };
      return config;
    },
    (error) => {
      Promise.reject(error);
    },
  );

  axiosApiRefreshToken.interceptors.request.use(
    (config) => {
      const refreshTokenByCookies = reactCookies.load('refreshToken');

      config.headers = {
        Authorization: `Bearer ${refreshTokenByCookies}`,
        Accept: 'application/json',
      };
      return config;
    },
    (error) => {
      Promise.reject(error);
    },
  );

  axiosApiInstance.interceptors.response.use(
    (response) => {
      return response;
    },
    async function (error) {
      const originalRequest = error.config;
      const refreshTokenByCookies = await Promise.resolve(reactCookies.load('refreshToken'));
      console.log('originalRequest.url', originalRequest.url);
      if (
        (error.response?.status === 401 || error.response?.status === 403) &&
        originalRequest.url === '/Web/RefreshToken'
      ) {
         console.log('Prevent infinite loops');
        dispatch(userSlice.actions.signOut());
        message.warn('만료된 토큰으로 반복해서 요청이 가고 있습니다. 다시 로그인이 필요합니다.', 4);
        return Promise.reject(error);
      }
      // console.log('error.response?.status', error.response?.status);
      // console.log('refreshTokenByCookies', refreshTokenByCookies);
      
if (error.response?.status === 401 || error.response?.status === 403) {
        if (refreshTokenByCookies) {
          try {
            const response = await axiosApiRefreshToken.get('/Web/RefreshToken');
            const newAccessToken = response.data.accessToken;

            dispatch(userSlice.actions.setAccessToken(newAccessToken));
            setToken(refreshTokenByCookies);
            originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;

            return axios(originalRequest);
          } catch (error) {
            console.log('error', error);
            message.warn('자동로그인 가능 기간이 지났습니다. 다시 로그인이 필요합니다.', 4);
           dispatch(userSlice.actions.signOut());
          }
        }
      }

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

export default setupAxios;

참고로 rtk-query의 경우 BaseQueryFn의 매개변수 중 api에 담긴 dispatch를 사용할 수 있다.

const baseQueryWithIntercept: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError | ResponseErrorType> =
  async (args, api, extraOptions) => {
    let result = await baseQuery(args, api, extraOptions);

    if (result.error && (result.error.status === 401 || result.error.status === 403)) {
      const refreshTokenByCookies = reactCookies.load('refreshToken');
      const refreshTokenResult: QueryReturnValue<any, FetchBaseQueryError | ResponseErrorType, FetchBaseQueryMeta> =
        await baseQuery(
          {
            url: 'Web/RefreshToken/',
            headers: {
              Authorization: `Bearer ${refreshTokenByCookies}`,
            },
          },
          api,
          extraOptions,
        );
      console.log('refreshTokenResult', refreshTokenResult);

      
      if (!!refreshTokenResult.data) {
        const newAccessToken = refreshTokenResult.data.accessToken;
        api.dispatch(userSlice.actions.setAccessToken(newAccessToken));
        setToken(refreshTokenByCookies);
        result = await baseQuery(args, api, extraOptions);
      } else if (refreshTokenResult?.error?.status === 401) {
        message.error('내 정보 가져오기에 실패했습니다. 다시 로그인이 필요합니다.', 4);
        api.dispatch(userSlice.actions.signOut());
      }
    return result;
  };


export const rtkApi = createApi({
  baseQuery: baseQueryWithIntercept,
  reducerPath: 'rtkApi',
  tagTypes: ['User'],
  endpoints: (build) => ({
  ...
                        })
             )
  })

다른 기기 로그인 후 refreshToken 만료 시 setAxios의 try catch문의 catch부분이 실행되고 있다.

catch (error) {
            console.log('error', error);
            message.warn('자동로그인 가능 기간이 지났습니다. 다시 로그인이 필요합니다.', 4);
           dispatch(userSlice.actions.signOut());
          }

rtk-query의 경우 아래 코드 부분이 실행되며 로그아웃이 되고 있다.

else if (refreshTokenResult?.error?.status === 401) {
        message.error('내 정보 가져오기에 실패했습니다. 다시 로그인이 필요합니다.', 4);
        api.dispatch(userSlice.actions.signOut());
      }

rtk-query의 refreshTokenResult에는 크게 data와 meta가 있고 그 안에는 다음과 같은 정보들이 들어있다.


출처:
https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0
https://redux-toolkit.js.org/api/createAsyncThunk
https://stackoverflow.com/questions/52946376/reactjs-axios-interceptors-how-dispatch-a-logout-action

profile
바꿀 수 있는 것에 주목하자

0개의 댓글