Next.js 캐싱 때문에 Axios 인터셉터를 포기하라고? Axios adapter 설정으로 Next.js caching 지원 받기

Jihan·2024년 12월 16일
13
post-thumbnail

들어가며

특정 상황에서 서비스 중 토큰 리프레시 API가 두 번씩 호출되는 것 같다는 백엔드 엔지니어 분의 리포트가 있었습니다. 이를 해결하기 위해 토큰 리프레시 로직에 스케쥴링을 한다던가 하는 여러 방법들을 생각해보았지만, Next.js의 캐싱 기능을 활용하는 것이 가장 좋을 것 같다는 결론이 나왔습니다.

다만 토큰 리프레시 API를 axios로 요청하고 있었기 때문에 Next.js의 캐싱 기능을 제대로 지원받지 못하고 있었는데요. 오늘은 Next.js의 캐싱 기능을 axios를 사용해도 지원받을 수 있는지에 대해 이야기해볼까 합니다. (글 내용과 관련해서 도움을 주신 준일님, 현석님, 준환님, 승준님 감사합니다!)

👀 TL;DR
Next.js의 일부 캐싱 기능은 fetch API를 통해서만 사용할 수 있습니다. 기존 axiosXML 기반으로 캐싱 기능이 지원되지 않습니다.
24년 5월 axios에 배포된 fetch adapter를 적용하면 axios가 내부적으로 fetch API를 통해 통신하도록 설정이 가능하기 때문에, axios의 편의성과 Next.js의 캐싱 지원 모두 챙길 수 있습니다.

Next.js에서는 axios 대신 fetch를 써야하나?

Next.js에서는 여러 가지 캐싱 기능을 제공하고 있습니다. ⛓️ 관련 공식문서에서는 기본적으로 Request Memoization(React Cache), Data Cache, Full Route, Router Cache의 4가지 캐싱 모델을 제공하고 있다고 설명하고 있는데요.

오늘은 그 중에서 SSR 과정에서 데이터를 가져올 때의 캐싱 기능인 Request Memoization(React Cache)Data Cache에 대해서 이야기해볼까 합니다.

왜 이 두 가지냐구요? 그것은 이 두 가지 캐싱 기능이 JavaScript의 기본 제공 함수인 fetch API를 통해서만 제공되기 때문입니다. 문서 중간중간에는 ⛓️ ReactNext.js가 기본 제공되는 fetch API를 확장(extend)하여 제공하기 때문에, fetch API를 사용하면 Next.js에서 제공하는 캐싱 기능에 대한 인터페이스를 설정할 수 있다고 설명하고 있습니다.

또한 이러한 이유로 Next.js에서 제공하는 캐싱 기능을 제대로 활용하려면 fetch API를 사용해야 한다.고 설명하고 있습니다.

무슨 이야기인지 알겠습니다. 저도 캐싱 기능을 지원받고 싶으니 fetch API를 써야겠습니다.

근데 지금까지 써온 axios는 어떡하죠? axios는 자체적인 XML통신 기반이기 때문에 fetch API를 통한 캐싱 기능이 적용되지 못합니다. Base Url이나 default config 등의 설정을 자유롭게 할 수 있도록 도와주는 axios Instance 패턴, 저희를 위해 request를 가로채서 JWT 토큰을 헤더에 주입해주고 있는 request interceptor 패턴 등 유용한 기능들을 전부 포기해야하는 건가요?

Next.js의 캐싱 기능을 적극적으로 활용하는 것은 좋지만, fetch 기반으로 interceptor를 직접 전부 구현하는 건 음...(하기 직전까지 갔지만) 별로 하고 싶은 작업은 아닙니다. 그래서 axios를 그대로 사용하면서 Next.js의 캐싱 기능을 지원 받는 방법은 없을까 조사해보았습니다.

Axios의 fetch adapter 업데이트

다행히도 axios에서 비교적 최근(올해 중)에 ⛓️ fetch API에 대한 adapter 업데이트를 배포했습니다.

⛓️ PR의 커밋 내역을 보니 올해 4월부터 업데이트를 위한 작업이 이루어진 것 같습니다. ⛓️ 이런 이슈와 같이 오래된 fetch 지원에 대한 니즈가 해소된 것 같네요. ⛓️ axios의 레포지토리 README에서도 확인이 가능합니다.

// fetch로 작성한 Next.js 캐싱이 켜진 호출
fetch(URL, { cache: "force-cache" })

// axios adapter로 작성한 Next.js 캐싱이 켜진 호출
axios.get(URL, {
  adapter: "fetch",
  fetchOptions: { cache: "force-cache" }
})

코드에는 위와 같은 식으로 적용할 수 있습니다. adapter는 기본적으로 XML로 설정되어 있으며, 위와 같이 명시할 경우 명시한 adapter에 맞는 방식으로 동작하게 됩니다. fetchOptions 프로퍼티를 통해 fetch의 두 번째 인자인 options도 입력해줄 수 있죠.

