Next.js의 캐싱 - fetchCache 자세히 알아보기
안녕하세요?
오늘은 Next.js의 캐싱 부분인 fetchCache 기능에 대해 자세히 알아 보겠습니다.
** 목차 **
- Next.js의 캐싱 - fetchCache 자세히 알아보기
Next.js 앱 라우터는 전역 fetch
함수에 어떤 패치를 적용하여 데이터 가져오기를 자동으로 최적화합니다.
여기서 추가된 기능은 바로 가져온 데이터를 캐시하고 재사용하는 최적화인데요.
캐시된 데이터는 필요한 경우 임의의 타이밍에서 Revalidate(데이터 다시 가져오기 및 캐시 업데이트)할 수 있습니다.
오늘은 fetch
함수를 통해 캐시되는 "fetchCache"에 대해 어떻게 다루어야 하는지 알아보겠습니다.
아래 공식 문서를 참조해 주세요.
Next.js 공식 문서 - 데이터 가져오기: 가져오기, 캐싱 및 재유효화
데이터 가져오기가 캐시되는 타이밍
데이터 가져오기가 캐시되는 타이밍은 두 가지입니다.
- 빌드 시: Next.js 앱을
next build
로 빌드한 경우 - 요청 시:
next start
로 시작한 Next.js 앱에 브라우저에서 요청이 발생한 경우
이 캐시되는 타이밍은 Pages Router의 ISR과 유사합니다.
Pages Router는 "페이지 단위" 캐시였지만, App Router에서는 "데이터 가져오기 단위"로 캐시되어 더 세밀한 캐시 전략을 적용할 수 있습니다.
fetchCache를 필요할 때마다 삭제하기
앞서 언급한대로 next build
로 빌드한 Next.js 앱을 next start
하지 않으면 캐시 동작을 확인할 수 없습니다.
따라서 개발 서버로 시작한 next dev
는 제외해야 합니다.
또한 검증 중에 이전 빌드에서 캐시된 파일이 그대로 남아있는 점에 주의해야 하는데요.
로컬 개발 환경에서 캐시는 .next/cache/fetch-cache
폴더에 정적 파일로 생성됩니다.
예를 들어 다음과 같은 형식의 파일입니다(prettier 적용한 상태):
.next/cache/fetch-cache/ee9c18264e5cc119a6db9334ae99a0e26fb6975947d8332197a5a152e77f2248
{
"kind": "FETCH",
"data": {
"headers": {
// ...
},
// base64로 인코딩된 본문
"body": "eyJ0aW1lIjoiMjAyMy0wNy0wOFQwNDo1ODoxMC41NDdaIiwibWV0aG9kIjoiR0VUIn0=",
"status": 200,
"tags": [
"/page"
]
},
"revalidate": 31536000
}
이 캐시 파일이 그대로 남아있으면 변경 사항을 확인 할수 없으니까 설정 값을 변경할 때마다 다음 명령으로 깨끗한 빌드로 재시작해야 합니다.
rm -rf .next && npm run build && npm start
fetchCache 생성 시점
Next.js는 "빌드 시 가능한 한 많은 것을 캐시"하는 방향으로 설계되었습니다.
빌드 시에는 레이아웃과 페이지를 사전 렌더링하고, fetch()가 실행될 때 fetchCache가 생성됩니다.
예를 들어 다음과 같은 경우 dataA가 캐시 가능하면 .next/cache/fetch-cache에 해당 파일이 빌드 시에 생성되는 것을 확인할 수 있습니다.
이러한 캐시 생성을 빌드 환경에서 API 서버와 통신할 수 있도록 설정해야 합니다.
import { cookies } from "next/headers";
export default async function Page() {
const dataA = await fetchDataA(); // <- 캐시됨 (정적 데이터의 경우)
return "...";
}
Dynamic Functions은 "빌드 시 캐시해서는 안 되는 경계"로 판단되는 함수입니다.
예를 들어 cookies()는 요청 헤더의 쿠키를 참조하는 함수입니다.
이는 "빌드 시점에서 어떤 요청인지를 특정할 수 없다"는 의미입니다.
Dynamic Functions이 빌드 시 데이터 가져오기 중에 실행되면 이후의 데이터 가져오기는 중단됩니다.
즉, Dynamic Functions 사용 후의 dataB는 정적 데이터일지라도 빌드 시 캐시의 대상이 아닙니다.
import { cookies } from "next/headers";
export default async function Page() {
const dataA = await fetchDataA(); // <- 캐시됨 (정적 데이터의 경우)
const cookieStore = cookies(); // <- 이후 캐시 중단하는 Dynamic Function
const dataB = await fetchDataB(); // <- 캐시되지 않음 (정적 데이터일지라도)
return "...";
}
요청 시점에 캐시되는 fetchCache
빌드 시 Dynamic Functions의 영향으로 캐시되지 않았던 dataB입니다.
그러나 계속해서 캐시되지 않는 것은 아닙니다.
다음과 같이 fetch 함수 옵션에 { cache: "force-cache" }
를 지정하는 경우 요청 시에 캐시됩니다.
원하는 데이터를 캐시하려면 명시적으로 force-cache를 지정하면 됩니다.
export async function fetchDataB() {
const res = await fetch("https://example.api.com/api/data-b", {
cache: "force-cache",
});
const data = await res.json();
return data;
}
이 요청 시점에서 캐시 생성을 확인하려면 해당 페이지를 누군가가 조회해야 합니다.
페이지를 조회한 순간 .next/cache/fetch-cache에 정적 캐시 파일이 추가되는 것을 확인하면 요청 시점 캐시가 잘 작동하고 있는겁니다.
참고로 fetch 함수로 가져온 데이터가 캐시되는지 여부를 판단할 때 요청 메서드는 관계가 없습니다.
POST도 대상이므로 fetch를 통한 GraphQL 데이터도 fetchCache로 캐시됩니다(빌드 시 및 요청 시 공통).
fetchCache 설정 변경하기
지금까지 설명한 fetchCache에 대해 Next.js에서 제공하는 기본 설정을 그대로 사용했습니다.
그러나 이 fetchCache 설정은 필요에 따라 변경할 수 있으며, fetch 함수의 { cache }
옵션을 일괄적으로 수정합니다.
Layout 또는 Page 파일에서 다음과 같이 fetchCache를 export합니다.
export const fetchCache = "auto";
// 'auto' | 'default-cache' | 'only-cache'
// 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store'
이 fetchCache 설정은 세그먼트별로 설정할 수 있으며, 세그먼트에 중첩된 모든 Route가 해당 대상입니다.
복잡한 상황을 피하기 위해 이 글에서는 RootLayout에만 설정되어 있다고 가정하겠습니다(만약 RootLayout에 설정하면 모든 fetch 함수가 해당 대상이 됩니다).
또한 공식 문서에도 언급되어 있듯이 이는 고급 설정이므로 실제 필요한 시점에 지정하시기 바랍니다.
【1】 only-no-store / only-cache: 절대로 캐시하고 싶지 않은 경우(하고 싶은 경우)
"only-no-store"를 지정하면 개별 fetch 함수에 { cache: "force-cache" }
가 지정되어 있으면 빌드 시 오류가 발생합니다.
완전히 인증 요건으로 구성된 프로젝트의 경우 { cache: "force-cache" }
가 개별 지정되는 것은 잘못된 경우일 가능성이 높으므로 빌드 오류로 인지할 수 있습니다.
export const fetchCache = "only-no-store";
반대로 "only-cache" 지정은 개별 fetch 함수에 { cache: "no-store" }
가 지정되어 있으면 빌드 시 오류를 발생시킵니다.
export const fetchCache = "only-cache";
이 only-* 지정은 "Dynamic Functions" 사용 전에만 유효 범위가 있습니다.
따라서 Dynamic Functions를 넘어서면 빌드 시 체크를 피할 수 있으므로 현재로서는 그리 효과적이지 않은 것 같습니다.
【2】 force-cache / force-no-store: 개별 지정을 강제로 덮어쓰고 싶은 경우
개별 fetch 함수에 지정된 { cache }
옵션을 모두 덮어씁니다.
// 모두 `{ cache: "no-store" }`로 덮어쓰기
export const fetchCache = "force-no-store";
// 모두 `{ cache: "force-cache" }`로 덮어쓰기
export const fetchCache = "force-cache";
개별 지정은 "의도적으로" 지정되어 있을 것으로 예상되므로 이 설정을 사용하는 경우는 드문 경우라고 생각합니다.
많은 fetch 함수에 하나씩 작성하는 것이 귀찮은 경우 유용할 수 있습니다.
또는 제3 라이브러리를 사용하고 있어 그 안에 작성된 설정을 덮어써야 하는 경우도 고려될 수 있습니다.
【3】 default-no-store: 기본적으로 캐시하고 싶지 않은 경우
기본적으로 캐시하지 않지만, 개별 fetch 함수에 { cache: "force-cache" }
가 지정된 경우에만 캐시됩니다.
캐시되는 시점은 Dynamic Functions 사용 전후에 따라 "빌드 시간" 또는 "요청 시간"으로 전환됩니다.
export const fetchCache = "default-no-store";
【4】 default-cache: 기본적으로 캐시를 사용하고 싶습니다
기본적으로 캐시를 사용하지만, 개별 fetch 함수에 { cache: "no-store" }
가 지정된 경우에만 캐시를 차단합니다.
캐시되는 시점은 Dynamic Functions 사용 전후의 fetch 함수 사용으로 인해 "빌드 시간 또는 요청 시간"으로 전환됩니다.
fetchCache 설정을 아무것도 지정하지 않은 경우 (auto)
와 거의 동일하지만 "Dynamic Functions를 건너뛰어도 요청 시에 캐시"하는 점이 다릅니다.
export const fetchCache = "default-cache";
이 fetchCache 설정을 사용하는 경우, { cache: "no-store" }
의 개별 지정을 잊어버리면 개인에 연결된 데이터 (동적 데이터)도 캐시될 수 있으므로 주의가 필요합니다.
【5】 auto: Next.js의 기본 설정
Next.js에 설정된 기본 fetchCache입니다.
default-cache와 거의 동일하지만 "Dynamic Functions를 건너뛰면 요청 시에 캐시하지 않음"이라는 점이 다릅니다.
요청 시에 캐시를 사용하려면 개별 fetch 함수에 { cache: "force-cache" }
를 지정해야 합니다.
export const fetchCache = "auto";
fetchCache를 안전하게 활용하는 방법
Next.js는 빌드 시간에 캐시를 적극적으로 사용해야 한다는 철학처럼 보입니다.
생각해 봤을 때 "auto보다 default-cache가 Next.js의 철학에 더 가까운 것은 아닐까?"라는 의문을 품었습니다.
auto는 "Dynamic Functions를 건너뛰면 요청 시에 캐시하지 않음"으로, 캐시되길 원하는 정적 데이터를 가져오려면 개별 fetch 함수에 { cache: "force-cache" }
를 지정해야 합니다.
이는 번거로운 일처럼 느껴질 수 있지만, 저는 안전성을 우선시한 결과라고 생각합니다.
요청 시에만 캐시할 수 있는 데이터 가져오기는 Dynamic Functions 사용 후에만 제한됩니다.
헤더 정보를 사용하여 데이터를 가져오든 말든, 이후의 처리는 개인에 연결된 데이터를 가져오는 경우가 많습니다.
이러한 배경 때문에 "force-cache 지정이 없는 데이터의 자동 캐시는 Dynamic Functions를 경계로 하여 중단"하는 판단은 이해됩니다.
이 auto 설정은 안전성과 편의성을 모두 고려한 결과라고 이해할 수 있습니다.
그러나 명백한 구현을 희생한 것처럼 느껴집니다.
공식 문서에서는 이해하기 쉽도록 "fetch()는 자동으로 { cache: "force-cache" }
가 설정됩니다"라고 설명을 (일부에서) 생략했지만, 실제로는 Dynamic Functions의 영향이 큽니다.
이는 기본적으로 적용되는 auto 동작을 충분히 설명하지 못해 혼란을 야기할 수 있습니다.
앞으로 App Router fetchCache를 고려할 때, 다음과 같은 방침을 기본적으로 적용할 것으로 생각합니다.
캐시할지 여부를 자동으로 결정하지 않고, 의도적으로 force-cache 또는 no-store를 지정하려고 합니다.
- 인증 요구 사항이 많은 경우 캐시를 신중하게 사용하려고 합니다.
- export const fetchCache = "default-no-store"를 설정합니다.
- 캐시되길 원하는 데이터를 가져오려면 반드시 개별적으로 force-cache를 지정합니다.
- 인증 요구 사항이 적은 경우 캐시를 적극적으로 사용하려고 합니다 (사고에 주의).
- export const fetchCache = "default-cache"를 설정합니다.
- 캐시되면 안 되는 데이터를 가져오려면 반드시 개별적으로 no-store를 지정합니다.
※참고: 여기서는 비교 요소를 줄이기 위해 일부로
{ revalidate: 0 }
옵션을 제외했습니다.
추가 내용
"Dynamic Functions가 개별 fetch cache 설정을 전환하는 경계"라는 내용을 기재했지만, 정확히는 약간 다릅니다.
여기서 말하는 Dynamic Functions는 next/headers에서 가져오는 cookies와 headers만을 의미합니다.
그러나 Next.js 문서에서는 Dynamic Functions에 searchParams도 포함됩니다.
이 searchParams 참조를 건너뛰기만 한다면, fetchCache의 자동 캐시 설정은 기본 force-cache 상태로 유지됩니다.
이러한 배경 때문에 우리는 안전성을 우선시한 결과로 이러한 동작을 생각했습니다.
끝.