Next.js 14에서 Axios를 버리고 Fetch로 전환한 이유

Next.js 14에서 Fetch를 사용하는 이유와 그 한계, 그리고 커스텀 Fetch 함수를 구현하는 방법에 대한 포스팅입니다.

일반적으로 데이터 패칭을 할 때 Axios를 많이 사용합니다.

Axios는 응답에 대한 자동 파싱, 인터셉터를 제공하며 직관적으로 사용할 수 있기 때문에 많이 사용되며, 저 또한 많은 프로젝트에서 Axios를 활용해왔습니다.

하지만 Next.js는 표준 Fetch 함수를 사용하는 것을 권장하고 있습니다. Next.js 공식문서 참고

그 이유는 웹 표준인 Fetch가 Axios보다 더 가볍고, Next.js 서버에서 제공하는 캐시 관련 기능을 자연스럽게 활용할 수 있기 때문입니다. Axios를 사용할 경우 직접 캐싱에 관련한 설정을 해줘야 하는 불편함이 있습니다. 또한, 서버 사이드 렌더링(SSR) 과정에서 Fetch의 기본 기능을 활용하면 추가적인 설정 없이도 효율적인 데이터 패칭이 가능합니다.

현 상황에서 렌더링을 서버에서 수행하는 과정이 많아진 만큼, 굳이 Axios를 사용해야 할 이유가 줄어들었습니다.

하지만 Fetch는 이런 점이 아쉽습니다.

Fetch는 매번 코드를 작성할 때 보일러플레이트 코드가 존재합니다.

export default async function Component() {
  try {
    const response = await fetch("https://api.example.com/data", {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
    });
 
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
 
    const data = await response.json();
 
    return <div>{JSON.stringify(data)}</div>;
  } catch (error) {
    return <div>Error: {error.message}</div>;
  }
}

위와 같이 기본적인 Fetch 사용 시, 매번 base URL을 포함한 전체 주소를 적어야 하고, HTTP 메서드, 헤더 설정, JSON 직렬화 및 오류 처리 등 여러 가지 추가 작업이 필요합니다. 이러한 보일러플레이트 코드는 코드의 중복을 유발하고, 유지보수를 어렵게 만듭니다.

이런 부분을 해결하기 위해 커스텀 Fetch 함수를 만들었으며, 이를 공유하고자 합니다.

1. 1. 토큰 갱신 관리

let isRefreshing = false; // 현재 토큰 갱신이 진행 중인지 여부를 나타냅니다.
let refreshPromise: Promise<string> | null = null; // 토큰 갱신 프로미스를 저장하여, 동시 요청이 동일한 갱신 프로세스를 공유하도록 합니다.

2. 토큰 갱신

if (token && isTokenExpired(token)) {
  // 토큰 만료 확인: 현재 토큰이 존재하고 만료되었는지 확인합니다.
 
  if (!isRefreshing) {
    // isRefreshing이 false일 때만 postRefreshToken을 호출하여 새로운 토큰을 발급받습니다. 이때 refreshPromise에 프로미스를 저장합니다.
    isRefreshing = true;
    refreshPromise = postRefreshToken()
      .then((refreshResult) => {
        if (refreshResult.accessToken && refreshResult.refreshToken) {
          setAppCookie(accessTokenKey, refreshResult.accessToken);
          setAppCookie(refreshTokenKey, refreshResult.refreshToken);
          return refreshResult.accessToken;
        } else {
          throw new Error("Invalid token response from refresh");
        }
      })
      .catch((error) => {
        throw new Error(error.message);
      })
      .finally(() => {
        isRefreshing = false;
        refreshPromise = null;
      });
  }
 
  try {
    token = await refreshPromise!; // 여러 요청이 동시에 들어올 때, isRefreshing이 true이면 기존의 refreshPromise를 await 하여 토큰 갱신이 완료될 때까지 기다립니다.
  } catch (error) {
    throw error;
  }
}

3. Fetch 요청 설정 및 실행

const headers: HeadersInit = {
  Accept: "application/json",
  "Content-Type": "application/json",
  "Cache-Control": "no-cache",
  ...(token && { Authorization: `Bearer ${token}` }),
};
 
const requestOptions: RequestInit = {
  method,
  headers,
  cache: revalidate ? "force-cache" : "no-cache",
  ...(revalidate ? { next: { revalidate } } : {}),
  ...(tags ? { next: { tags } } : {}),
  ...(body && typeof body === "object" ? { body: JSON.stringify(body) } : {}),
};
 
const res = await fetch(apiUrl, requestOptions);

이러한 customFetch 함수는 다음과 같이 사용할 수 있습니다.

GET 요청 예시

async function searchProducts(keyword: string, limit: number) {
  return await api.get({
    url: "products/search",
    query: { keyword, limit },
    revalidate: 120, // 2분마다 캐시 재검증
    tags: ["products", "search"],
  });
}

POST, PUT, PATCH 요청 예시

async function createUser(userData: { name: string; email: string }) {
  return await api.post({
    //put, post, patch 동일
    url: "users/create",
    body: userData,
    tags: ["user", "create"],
  });
}
 
ex: createUser({ name: "Jane Doe", email: "jane@example.com" });

DELETE 요청 예시

async function deleteUser(userId: string) {
  return await api.delete({
    url: `users/delete/${userId}`,
    tags: ["user", "delete"],
  });
}

다음과 같이 사용할 때 useQuery, useMutation과 결합하면 더욱 더 강력하게 사용할 수 있습니다.

추가로 try, catch문을 활용하여 에러 핸들링에 대한 처리도 추가로 할 수 있습니다.

또한 RSC를 통한 SSR에서는 다음과 같이 사용할 수 있습니다.

export default async function Main() {
  const cardData = await getFirstCard();
 
  return (
    <div className={styles.Container}>
      <MainClient cardData={cardData} />
    </div>
  );
}

이를 통해 fetch에서도 다양한 캐시 기능을 활용할 수 있으며 401 등의 에러를 처리할 수 있습니다.

다들 Next.js와 호환이 잘되는 fetch를 사용하세요!