#부록. interceptors 사용하기

toto9602·2022년 2월 25일
0

첫 Express 프로젝트

목록 보기
7/7

참고자료: axios interceptors로 토큰 리프레시 하기
참고자료: axios 문서

해당 참고자료를 주로 참고하여 코드 및 글을 작성했음을 밝힙니다! :)


어쩌다 보니 프로젝트에서 인증 부분을 거의 다 담당하게 되어..
프런트엔드에서 interceptors까지 구현해 보게 되었다.

인터셉터(interceptors)란?

axios 문서에는 다음과 같이 설명이 되어 있다.

then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있습니다.

요청이나 응답이 도착하기 전, 이를 intercept(가로채서)하여 가공하는 과정을 거칠 수 있게 해 주는 것인 듯!


이번 프로젝트에서는,
토큰을 headers에 담아 요청을 보냈는데, 토큰이 만료되어 401 Unauthrorized 에러가 돌아올 경우,
해당 응답을 가로채서, 토큰을 갱신하는 로직을 거친 후 다시 요청을 보내주는 데 사용해 볼 예정!

글의 순서는

0. utility/auth.js에 작성한 토큰 갱신 함수
1. setInterceptors 함수 - request
2. setInterceptors 함수 - response
3. setInterceptors 전체 코드
4. index.js에서 axios instance 넣어주기
5. axios 요청에 적용하기

로 해 볼 예정!

우선, 프런트엔드 단에서, 기존에 있던 utility 디렉토리 아래에 interceptors라는 디렉토리를 생성해 주었다.

코드 정리에 앞서, 본 프로젝트는 프런트엔드에서 React-Native를 사용 중이라,
interceptors.js 파일을 작성할 때, localStorage가 아닌 AsyncStorage로 작성하였다는 점,

그리고, interceptors 로직 부분은 setInterceptors라는 함수 안에

  • 요청을 보낼 때
  • 응답을 받을 때

두 경우를 구분해서 작성하였다는 점을 밝히고 시작하는 게 좋겠다!

0. utility/auth.js에 작성한 토큰 갱신 함수

해당 파일에는, 인증 관련하여 프런트엔드 단에서 사용하는 함수들을 작성해 두었다.
tokenRefresh 말고도 작성해 둔 함수들이 있지만,
이번 글에서는 tokenRefresh 만을 사용하므로 해당 함수만 글에 작성하였다!

utility/auth.js

import AsyncStorage from "@react-native-async-storage/async-storage";
import LOCAL_HOST from "../../features/local.js";
import axios from 'axios';

export const tokenRefresh = async (accessToken, refreshToken) => {

    const refresh = await axios(`http://${LOCAL_HOST}:3000/auth/token/refresh`, {
        method:"POST",
        headers:{
            Accept:"application/json",
            Authorization: `Bearer ${accessToken}`,
            Refresh:refreshToken,
        }
    }).then((res) => {
        console.log('res.data', res.data);
        return res.data;
    }).catch((error) => {
        console.log('Token refresh error', error.message);
        return error;
    }) 
    return refresh;
}

Expo를 개발 과정에서 사용 중이라,
외부 기기에서 요청을 보내는 경우 로컬호스트의 IP 주소를 명시해 주어야 하는 것 같은데,
다른 IP에 연결할 때마다 여러 군데에서 IP 주소를 바꿔주기는 번거로워서
로컬 호스트 주소만을 작성하는 별도 파일 local.js를 두어 관리 중이다!

tokenRefresh 함수는 accessTokenrefreshToken을 받아서
headers에 담아 토큰을 갱신하는 api에 요청을 반환하고, 반환값을 돌려주는 함수이다!

백엔드 단에서 실제로 토큰을 갱신하는 로직은 #4. JWT 사용하기 (2)에 작성해 두었다 :)

이제 tokenRefresh 함수를 사용한, 본격적인 interceptors 작성하기!!

1. setInterceptors 함수 - request

utility/interceptors/interceptors.js

