본문 바로가기
source-code/Next JS

[next js] App Router Fetch Cache가 동작하지 않을 때

by mattew4483 2024. 3. 11.
728x90
반응형

Data Fetching

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating

 

Data Fetching: Fetching, Caching, and Revalidating | Next.js

Learn how to fetch, cache, and revalidate data in your Next.js application.

nextjs.org

Next js App router는 fetch web api를 확장,

caching, revalidating 등의 기능을 제공해 각 요청을 자동으로 memoize 할 수 있도록 합니다.

 

async function getData() {
  const res = await fetch('https://api.example.com/...')
  // The return value is *not* serialized
  // You can return Date, Map, Set, etc.
 
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}
 
export default async function Page() {
  const data = await getData()
 
  return <main></main>
}

 

즉 기존에는 서버 데이터 상태 관리를 위해

애플리케이션 내부적으로 useEffect+useState 조합을 사용하거나

react-query, swr 등의 라이브러리를 사용했다면...

 

App router 기반에서는 기본 web api인 fetch 만으로도

서버 데이터 상태 관리에 필요한 동작들을 어느 정도 수행할 수 있게 된 것이죠!

 

공식 문서에서는 데이터 패칭 전략을 위와 같이 제시하고 있습니다.

간단히 요약하자면, 해당 데이터가 필요한 컴포넌트에서, 직접 API를 호출해 데이터를 조회해라!라는 것인데...

 

이때 fetch는 자동으로 각 request를 memoize 하기 때문에,

불필요한 네트워크 호출이나 성능 저하를 걱정할 필요 없다 작성되어 있습니다.

 

문제 상황

현재 Next 14로 구성된 Admin 페이지에서, 하나의 데이터를 여러 자식 컴포넌트에서 필요로 하는 상황이 발생했습니다.

 

물론 최상단 부모 컴포넌트에서 API 요청을 한 후, 자식 컴포넌트들에 props로 전달해도 되지만...

이 경우 UI 구조상 sequential data fetching이 발생하는 문제가 있었습니다.

 

따라서 A페이지 렌더링 시,

동일한 데이터 요청을 보내는 자식 컴포넌트 a, b가 존재하게끔 fetching 전략을 세웠습니다.

const page = ({ params }: { params: { id: string } }) => {
  return (
    <>
      <A id={params.id} />
      <B id={params.id} />
    </>
  );
};

const A = ({id}:{id:string}) => {
    const data = getData(id)
    return <></>
};

const B = ({id}:{id:string}) => {
    const data = getData(id)
    return <></>
};

const getData = async (id:string) => {
    console.log('fetch')
    const response = await fetch()
    return await response.json
};

해당 page에 들어올 경우, getData 요청에 해당하는 fetch request가 한 번만 이뤄질 것으로 기대했지만...

실제 backend server에 로그를 추가한 결과, 네트워크 요청이 2번씩 발생하고 있었습니다!

 

즉 단순히 getData의 console이 2번 동작하는 것이 아니라 (caching이 되고 있어도, 해당 함수는 두 번 동작하는 게 정상)

실제 remote server에 요청이 2번 씩 가고 있었던 것이며,

이는 fetch caching이 정상적으로 동작하지 않고 있음을 의미했습니다. 왜 이런 일이 일어난 것일까요?

 

원인

원인 파악을 위해 공식 문서를 다시 읽었습니다.

 

https://nextjs.org/docs/app/api-reference/functions/fetch

 

Functions: fetch | Next.js

API reference for the extended fetch function.

nextjs.org

 

fetch에는 cache와 관련된 옵션들이 존재하는데, 간단히 요약하자면 다음과 같습니다.

 

1. force-cache

default. fetch의 request와 일치하는 cache 가 존재할 경우, 해당 cache 데이터를 반환합니다.

(이때 fetch의 request는 url + options를 의미)

 

일치하는 cache가 존재하지 않을 경우, remote server에 요청을 보낸 후, fetch 함수 반환값을 캐싱합니다.

 

equest time에만 알 수 있는 동적 함수(ex Cookie 조회, Header 조회, searchParams 조회)가 사용된 경우,

cache 옵션의 기본값은 no-cache입니다.

(각 요청마다 fetch request가 달라질 것으로 간주, 기본값으로 cache 하지 않는 듯합니다)

 

2. no-store

매 요청마다 remote server에 요청을 보냅니다.


 

아하...! Good to know를 스치듯 읽고 넘어간 것이 원인이었습니다.

 

현재 Admin 페이지는 인증을 위해

각 API Request의 Authorization header를 통해 Access Token을 전달하는데

해당 토큰을 Cookie에 저장하고 있었습니다.

 

→ 즉 getData 함수 내부적으로 Cookie 조회 함수를 사용하고 있었으며...

이로 인해 cache 기본값이 no-cache로 설정되어버리고 만 것이죠

import { cookies } from "next/headers";

const getData = async (id:string) => {
    console.log('fetch')
    // cookies()로 인해 default cache X
    const accessToken = cookies().get(AuthKeyName.ACCESS_TOKEN)?.value;
    const response = await fetch()
    return await response.json
};

 

해결 방안

cache를 해주면 됩니다!

import { cookies } from "next/headers";

const getData = async (id:string) => {
    console.log('fetch')
    const accessToken = cookies().get(AuthKeyName.ACCESS_TOKEN)?.value;
    const response = await fetch(url, {
    	cache: 'force-cache' 
    })
    return await response.json
};

기본값이  no-cache인 것이지 caching 자체가 불가능한 것은 아니며,

위처럼 force-cache 옵션을 지정해 준 경우

backend server에서도 의도한 것과 같이, 네트워크 요청이 한 번만 들어오는 것을 확인할 수 있었습니다.

 

유의사항

하지만 위 작성한 것과 같이

fetch API의 cache 데이터는 URL + options를 통해 각 데이터를 식별하므로...

import { cookies } from "next/headers";

const getData = async (id:string) => {
    const accessToken = cookies().get(AuthKeyName.ACCESS_TOKEN)?.value;
    const response = await fetch(url, {
    	headers: {Authorization: accessToken},
    	cache: 'force-cache' 
    })
    return await response.json
};

위와 같이 작성한 경우

1) 사용자마다 cookies를 통해 조회한 Access Token이 다르므로, 모든 사용자에 대해 최조 1회 네트워크 요청 발생

2) 각 사용자가 해당 페이지에 여러 번 접속하더라도(페이지가 여러 번 렌더링 되어도), 네트워크 요청 발생 X

3) access token 등을 통해 기존 토큰 만료 + 새로운 토큰으로 요청 시, 또다시 네트워크 요청 발생

과 같이 동작하게 될 테지요!

 

https://www.smashingmagazine.com/2022/07/new-pattern-jamstack-segmented-rendering/

 

A New Pattern For The Jamstack: Segmented Rendering — Smashing Magazine

“Segmented Rendering” is a new pattern and it will change the Jamstack game forever. Let’s try to push the boundaries of static rendering and learn how to apply it to personalized content.

www.smashingmagazine.com

해당 한계점을 극복하기 위해, Segmented Rendering과 같은 fetching 전략도 제시되고 있음을 찾아볼 수 있었습니다.

728x90
반응형