Cute Bow Tie Hearts Blinking Pink Pointer

프론트엔드/React-Native(RN)

[JS/React/RN] axios interceptors로 JWT 토큰 재발급 후 API 재호출 (feat. React-native Hook에서 사용하는 법)

청포도 에이드 2023. 10. 30. 17:41
728x90

목차

- axios interceptors 사용 시 장점

- JWT 토큰 요청 헤더에 넣어주는 법

- access, refresh token 만료 시 재발급 받기

- RN Hook에서 사용하는 법

 

 

Axios Interceptors를 사용하게 된 배경 및 장점

 

 

기존에는 api 요청을 보낼 때마다, 모든 요청에 JWT 토큰을 담은 header를 매번 넣어줬다.

개발 초기 단계일 땐 큰 문제가 없었지만, 유지보수와 업데이트를 하며 기능이 늘어 api 요청이 늘 때마다 중복된 코드가 계속해서 추가되었다. 100개가 넘어가는 api 요청 코드를 당장 뜯어고칠 수는 없었기에 현황을 유지했지만, 기존 코드는 JWT 토큰의 유효기간이 없다는 치명적인 단점이 존재했다...(100년이었으니 사실상 없는 거나 마찬가지)

이러한 보안 문제를 해결하기 위해 JWT access 토큰과 refresh 토큰을 적정 기간마다 재발급 해주기로 결정!

기존 방식대로 header에 매번 토큰을 넣어주던 것처럼, 토큰 재발급도 같은 방식으로 하드 코딩하기엔 코드가 굉장히 길었고..

장기적으로 봤을 때 매우 비효율적임이 분명했기에 api 요청/응답 전 거칠 미들 웨어로 axios interceptor를 사용해 전체적으로 갈아 엎기로 결정하였다.

 

interceptors를 사용함으로써 보일러 플레이트가 확연히 줄어들었으며, 재사용성 및 유지보수성이 좋아졌다.

 

그래서 어떻게 쓰는데?

 

JWT 토큰 요청 헤더에 넣어주는 법 & access, refresh token 만료 시 재발급 받기

 

// APIConfig.js


import axios from 'axios';
import Config from 'react-native-config';
import EncryptedStorage from 'react-native-encrypted-storage';


const setTokens = async (tokens) => {
  // 토큰 재발급 해주는 코드
};


const api = axios.create({
  baseURL: Config.BASE_URL, // env파일에 있는 변수, string으로 넣어도 문제는 없음
});
api.defaults.headers.common['Content-Type'] = 'application/json';
api.defaults.headers.common['Access-Control-Allow-Origin'] = '*';

 api.interceptors.response.use(
      function (response) {
        //status 200일 때
        // 정상적으로 response를 return해줌
        return response;
      },
      async function (error) {
        // status 200 아닐 때
        const originalRequest = error.config; //기존 요청 저장

        // 인증 에러 발생시
        if (error.response.status === 401) {
        	// 토큰 재발급해주는 코드, 만약 reponse로 새로운 토큰이 같이 온다면 아래처럼 작업
            const {expiredType, accessToken, refreshToken } = error.response.data;
            // 필자의 경우 expiredType이라는 값을 받아 만료의 경우의 수를 나누어주었다.
            // access만료, refresh만료, 잘못된 토큰. 세 가지 경우로 나누었다.
             switch (expiredType) {
            case "access":
         	const res = await setTokens({ accessToken, refreshToken }); // 받은 새로운 토큰을 local에 저장
              if (res.success) { // 한번 더 체크하는 용도
                axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
                originalRequest.headers.Authorization = `Bearer ${accessToken}`;
               
                // token을 세팅해준 후 기존에 요청했던 api를 새로운 헤더를 담아 재요청해준다.
                return axios(originalRequest); // api 재요청 함수
              } else {
                console.log('storage 저장 중 에러 발생');
                return new Error('어플리케이션 재실행을 해주세요.');
              }
             case "refresh" :
              // refresh 토큰이 만료된 경우 재로그인을 해주어야한다.
              // 로그인하는 페이지로 강제 이동하는 코드 작성
              
             case "wrong":
             //잘못된 접근 시, 기기에 저장된 token을 강제로 다 삭제해버린 뒤, 로그인 페이지로 강제 이동
             }
          }
          return Promise.reject(error);
      },
    );


    
export default api;

 