이 방법이라면 Next.jsfetch API기반 캐싱도 활용하고, 제가 익숙하게 쓰던 axios interceptor도 포기하지 않아도 되겠죠.

근데 이거 왜 이렇게 레퍼런스가 없죠?

axiosfetch adapter를 적용해서 Next.js에서 사용하고 있다는 레퍼런스가 열심히 찾아봐도 찾기가 힘듭니다. 앞서서 적용하신 분들의 경험들을 날먹하려고 했는데 실패했습니다. 제가 원하는 동작을 하는지 직접 검증해봐야겠습니다.

axios adapter로 해결되는 게 맞는지 직접 확인해보기

크게 두 가지로 간단하게 확인해봤습니다. 첫 번째로 axios adapterfetchNext.jsfetch가 동일한지 코드를 간단하게 까봤고요. 두 번째로 fetch APIaxios adapted fetch의 런타임 동작을 비교해봤습니다.

axios의 fetch가 Next.js의 fetch가 맞아?

첫 번째 분석의 목적은 axios에서 adapter를 적용했을 때 내부적으로 동작하는 fetch가, Next.js에서 확장해서 제공한다는 그 fetch와 같은지 확인하는 것입니다. 일단 Next.js에서 fetch를 확장하는 과정을 살펴봅시다.

⛓️ Next.js v15.1.0 tag에서 patch-fetch라는 코드를 봅시다. 1,000 줄에 육박하는 코드이지만 전부 볼 필요는 없고, 맨 마지막 코드만 보면 될 것 같습니다.

export function patchFetch(options: PatchableModule) {
  // If we've already patched fetch, we should not patch it again.
  if (isFetchPatched()) return

  // Grab the original fetch function. We'll attach this so we can use it in
  // the patched fetch function.
  const original = createDedupeFetch(globalThis.fetch)

  // Set the global fetch to the patched fetch.
  globalThis.fetch = createPatchedFetcher(original, options)
}

Next.jsglobalThis를 통해 globalThis.fetch와 같은 식으로 기존의 fetch 객체에 접근합니다. globalThis에 대해 궁금하신 분들은 ⛓️ 자바스크립트에서 globalThis의 소름끼치는 폴리필을 읽어보시면 도움이 될 것 같습니다.

아무튼 코드를 살펴보면, 원본 fetch 함수를 original에 저장한 뒤, 이를 통해 새로운(patched) fetch함수를 생성하여 다시 globalThis.fetch에 할당해주고 있습니다. 간단한 오버라이드입니다. 브라우저에서는 전역 객체인 window에, node 환경에서는 global에 이것이 담길 것이고, Next.js 환경에서는 전역 컨텍스트에서 추가적인 설정 없이 fetch를 실행하면 이 함수가 호출될 것입니다.

이번엔 ⛓️ axiosfetch adapter 코드를 살펴보겠습니다.

