React Native에서 토큰 갱신 및 API 호출 최적화하기

oversleep·2025년 3월 3일

troubleshooting

목록 보기
14/19
post-thumbnail

React Native 앱에서 API 통신 시 토큰 기반 인증은 필수적입니다.
하지만 토큰 만료와 관련된 문제는 사용자 경험을 저해할 수 있습니다.
특히 여러 API 호출이 동시에 일어나는 경우, 토큰 갱신 메커니즘이 제대로 작동하지 않아 사용자가 갑자기 로그아웃되는 상황이 발생할 수 있습니다.

이 글에서는 React Native 앱에서 토큰 갱신 메커니즘을 최적화하고 API 호출을 효율적으로
관리하는 방법을 알아보겠습니다.

문제 상황

다음과 같은 문제 상황이 발생할 수 있습니다:

  1. 특정 화면(예: 마이페이지)에 접근할 때만 세션 만료 메시지가 나타납니다.
  2. 여러 API 요청이 동시에 발생하면 토큰 갱신이 제대로 이루어지지 않습니다.
  3. 중복된 이벤트 리스너나 API 호출로 인해 앱 성능이 저하됩니다.

해결책 1: Axios 인터셉터를 이용한 토큰 갱신 구현

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;

중요 포인트

  1. 별도의 리프레시 인스턴스: 토큰 갱신용 요청은 별도의 Axios 인스턴스를 사용하여 무한 루프를 방지합니다.
  2. 재시도 플래그: _retry 플래그를 사용하여 동일한 요청에 대한 중복 갱신을 방지합니다.
  3. 정확한 오류 처리: 토큰 만료를 정확히 감지하기 위해 상태 코드와 오류 메시지를 모두 확인합니다.
  4. 이벤트 기반 상태 관리: 토큰 갱신 및 로그아웃을 이벤트로 처리하여 앱 전체에서 일관된 상태를 유지합니다.

해결책 2: API 호출 최적화

여러 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]);
  
  // 나머지 컴포넌트 코드...
}

최적화 포인트

  1. 순차적 API 호출: fetchProfile()loadParticipations()를 순차적으로 호출하여 동시에 여러 요청이 발생하는 것을 방지합니다.
  2. 중복 제거: 여러 useEffect를 하나로 통합하여 중복 코드와 호출을 제거합니다.
  3. 효율적인 이벤트 처리: 이벤트 리스너를 한 번만 등록하고 적절히 정리합니다.
  4. 메모리 관리: 컴포넌트 언마운트 시 모든 이벤트 리스너를 정리합니다.

부가적인 팁

  1. 디버깅 로그 추가: API 요청과 응답에 로그를 추가하여 문제를 쉽게 추적할 수 있습니다.
  2. 토큰 유효성 미리 확인: 중요한 작업 전에 토큰의 유효성을 미리 확인하여 사용자 경험을 개선할 수 있습니다.
  3. 오프라인 모드 고려: 네트워크 연결이 불안정한 상황에서의 동작도 고려하여 설계합니다.

결론

React Native 앱에서 토큰 기반 인증을 효율적으로 관리하기 위해서는:

  1. Axios 인터셉터를 활용하여 강력한 토큰 갱신 메커니즘을 구현합니다.
  2. API 호출을 최적화하여 동시 요청으로 인한 문제를 방지합니다.
  3. 이벤트 기반 상태 관리를 통해 앱 전체에서 일관된 인증 상태를 유지합니다.

이러한 접근 방식을 통해 사용자는 토큰 만료로 인한 불편함 없이 앱을 원활하게 사용할 수 있습니다.

profile
궁금한 것, 했던 것, 시행착오 그리고 기억하고 싶은 것들을 기록합니다.

0개의 댓글