import AsyncStorage from "@react-native-async-storage/async-storage";
import {tokenRefresh} from '../auth.js';
import axios from 'axios';
export default function setInterceptors(instance) {
    instance.interceptors.request.use(
        async (res) => {
            const access = await AsyncStorage.getItem('accessToken');
            const refresh = await AsyncStorage.getItem('refreshToken');
    
            if (access) {
                res.headers['Authorization'] = `Bearer ${access}`;
            }
    
            const newConfig = {...res};
            newConfig.url = `${res.url}`;
    
            return newConfig;
        }
    );

request 쪽은 원래 따로 작성 안 하려다가 에러가 나길래 디버깅 과정에서 제대로 이해를 못하고 따라 친 것에 가까워서.. 조금 더 정리를 하고 내용 작성할 예정

2. setInterceptors 함수 - response

instance.interceptors.response.use((response) => {
        return response
    }, async function(error) {
        const originalRequest = error.config;
            if (error.response.status === 401 && !originalRequest.retry) {
                const accessToken = await AsyncStorage.getItem('accessToken');
                const refreshToken = await AsyncStorage.getItem('refreshToken');
    
                const newData = await tokenRefresh(accessToken, refreshToken);
    
                if (newData.status === 'New Access Token granted') {
                    originalRequest.retry = true;
                    AsyncStorage.setItem('accessToken', newData.tokens.access);
                    const content_regex = /"content":".+?"/
                    const latitude_regex = /"latitude":[0-9\.]+/
                    const longitude_regex = /"longitude":[0-9\.]+/
                    const originData = {
                        content:content_regex.exec(originalRequest.data)[0].split(':')[1].slice(1,-1),
                        latitude:latitude_regex.exec(originalRequest.data)[0].split(':')[1],
                        longitude:longitude_regex.exec(originalRequest.data)[0].split(':')[1]
                    }
    
                    originalRequest.headers['Authorization'] = `Bearer ${newData.tokens.access}`;
                    originalRequest.data = originData;

                    return axios(originalRequest);
                } else {
                    return error;
                }
            }
            return Promise.reject(error);
        })

        return instance;
}

request 로직과 동일하게, 응답이 정상적(2xx 범위에 있는 상태 코드)일 경우에는
첫 번째 인자로 받는 함수를 실행한다!

응답이 정상적일 경우에는 그대로 response를 반환해 줌!


그렇지 않을 경우, 2번째 인자로 받는 함수를 실행하게 된다.

error에서 처음에 보낸 요청을 error.config로 가져와서,

if (error.response.status === 401 && !originalRequest.retry)

해당 부분에서 응답 코드가 401인지, 다시 요청된 적 있는 응답인지 검사한다.

응답 코드가 401이고, interceptors에 의해 다시 요청된 바가 없는 응답이라면

AsyncStorage에서 accessTokenrefreshToken을 가져와
accessToken을 갱신하는 요청을 보내준다.


위 요청이 정상적으로 처리되어, 새로운 accessToken이 발급되었다면

originalRequest.retry = true;

재시도된 요청임을 표시해 주고,

AsyncStorage.setItem('accessToken', newData.tokens.access);

새로 발급된 accessToken을 저장!

const content_regex = /"content":".+?"/
const latitude_regex = /"latitude":[0-9\.]+/
const longitude_regex = /"longitude":[0-9\.]+/
const originData = {
                content:content_regex.exec(originalRequest.data)[0].split(':')[1].slice(1,-1),
                latitude:latitude_regex.exec(originalRequest.data)[0].split(':')[1],
                longitude:longitude_regex.exec(originalRequest.data)[0].split(':')[1]
                   }

아래 부분은 originalRequest의 데이터를,
다시 보낼 요청에 포함시켜 주는 과정에서..

originalRequest의 데이터가 왜인지 Object가 아니라 string 타입이라..
정규표현으로 무식하게 데이터를 뽑아내는 과정에서 작성한 코드이다..

근데 다른 팀원이, 그냥 JSON 타입으로 파싱하면 될 거라고 해서 이 글 쓰고 나서 코드 수정하고..
글도 수정할 예정..ㅎㅎ


여튼 originalRequest
headers에는 신규 발급한 accessToken을 넣어주고
data에는 data를 넣어주고

이렇게 가공한 originalRequest를 다시 반환해 주면 된다.

originalRequest.headers['Authorization'] = `Bearer ${newData.tokens.access}`;
originalRequest.data = originData;

return axios(originalRequest);

P. S. 그리고 setInterceptors 함수 자체는 꼭 마지막에 다음과 같이
interceptors를 적용한 instance를 반환해 줘야 한다..!!

        return instance;

