
React Native 앱에서 API 통신 시 토큰 기반 인증은 필수적입니다.
하지만 토큰 만료와 관련된 문제는 사용자 경험을 저해할 수 있습니다.
특히 여러 API 호출이 동시에 일어나는 경우, 토큰 갱신 메커니즘이 제대로 작동하지 않아 사용자가 갑자기 로그아웃되는 상황이 발생할 수 있습니다.
이 글에서는 React Native 앱에서 토큰 갱신 메커니즘을 최적화하고 API 호출을 효율적으로
관리하는 방법을 알아보겠습니다.
다음과 같은 문제 상황이 발생할 수 있습니다:
Axios 인터셉터를 활용하면 모든 API 요청에 토큰을 자동으로 포함시키고, 토큰 만료 시 자동으로 갱신하는 메커니즘을 구현할 수 있습니다.
import axios from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { navigate } from "../navigation/NavigationService";
import { authEventEmitter } from "../utils/event";
const axiosInstance = axios.create({
baseURL: "http://your-api-url.com",
timeout: 5000,
headers: {
"Content-Type": "application/json",
},
});
const refreshAxios = axios.create({
baseURL: "http://your-api-url.com",
timeout: 5000,
headers: {
"Content-Type": "application/json",
},
});
// 요청 인터셉터 - 모든 요청에 토큰 추가
axiosInstance.interceptors.request.use(
async (config) => {
console.log("🚀 요청 시작:", config.method?.toUpperCase(), config.url);
// 인증이 필요 없는 요청은 바로 진행
if (
config.url === "/auth/login" ||
config.url === "/auth/signup" ||
config.url === "/auth/refresh"
) {
return config;
}
// 토큰 추가
const token = await AsyncStorage.getItem("accessToken");
if (!token) {
console.warn("⚠️ 토큰 없음: 로그인 화면으로 이동합니다.");
navigate("Login");
} else {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 응답 인터셉터 - 토큰 만료 시 갱신
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 토큰 만료 감지
if (
(error.response?.status === 401 ||
(error.response?.status === 400 &&
error.response?.data?.message?.includes("토큰이 만료"))) &&
!originalRequest._retry
) {
console.log("🔄 토큰 만료 감지: 토큰 갱신 시도...");
// 재시도 플래그 설정
originalRequest._retry = true;
try {
// 리프레시 토큰 가져오기
const refreshToken = await AsyncStorage.getItem("refreshToken");
if (!refreshToken) {
throw new Error("리프레시 토큰 없음");
}
// 토큰 갱신 요청
const refreshResponse = await refreshAxios.post("/auth/refresh", "", {
headers: {
"Refresh-Token": refreshToken,
},
});
// 새 토큰 저장
const {
accessToken,
refreshToken: newRefreshToken,
jti,
} = refreshResponse.data;
// 이벤트 발행을 통한 상태 관리
authEventEmitter.emit("login", {
accessToken,
refreshToken: newRefreshToken,
jti: jti || "",
shouldNavigate: false,
});
// 원래 요청 재시도
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axiosInstance(originalRequest);
} catch (error) {
console.error("❌ 토큰 갱신 실패:", error);
// 로그아웃 처리
authEventEmitter.emit(
"logout",
"세션이 만료되었습니다. 다시 로그인해주세요."
);
return Promise.reject(error);
}
}
return Promise.reject(error);
}
);
export default axiosInstance;
_retry 플래그를 사용하여 동일한 요청에 대한 중복 갱신을 방지합니다.여러 API 호출이 동시에 발생하면 토큰 갱신 메커니즘에 부담이 될 수 있습니다. 이러한 문제를 해결하기 위해 API 호출을 순차적으로 처리하고 중복을 제거해야 합니다.
아래는 마이페이지 컴포넌트의 최적화된 예시입니다:
import React, { useEffect, useState } from "react";
import { SafeAreaView, View, Text, StyleSheet, ScrollView } from "react-native";
import { useUserProfile } from "../../hooks/useUserProfile";
import { useParticipations } from "../../hooks/useParticipations";
import { matchEventEmitter } from "../../utils/event";
import AsyncStorage from "@react-native-async-storage/async-storage";
export const MyPageScreen = ({ navigation }) => {
const { userProfile, fetchProfile } = useUserProfile();
const { participations, loadParticipations } = useParticipations();
useEffect(() => {
// 초기 데이터 로드를 순차적으로 실행
const loadInitialData = async () => {
try {
const token = await AsyncStorage.getItem("accessToken");
if (!token) {
navigation.reset({
index: 0,
routes: [{ name: "Login" }],
});
return;
}
await fetchProfile(); // 먼저 프로필 데이터 로드
await loadParticipations(); // 프로필 로드 후 참여 데이터 로드
} catch (error) {
console.error("데이터 로드 실패:", error);
}
};
loadInitialData();
// 이벤트 리스너 등록 (한 번만)
const matchCreatedListener = () => {
loadParticipations();
};
const matchUpdatedListener = () => {
loadParticipations();
};
matchEventEmitter.addListener("matchCreated", matchCreatedListener);
matchEventEmitter.addListener("matchUpdated", matchUpdatedListener);
return () => {
matchEventEmitter.removeListener("matchCreated", matchCreatedListener);
matchEventEmitter.removeListener("matchUpdated", matchUpdatedListener);
};
}, [fetchProfile, loadParticipations, navigation]);
// 나머지 컴포넌트 코드...
}
fetchProfile()과 loadParticipations()를 순차적으로 호출하여 동시에 여러 요청이 발생하는 것을 방지합니다.useEffect를 하나로 통합하여 중복 코드와 호출을 제거합니다.React Native 앱에서 토큰 기반 인증을 효율적으로 관리하기 위해서는:
이러한 접근 방식을 통해 사용자는 토큰 만료로 인한 불편함 없이 앱을 원활하게 사용할 수 있습니다.