Ramble-FE / lib / fetchWithAuth.ts
fetchWithAuth.ts
Raw
import { ResponseUtils, tokenStorage, TokenUtils } from "@/utils";
import Constants from "expo-constants";
import { Platform } from "react-native";

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

export async function fetchWithAuth(
    input: RequestInfo,
    init: RequestInit = {}
): Promise<Response> {
    let storedAccess = await tokenStorage.getAccessToken();

    const makeRequest = async (token?: string) => {
        const headers = {
            ...(init.headers || {}),
            ...(token ? { Authorization: `Bearer ${token}` } : {}),
            ...DEFAULT_HEADERS,
        };

        const url =
            typeof input === "string" && input.startsWith("/")
                ? `${API_BASE_URL}${input}`
                : input;
                
        return fetch(url, {
            ...init,
            headers,
        });
    };

    const reissueToken = async (): Promise<string | null> => {
        try {
            const reissueResponse = await fetch(`${API_BASE_URL}/auth/reissue`, {
                method: 'POST',
                credentials: 'include' as RequestCredentials,
                headers: DEFAULT_HEADERS,
            });

            if (!reissueResponse.ok) {
                throw new Error("토큰 재발급 실패");
            }

            const tokens = TokenUtils.extractTokensFromHeaders(reissueResponse.headers);
            await tokenStorage.setAccessToken(tokens.accessToken);

            return tokens.accessToken;
        } catch {
            await tokenStorage.removeAccessToken();

            return null;
        }
    };

    // 1. 사전 토큰 만료 체크 및 갱신 (네트워크 요청 최적화)
    if (storedAccess && TokenUtils.isTokenExpired(storedAccess)) {
        const newToken = await reissueToken();
        if (newToken) {
            storedAccess = newToken;
        } else {
            storedAccess = null;
        }
    }

    // 2. 메인 요청 실행
    let response = await makeRequest(storedAccess ?? undefined);

    // 3. 서버 응답 기반 토큰 재발급 (사전 체크 실패 케이스 대응)
    if (response.status === 401) {
        const res = await ResponseUtils.parseResponse(response);
        if (res.message !== "Expired Access Token") {
            return response;
        }

        const newToken = await reissueToken();

        if (newToken) {
            response = await makeRequest(newToken);
        }
    }

    return response;
}