이거 빼먹었다가 undefined 에러 계속 났는데 이유를 한참 만에 알아버림..ㅠ

3. setInterceptors 전체 코드

instance 반환을 포함한 전체 코드는 다음과 같다.

utility/interceptors/interceptors.js

import AsyncStorage from "@react-native-async-storage/async-storage";
import {tokenRefresh} from '../auth.js';
import axios from 'axios';
export default function setInterceptors(instance) {
    instance.interceptors.request.use(
        async (res) => {
            const access = await AsyncStorage.getItem('accessToken');
            const refresh = await AsyncStorage.getItem('refreshToken');
    
            if (access) {
                res.headers['Authorization'] = `Bearer ${access}`;
            }
    
            const newConfig = {...res};
            newConfig.url = `${res.url}`;
    
            return newConfig;
        }
    );

    instance.interceptors.response.use((response) => {
        return response
    }, async function(error) {
        const originalRequest = error.config;
            if (error.response.status === 401 && !originalRequest.retry) {
                const accessToken = await AsyncStorage.getItem('accessToken');
                const refreshToken = await AsyncStorage.getItem('refreshToken');
    
                const newData = await tokenRefresh(accessToken, refreshToken);
    
                if (newData.status === 'New Access Token granted') {
                    originalRequest.retry = true;
                    AsyncStorage.setItem('accessToken', newData.tokens.access);
                    console.log(originalRequest);
                    const content_regex = /"content":".+?"/
                    const latitude_regex = /"latitude":[0-9\.]+/
                    const longitude_regex = /"longitude":[0-9\.]+/
                    const originData = {
                        content:content_regex.exec(originalRequest.data)[0].split(':')[1].slice(1,-1),
                        latitude:latitude_regex.exec(originalRequest.data)[0].split(':')[1],
                        longitude:longitude_regex.exec(originalRequest.data)[0].split(':')[1]
                    }
    
                    originalRequest.headers['Authorization'] = `Bearer ${newData.tokens.access}`;
                    originalRequest.data = originData;

                    return axios(originalRequest);
                } else {
                    return error;
                }
            }
            return Promise.reject(error);
        })

        return instance;
}

4. index.js에서 axios instance 넣어주기

위에 작성한 setInteceptors는 instance를 인자로 받으니,
axios instance를 생성해서 인자로 넣어 줘야 한다!

utility/interceptors/index.js

import axios from 'axios';
import LOCAL_HOST from '../../../features/local.js'
import setInterceptors from './interceptors.js';
const defaultURL = LOCAL_HOST;

function noAuth() {
    const instance = axios.create({
        baseURL: defaultURL + ':3000/'
    });
    return setInterceptors(instance);
}

export const noAuthInstance = noAuth();

인증이 되어 있지 않은 경우 사용하는 함수라 noAuth인 듯!

백엔드 서버가 3000번 포트에서 열리기 때문에 로컬 호스트에 포트 번호 3000을 추가해서
baseURL을 작성하고, axios instance를 만들어 주었다.

그리고 setInterceptorsaxios instance를 넣은 반환값인,
interceptors가 적용된 반환값을 noAuthInstance로 보내줌!

5. axios 요청에 적용하기

write.screen.js

...
  const PostWrite = async () => {
    console.log("Postwrite request sent");
    if (checkIfTokenExists) {
      const accessToken = await AsyncStorage.getItem("accessToken");

      await noAuthInstance.post(`http://${LOCAL_HOST}:3000/drops`, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
        data: {
          content,
          latitude,
          longitude,
        },
      })
        .then((res) => {
          console.log('response got', res);
          console.log(`${res.data.data.content} 내용으로 ${res.data.msg}!`);
        })
        .catch((e) => console.log(e));

    } else {
      alert('유효한 사용자가 아닙니다.');
    }
    
  };
...

#4. 에서 export 해 준 axios instancenoAuthInstance를 가져와서,
위와 같이 요청을 보내주면
interceptors 적용은 마무리 !!

구현하면서 필요한 내용은 거의 다 알았다고 생각했는데, 글로 정리하려니 막상 모르는 내용이 많다..ㅠ
이 글은 더 공부해서 수정해야 할 듯..!

profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글