[React Native] Axios Interceptor

mainsain·2023년 10월 12일

React Native

목록 보기
9/9
post-thumbnail

Axios의 미친 기능인 interceptor에 로직을 추가하여 api요청 전/후처리를 해보자.

Axios Interceptor로 토큰을 재발급해보자.

인터셉터 | Axios Docs

기존에 만들어진 axios Interceptors

import axios from "axios";
import * as Sentry from "@sentry/react-native";
import * as SecureStore from "expo-secure-store";

const getValueFor = async (key: string) => {
	return await SecureStore.getItemAsync(key);
};

const instance = axios.create({
	baseURL: "ㅇㅇ",
	timeout: 1000,
});

instance.interceptors.request.use(
	async (config) => {
		config.headers["Content-Type"] = "application/json; charset=utf-8";
		config.headers["Authorization"] = `Bearer ${await getValueFor(
			"accessToken",
		)}`;
		return config;
	},
	(error) => {
		console.error(error);
		return Promise.reject(error);
	},
);

instance.interceptors.response.use(
	(response) => {
		console.log("response", response);
		return response;
	},
	async (error) => {
		if (error.response.status === 400) {
			alert(
				"세션이 만료되었습니다. 해당 서비스는 재 로그인 이후 이용 가능합니다.",
			);
			return "session expire";
		}

		if (error.response.status === 500) {
			Sentry.captureMessage("서버 에러");
			alert("시스템 에러, 관리자에게 문의 바랍니다.");
		}

		console.error(error);
	},
);

중간에서 request, response를 가로채 커스텀한다.

  • request에선 요청을 보내기 전 작업이 가능하다.
    • 재사용되는 헤더들을 정의해뒀다.
  • response에선 서버의 응답이 return 되기 전 작업이 가능하다.
    • 에러핸들링을 처리했다.

코드를 기능별로 분리를 더 해보자.

import axios from "axios";
import * as Sentry from "@sentry/react-native";
import * as SecureStore from "expo-secure-store";

const getValueFor = async (key: string) => {
	return await SecureStore.getItemAsync(key);
};

const setRequestHeaders = async (config) => {
	config.headers["Content-Type"] = "application/json; charset=utf-8";
	config.headers["Authorization"] = `Bearer ${await getValueFor(
		"accessToken",
	)}`;
	return config;
};

const handleResponseSuccess = (response) => {
	console.log("Success response", response);
	return response;
};

const handleRequestError = (error) => {
	console.error("handleRequestError :", error);
	return Promise.reject(error);
};

const handleResponseError = async (error) => {
	console.log("handleResponseError :", error);
	if (!error.response) return;

	const { status } = error.response;

	if (status === 400) {
		alert(
			"세션이 만료되었습니다. 해당 서비스는 재 로그인 이후 이용 가능합니다.",
		);
		return "session expire";
	} else if (status === 500) {
		Sentry.captureMessage("서버 에러");
		alert("시스템 에러, 관리자에게 문의 바랍니다.");
	}
	console.error(error);
};

const instance = axios.create({
	baseURL: "ㅇㅇ",
	timeout: 1000,
});

instance.interceptors.request.use(setRequestHeaders, handleRequestError);
instance.interceptors.response.use(handleResponseSuccess, handleResponseError);

export default instance;
  • Request에서, Header, Error함수를 만들어 분리했다.
  • Response에서, Success, Error를 분리했다.

선택의 상황

클라이언트에서 토큰 만료시간 체크

  • 클라이언트에서 토큰을 디코딩해 만료시간을 체크한다. 불필요한 요청을 줄일 수 있는 장점이 있음

서버에서 토큰 만료시간 체크

  • 토큰이 만료된 경우, API요청이 실패한다. 그러면 다시 토큰을 갱신 → API요청 재시도해야함. 클라이언트와 서버 간의 요청이 많아진다.

선택

  • 클라이언트에서 토큰을 디코딩하는 것 자체에 대한 우려가 있었다. 보안적으로 클라이언트에서 디코딩은 안된다고 판단했다.
  • 번거로워도, 보안을 신경써보자.

access token이 만료되었을 때, 서버에 refreshToken → response access받는 과정, secure에 업데이트하는 그 로직들을 한번에 처리해버리자.

코드 정리

import axios, { AxiosError, AxiosRequestConfig } from "axios";
import * as Sentry from "@sentry/react-native";
import * as SecureStore from "expo-secure-store";
import { BASE_URL, CONTENT_TYPE, TIMEOUT } from "../constants/constants";
import { logout } from "./logout";

const instance = axios.create({
	baseURL: BASE_URL,
	timeout: TIMEOUT,
});

const getValueFor = async (key: string) => {
	return await SecureStore.getItemAsync(key);
};

const getAuthorizationHeader = async (tokenKey: string) => {
	return `Bearer ${await getValueFor(tokenKey)}`;
};

const setCommonHeaders = async (config: any) => {
	// default header 설정
	config.headers["Content-Type"] = CONTENT_TYPE;
	config.headers["Authorization"] = await getAuthorizationHeader("accessToken");
	return config;
};

const refreshAccessTokenAndRetry = async (config: AxiosRequestConfig) => {
	// accessToken 만료시 refreshToken으로 재발급
	try {
		const response = await axios.post(
			`${BASE_URL}/user/token`,
			{},
			{
				headers: {
					"Content-Type": CONTENT_TYPE,
					Authorization: await getAuthorizationHeader("refreshToken"),
				},
			},
		);
		if (response.status === 201) {
			const newAccessToken = response.data.data.accessToken;
			await SecureStore.setItemAsync("accessToken", newAccessToken);
			if (!config.headers) {
				config.headers = {};
			}
			config.headers["Authorization"] = `Bearer ${newAccessToken}`;
			return axios(config);
		}
		console.error("refreshAccessTokenAndRetry error :", response);
		return Promise.reject(response);
	} catch (error: any) {
		console.error(error.response.status);
		if (error.response.status === 401) {
			await logout();
			SecureStore.setItemAsync("session_expire", 'expired');
			alert("토큰 갱신에 실패했습니다. 다시 로그인 해주세요.");
			return Promise.reject(error);
		}
	}
};

const handleResponseError = async (error: AxiosError) => {
	if (!error.response) return Promise.reject(error);
	const { status, config } = error.response;
	console.log("status :", status);

	switch (status) {
		case 400:
			alert(
				"올바르지 않은 내용을 입력하셨습니다. 다시 확인해주세요.",
			);
			break;
		case 401:
			return await refreshAccessTokenAndRetry(config);
		case 409:
			alert("동일한 이름의 반려견이 이미 등록되어 있습니다.");
		case 500:
			Sentry.captureMessage("서버 에러");
			alert("시스템 에러, 관리자에게 문의 바랍니다.");
			break;
		default:
			console.error(error);
			return Promise.reject(error);
	}
};

const handleResponseSuccess = (response) => {
	console.log("Success response");
	return response;
};

const handleRequestError = (error: AxiosError) => {
	console.error("handleRequestError :", error);
	return Promise.reject(error);
};

instance.interceptors.request.use(setCommonHeaders, handleRequestError);
instance.interceptors.response.use(handleResponseSuccess, handleResponseError);

export default instance;

api 요청 전후처리를 진행해버리면, 실제 api 요청 시에 굉장히 간결한 코드작성이 가능하다.

profile
새로운 자극을 주세요.

0개의 댓글