[Next.js] Axios에서 Fetch API로 전환하여 캐싱 기능 사용하기

YouGyoung·2024년 8월 4일
0

Axios의 명확한 단점

Next.js에서 권장하는 Fetch API는 Interceptors 기능을 제공하지 않기 때문에,
저는 주로 Axios의 Interceptors 기능을 사용하고 있습니다.

Axios는 캐싱을 지원하지 않아 따로 구현을 해줘야 하는 단점이 있습니다.

하지만, Interceptors 기능이 제공하는 여러 장점(예: API 요청 통합 처리 등) 때문에 Axios를 주로 사용하고 있습니다.

Axios의 캐싱 문제

axios-cache-adapter 라이브러리를 사용하여 Axios에서도 캐싱을 구현할 수 있다고 하여 적용해 보았습니다. 하지만 캐싱이 제대로 이루어지지 않아 원격 저장소에 지속적인 API 요청이 보내지는 문제가 발생했습니다.

그럼에도 불구하고 Axios를 사용하게 되는 이유는 Interceptors 기능 덕분입니다.

Route Handler를 적용한 프로젝트

Next.js에서 Route Handler를 사용하려면 GET, POST, PUT, DELETE 요청 각각에 대해 작성해야 합니다

이러한 API요청 별 코드 작성이 번거러워 Axios Interceptors를 사용했었는데요.

최근 참여한 프로젝트에서는 외부 API를 사용하지 않고 내부 API를 사용할 예정이었으며, 사용할 API 개수도 3개뿐이었습니다. 또한 처음 한 번만 해당 API를 이용해 데이터를 패칭하면 되는 프로젝트였습니다.

Axios에 너무 익숙해져 버린 탓에 이번에도 Axios Interceptors를 사용한 fetcher 함수를 만들고 시작을 했었습니다.

import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { MOCK_SERVER_URL } from "../consts/mock";

const axiosInstance = axios.create({ baseURL: MOCK_SERVER_URL });

axiosInstance.interceptors.request.use(
  (config) => config,
  (error) => Promise.reject(error)
);

axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (axios.isAxiosError(error)) {
      return Promise.reject(
        new Error(error.response?.data?.message || error.message)
      );
    }
    return Promise.reject(new Error("An unknown error occurred"));
  }
);

const fetcher = async (
  url: string,
  method: "get" | "post" | "put" | "delete" | "patch",
  headers?: Record<string, string>,
  params?: Record<string, any>,
  data?: any,
  withCredentials?: boolean
): Promise<any> => {
  try {
    const config: AxiosRequestConfig = {
      method,
      url: url,
      headers,
      params,
      withCredentials,
      ...(method !== "get" && data && { data }),
    };

    const response: AxiosResponse = await axiosInstance(config);
    return response;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(error.response?.data?.message || error.message);
    }
    throw new Error("알 수 없는 에러 발생");
  }
};

export default fetcher;

지금은 Postman Mock 서버를 이용하고 있어서 괜찮지만 실 서버를 이용하게 됐을 때 캐싱을 고려하지 않는다면 서비스 중에 서버 측 성능 저하가 나타날 수 있겠다 싶더라구요..

API 요청은 GET만 사용하니 Fetch API를 이용하면 번거롭게 캐싱을 구현하지 않아도 문제를 해결할 수 있었습니다.

// /src/app/api/store/route.ts
import { fetchWithFallback, MOCK_SERVER_URL, parseJSON } from "@/shared";

const fetchStoreInfo = async (storeId: number) => {
  try {
    const storeRes = await fetchWithFallback(
      `${MOCK_SERVER_URL}/stores/${storeId}`,
      "force-cache"
    );
    const storeData = await parseJSON(storeRes);
    return storeData;
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "알 수 없는 에러 발생";
    throw new Error(`getStoreInfo: ${message}`);
  }
};

const fetchMenu = async (storeId: number) => {
  try {
    const menuRes = await fetchWithFallback(
      `${MOCK_SERVER_URL}/menu/${storeId}`,
      "force-cache"
    );
    const menuData = await parseJSON(menuRes);
    return menuData;
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "알 수 없는 에러 발생";
    throw new Error(`getMenu: ${message}`);
  }
};

const fetchCategories = async (storeId: number) => {
  try {
    const categoriesRes = await fetchWithFallback(
      `${MOCK_SERVER_URL}/categories/${storeId}`,
      "force-cache"
    );
    const categoriesData = await parseJSON(categoriesRes);
    return categoriesData;
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "알 수 없는 에러 발생";
    throw new Error(`getCategories: ${message}`);
  }
};

export async function GET(request: Request) {
  try {
    const url = new URL(request.url, `http://${request.headers.get("host")}`);
    const storeId = Number(url.searchParams.get("storeId"));
    if (isNaN(storeId)) {
      return new Response(JSON.stringify({ error: "Invalid storeId" }), {
        status: 400,
        headers: { "Content-Type": "application/json" },
      });
    }

    const [storeInfo, menu, categories] = await Promise.all([
      fetchStoreInfo(storeId),
      fetchMenu(storeId),
      fetchCategories(storeId),
    ]);
    return new Response(JSON.stringify({ storeInfo, menu, categories }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  } catch (error) {
    console.error(error);
    const message =
      error instanceof Error ? error.message : "알 수 없는 에러 발생";
    return new Response(JSON.stringify({ error: message }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

테스트 시 캐시된 데이터를 사용하기 때문에 Mock 서버에서 데이터를 변경했을 때
'force-cache'에서 'no-store'로 변경해 줘야 하는 번거로움이 있었습니다.

이는 캐시된 데이터의 유효시간을 설정하면 해결될 부분이므로 추후 반영하려고 합니다.

그리고 fetchWithFallback 이라는 함수도 작성을 해줬는데요.
해당 함수는 캐시된 데이터에 에러가 있을 때 원격 저장소에 API 요청을 보내는 함수입니다.

const fetchWithFallback = async (url: string, cacheMode: RequestCache) => {
  try {
    const response = await fetch(url, { cache: cacheMode });
    if (!response.ok) {
      if (cacheMode === "force-cache") {
        console.error("캐시된 데이터 실패, no-cache로 재시도");
        return await fetch(url, { cache: "no-cache" });
      }
      throw new Error("HTTP 상태 코드: " + response.status);
    }
    return response;
  } catch (error) {
    console.error(`네트워크 요청 실패: ${error}`);
    throw error;
  }
};
export default fetchWithFallback;

조금 오래된 브라우저에서는 'no-store'만으로는 인식을 못하기 때문에 여기에서는 'no-cache' 값을 사용했습니다.

profile
프론트엔드 개발자

0개의 댓글

관련 채용 정보