export default isFetchSupported && (async (config) => {

// ...

    let response = await fetch(request); // line 170

// ...
  
    return await new Promise((resolve, reject) => { // line 203
      settle(resolve, reject, {
        data: responseData,
        headers: AxiosHeaders.from(response.headers),
        status: response.status,
        statusText: response.statusText,
        config,
        request
      })
      
// ...
      
}

코드의 170번째 줄을 보면, fetch를 호출하고 있습니다. 그리고 그 반환값을 settle함수로 axios response 형태로 wrapping하여 반환하고 있습니다. 별 특별한 내용 없이 fetch를 내부적으로 사용하고 있는 것입니다. (+ axios에서 fetchglobalThis를 검색해보니, 추가적인 오버라이딩 작업은 없었던 것으로 확인됩니다.)

axiosNext.js의 코드를 간단하게 살펴본 결과, 둘이 사용하는 fetch함수는 같은 것으로 보입니다.

동일하게 동작하는지 테스트해보기

코드상으로 같은 아이인 것 같으니 동작도 동일하게 하겠죠? 테스트해봅시다. 제가 궁금한 것은 axiosfetch adapter를 사용하면 Next.jsRequest MemoizationData Cache를 지원받을 수 있는가입니다. 이를 확인하기 위해서 Open API⛓️ jsonplaceholder에서 포스트 목록을 불러오는 getPosts함수를, fetchaxios의 사용 여부, cache에 대한 설정 여부를 두고 총 4가지 조건으로 각 캐싱 기능을 테스트해보았습니다. 조건별로 아래와 같은 함수로 선언해주었습니다.

  • fetch(no cache)
const getPosts = async () =>
  fetch("https://jsonplaceholder.typicode.com/posts")
    .then((res) => res.json());
  • fetch(with cache)
const getPosts = async () =>
  fetch("https://jsonplaceholder.typicode.com/posts", {
    cache: "force-cache",
  }).then((res) => res.json());
  • axios
const getPosts = async () =>
  axios
    .get("https://jsonplaceholder.typicode.com/posts")
    .then((res) => res.data);
  • axios(with fetch adapter & cache)
const getPosts = async () =>
  axios
    .get("https://jsonplaceholder.typicode.com/posts", {
      adapter: "fetch",
      fetchOptions: { cache: "force-cache" },
    })
    .then((res) => res.data);

함수를 호출하는 컴포넌트의 경우 테스트 목적이 되는 캐싱 기능에 따라 다르게 작성했습니다.

Data Cache 테스트

Next.jsData Cache는 이미 요청된 데이터에 대해 다시 요청할 경우 Next.js 서버 내의 캐시 레이어를 통해 이전 요청의 결과를 반환하도록 해줍니다.

const PostList1 = async () => {
  const posts: Post[] = await (async () => {
    const now = new Date();
    return getPosts().then((res) => {
      console.log(
        `📊 data fetched in ${new Date().getTime() - now.getTime()}ms.`
      );
      return res;
    });
  })();

  return <div />;
};

동일한 요청을 여러 번 하는 환경을 구축하고 싶었기 때문에, 위와 같이 SSR 과정에서 데이터를 호출하도록 하였어요. 클라이언트에게 페이지를 렌더링해서 전송하는 과정에서 서버가 getPosts를 통해 데이터를 불러오고 이를 화면에 반영합니다. 위 컴포넌트의 콘솔 출력은 서버 콘솔로 이루어지게 됩니다.

이제 내부의 getPosts함수를 네 가지 조건에 맞춰서 바꿔가면서 서버를 재시작한 뒤 초기 렌더링을 포함한 3회의 새로고침을 해주었습니다.

  • fetch(no cache)

  • fetch(with cache)

  • axios

  • axios(with fetch adapter & cache)

Request Memoization 테스트

Request Memoization은 한 번 캐싱한 내용을 다음 호출 시기에도 불러오는 Data Cache와 다르게, 한 번의 호출 과정에서 생기는 중복된 호출을 한 가지로 통일해줍니다. 예를 들어, 컴포넌트 트리에서 동일한 데이터 요청을 다른 컴포넌트에서 할 경우 각각의 호출이 실행되지 않고 하나의 호출 결과를 여러 컴포넌트가 공유하게 됩니다.

const PostList2 = async () => {
  const posts1: Post[] = await (async () => {
    const now = new Date();
    return getPosts().then((res) => {
      console.log(
        `📊 data 1 fetched in ${new Date().getTime() - now.getTime()}ms.`
      );
      return res;
    });
  })();

  const posts2: Post[] = await (async () => {
    const now = new Date();
    return getPosts().then((res) => {
      console.log(
        `📊 data 2 fetched in ${new Date().getTime() - now.getTime()}ms.`
      );
      return res;
    });
  })();

  return <div />;
};

이를 테스트하기 위해 위와 같이 한 컴포넌트 내에서 같은 함수로 두 개의 데이터를 호출하도록 하고, 각각의 호출에 얼마만큼의 시간이 소요됐는지 출력되도록 작성해주었습니다.

  • fetch(no cache)

  • fetch(with cache)

  • axios

  • axios(with fetch adapter & cache)

테스트 결과

유형fetch(no cache)fetch (with cache)axiosaxios fetch adapter(with cache)
Data CacheXOXO
Reqeust MemoizationO?OXO

결과는 자연스러웠습니다. 코드상으로 살펴본 것과 같이 axiosfetch adapter를 추가하여 설정한 것은 fetch API를 사용한 것과 (적어도 제가 확인해본 범위에서는) 동일한 동작을 보였습니다.

마치며

axiosfetch adapter를 설정하여 Next.js의 캐싱을 지원받을 수 있다는 것을 코드 상으로도 확인하였고, 테스트에서도 확인할 수 있었습니다.

따라서 저는 익숙한 axios의 기능과 Next.js의 캐싱을 모두 활용할 수 있는 이 방법을 우선 선택하며, 필요 시 다른 대안을 검토할 것 같습니다.

저는 JavaScript 생태계에서 개발을 시작한 지 오래되지 않아서, 이렇게 라이브러리 생태계가 서로 의존하면서 발전하는 과정이 새롭게 다가옵니다. 라이브러리 레벨에서 일종의 문법 설탕을 지원하면 이렇게 쉽게 해결할 수 있는 문제들을 복잡하게 인터페이스를 구축하게 될 때가 있는데요.

이번 글과 같은 경우에는 axiosfetch adapter 지원여부를 모르고 넘어갔을 수도 있을 것 같아요. 그렇다면 아마 (직접 짰으니) 비교적 안정성이 낮은 커스텀 유틸을 작성해 사용하게 되었겠지요. 가능하다면 많은 정보를 수용해 이 중에서 필요한 방법으로 문제를 해결하는 것이 생산적인 것 같습니다.

아무튼 도움이 되셨길 바라며, 긴 글 읽어주셔서 감사합니다!

글의 내용뿐 아니라, 가독성 등에 대한 피드백 또한 언제나 감사드립니다.

profile
DIVIDE AND CONQUER

2개의 댓글

comment-user-thumbnail
2025년 1월 21일

글 너무 재미있게 잘 읽었습니다!

1개의 답글

관련 채용 정보