이렇게 작성을 해주고, api라는 함수를 다른 파일에서 import 해주어서 간편히 사용하면 된다.

 

예를 들어,

 

// test.js
import api from './APIConfig';

// 로그인의 경우
export const login = async (data) => {
  try {
    const res = await api.post('/api/login', data);

    return { data: res.data };
  } catch (error) {
    return { success: false, message: error.response.data.message };
  }
};

 

이렇게 간단한 방식으로 api를 호출할 수 있다.

 

 

근데 이 때 발생한 문제...!

 

필자는 recoil이라는 상태관리 라이브러리를 사용하는데...

 

api 호출로 응답받은 data를 전역으로 관리한다.

따라서 이 interceptor를 리액트 Hook 안에서만 사용해만하는 상황이 발생...!

이대로 interceptor 사용에 실패하나 했지만...

 

안 되면 되게 만든다.

 

RN Hook에서 사용하는 법

 

아래처럼 수정해준다.

 

// APIConfig.js


import axios from 'axios';
import Config from 'react-native-config';
import EncryptedStorage from 'react-native-encrypted-storage';


const setTokens = async (tokens) => {
  // 토큰 재발급 해주는 코드
};


const api = axios.create({
  baseURL: Config.BASE_URL, // env파일에 있는 변수, string으로 넣어도 문제는 없음
});
api.defaults.headers.common['Content-Type'] = 'application/json';
api.defaults.headers.common['Access-Control-Allow-Origin'] = '*';

const Interceptor = ({ children }) => {

  useEffect(() => {

     api.interceptors.response.use(
          function (response) {
            //status 200일 때
            // 정상적으로 response를 return해줌
            return response;
          },
          async function (error) {
            // status 200 아닐 때
            const originalRequest = error.config; //기존 요청 저장

            // 인증 에러 발생시
            if (error.response.status === 401) {
                // 토큰 재발급해주는 코드, 만약 reponse로 새로운 토큰이 같이 온다면 아래처럼 작업
                const {expiredType, accessToken, refreshToken } = error.response.data;
                // 필자의 경우 expiredType이라는 값을 받아 만료의 경우의 수를 나누어주었다.
                // access만료, refresh만료, 잘못된 토큰. 세 가지 경우로 나누었다.
                 switch (expiredType) {
                case "access":
                const res = await setTokens({ accessToken, refreshToken }); // 받은 새로운 토큰을 local에 저장
                  if (res.success) { // 한번 더 체크하는 용도
                    axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
                    originalRequest.headers.Authorization = `Bearer ${accessToken}`;

                    // token을 세팅해준 후 기존에 요청했던 api를 새로운 헤더를 담아 재요청해준다.
                    return axios(originalRequest); // api 재요청 함수
                  } else {
                    console.log('storage 저장 중 에러 발생');
                    return new Error('어플리케이션 재실행을 해주세요.');
                  }
                 case "refresh" :
                  // refresh 토큰이 만료된 경우 재로그인을 해주어야한다.
                  // 로그인하는 페이지로 강제 이동하는 코드 작성

                 case "wrong":
                 //잘못된 접근 시, 기기에 저장된 token을 강제로 다 삭제해버린 뒤, 로그인 페이지로 강제 이동
                 }
              }
              return Promise.reject(error);
          },
        );
      }, []);
   return children;

}
    
export default api;
export { Interceptor };

 

여기서 끝이 아니다. 이 컴포넌트를 프로젝트 최상단 컴포넌트에 두어야한다!! (정확히는 구현한 모든 페이지의 상단.)

개발 방식에 따라 차이가 있겠지만 

 

필자는 모바일 환경에서 개발했기 때문에 @react-navigation/native, @react-navigation/native-stack 이라는 라이브러리를 사용해서 navigation을 구현하였다.

 

 

RN Hook에서 사용하는 법

 

 

따라서  아래와 같은 식으로 Interceptor Hook 을 삽입해주었다. 

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Test from "./Test" // Test페이지
import { Interceptor } from './APIConfig';
const Stack = createNativeStackNavigator();
const AppNavigator = () => {

  return (
    <NavigationContainer>
      <Interceptor> // 모든 페이지의 상단인 부분에서 적용해주었다
        <Stack.Navigator>
        
            <Stack.Screen name="Test" component={Test} options={{ headerShown: false }} />
            
        </Stack.Navigator>
      </Interceptor>
    </NavigationContainer>
  );
}

export default AppNavigator;

 

728x90