Ramble-FE / utils / tokenReissue.ts
tokenReissue.ts
Raw
import Constants from "expo-constants";
import { Platform } from "react-native";
import { cookieStorage } from "./cookieStorage";
import { tokenStorage } from "./tokenStorage";
import { TokenUtils } from "./tokenUtils";

const API_BASE_URL = Constants.expoConfig?.extra?.apiBaseUrl as string;
const DEFAULT_HEADERS = {
    "Content-Type": "application/json",
    "Ramble-Client-Platform": Platform.OS,
};

/**
 * 토큰 재발급 중복 호출 방지를 위한 Promise 메모이제이션
 * 여러 요청이 동시에 토큰 재발급을 시도해도 실제 API 호출은 1번만 수행
 */
let reissuePromise: Promise<string | null> | null = null;

const isWeb = Platform.OS === "web";

export const TokenReissueManager = {
    /**
     * 토큰 재발급 (중복 호출 방지)
     * @param refreshToken - Native 환경에서 사용할 refresh token (optional)
     * @returns 새로운 accessToken 또는 null (실패 시)
     */
    async reissueToken(refreshToken?: string): Promise<string | null> {
        if (!isWeb && !refreshToken) {
            console.warn("[TokenReissueManager] Native 환경에서 refresh token이 제공되지 않았습니다.");
            return null;
        }

        if (reissuePromise) {
            console.log("[TokenReissueManager] 진행 중인 토큰 재발급 대기");
            return reissuePromise;
        }

        console.log("[TokenReissueManager] 토큰 재발급 시작");

        // 새로운 재발급 Promise 생성
        reissuePromise = (async () => {
            try {
                const response = await fetch(`${API_BASE_URL}/auth/reissue`, {
                    method: "POST",
                    ...(isWeb ? { credentials: "include" as RequestCredentials } : {}),
                    headers: {
                        ...DEFAULT_HEADERS,
                        ...(!isWeb ? { "set-cookie": `refresh=${refreshToken}` } : {}), // Native 환경에서는 refresh token을 헤더로 전달
                    },
                });

                if (!response.ok) {
                    throw new Error(`토큰 재발급 실패: ${response.status}`);
                }

                const tokens = TokenUtils.extractTokensFromHeaders(response.headers);
                await Promise.all([
                    tokenStorage.setAccessToken(tokens.accessToken),
                    cookieStorage.setRefreshToken(tokens.cookieHeader),
                ]);

                console.log("[TokenReissueManager] 토큰 재발급 성공");
                return tokens.accessToken;
            } catch (error) {
                console.error("[TokenReissueManager] 토큰 재발급 실패:", error);
                await Promise.all([
                    tokenStorage.removeAccessToken(),
                    cookieStorage.removeRefreshToken(),
                ]);

                return null;
            } finally {
                // 완료 후 Promise 초기화하여 다음 재발급 가능하도록 함
                reissuePromise = null;
            }
        })();

        return reissuePromise;
    },

    /**
     * 현재 진행 중인 토큰 재발급이 있는지 확인
     */
    isReissuing(): boolean {
        return reissuePromise !== null;
    },
};