풀 라우트 캐시(Full Route Cache)
풀 라우트 캐시란 Next 서버 측에서 빌드 타임에 특정 페이지의 렌더링 결과를 캐싱하는 기능을 말한다.
그림과 같은 과정에서 /a 라는 경로를 갖는 페이지가 풀 라우트 캐시를 가질 수 있는 페이지라고 할때, 빌드 타임 시 사전렌더링과정에서 먼저 리퀘스트 메모이제이션과 데이터 캐시 등의 캐시 과정들을 거쳐서 렌더링이 완료가 되면 완료된 렌더링 결과를 Next 서버 측 풀 라우트 캐시라는 곳에 미리 저장하게 된다.
그래서 실제로 빌드 타임 후 서버를 가동하여 /a 라는 경로로 접속을 요청하게 되면 빌드 타임에 미리 렌더링해서 풀 라우트 캐시에 저장된 렌더링 결과를 불러오기 때문에 새롭게 렌더링할 필요가 없이 캐시된 페이지를 그대로 브라우저에게 전송하여 빠른 응답이 가능하게 된다.
그럼 어떤 페이지가 풀 라우트 캐시에 저장이 되는 페이지가 될 수 있을까?
사실 Next 앱에서 만들어둔 모든 페이지는 자동으로 Static Page 와 Dynamic Page로 나뉘는데, 이 중 Static Page, 정적 페이지에 풀 라우트 캐시가 적용이 된다.
Static Page 와 Dynamic Page를 나누는 기준은 다음과 같다.
먼저 Dynamic Page로 설정되는 기준은
"특정 페이지가 접속 요청을 받을 때마다 매법 변화가 생기거나, 데이터가 달라지는 경우" 이다.
1. 캐시가되지 않는 Data Fetching을 사용할 경우
async function Comp() {
const response = await fetch("...");
return <div>...</div>;
}
컴포넌트가 지금처럼 데이터 페칭을 Fetch 메서드로 수행할 때 이렇게 Cache 옵션이 존재하지 않거나 Cache 옵션이 지금처럼 no-store 로 설정이 되어서 매번 새로운 데이터를 불러오게 될 경우 이 컴포넌트를 사용하는 모든 페이지는 자동으로 다이나믹 페이지가 된다.
2. 동적 함수(쿠키, 헤더, 쿼리스트링)을 사용하는 컴포넌트가 있을 때
import { cookies } from "next/headers";
async function Comp() {
const cookieStore = cookies();
const theme = cookieStore.get("theme");
return <div>...</div>;
}
두번째는 페이지 내부에 있는 어떠한 컴포넌트가 동적 함수라는 것을 사용할 경우에는 해당 페이지는 자동으로 다이나믹 페이지로 설정이 된다.
그 이유는 쿠키, 헤더, 쿼리스트링와 같은 동적인 값을 꺼내오는 동적 함수는 접속 요청에 따라 언제든지 시간에 따라서 자유롭게 변화할 수 있는 값이기 때문이다.
그리고 반대로 Static Page로 설정되는 기준은
Dynamic Page가 아니면 모두 Static Page가 되고, Static Page가 Default 이다.
그리고 이러한 Static Page도 Revalidate가 가능한데,
그림과 같이 3초마다 revalidate가 되어야 하는 데이터 페칭이 존재한다면 이 데이터 캐시에 보관이 되는 데이터 패칭의 결과 뿐만 아니라 이 풀 라우트 캐시에 저장이되는 페이지의 렌더링 결과 또한 이 revalidate 타임으로 설정된 3초 주기로 계속해서 다시 업데이트 된다.
그래서 3초가 지나면 풀 라우트 캐시에 저장된 페이지는 Stale 상태가 되고, 서버 측에서 다시 최신 데이터를 불러와 데이터 캐시를 먼저 갱신 후 페이지까지 다시 렌더링을 해서 풀 라우트 캐시의 값을 업데이트 시켜, 풀 라우트 캐시에 보관된 페이지를 최신화 한다.
1. 풀 라우트 캐시 적용하기
풀 라우트 캐시 확인하기 위해서 빌드를 해보면 처음에 에러가 발생한다, 그 이유는
"use client";
import { useRouter, useSearchParams } from "next/navigation";
export default function Searchbar() {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = useState("");
const q = searchParams.get("q");
(...)
return (
(...)
);
}
서치바 컴포넌트에서 useSearchParams 훅을 사용하고 있는데,
useSearchParams 훅은 브라우저로부터 요청을 받은 쿼리스트링의 값을 꺼내오는 훅이기 때문에 빌드 타임시에는 쿼리스트링 값은 존재하지 않는다.
그래서 빌드 타임 시에는 useSearchParams 같은 빌드 타임에는 절대로 알수 없는 값을 가진 쿼리스트링을 불러오는 이런 훅을 실행하려고 하면 그 값을 현재는 알 수 없어 에러가 발생하게 된다.
이를 해결하기 위해서는 서치바 컴포넌트 사용하고 있는 레이아웃에 가서
import { ReactNode, Suspense } from "react";
import Searchbar from "../../components/searchbar";
export default function Layout({
children,
}: {
children: ReactNode;
}) {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Searchbar />
</Suspense>
{children}
</div>
);
}
리액트의 <Suspense> 라는 내장 컴포넌트로 감싸주고 fallback 로 loading 같은 대체 UI 를 설정하면 된다.
그러면 이 컴포넌트는 사전 렌더링 과정에서는 배제되고 오작 클라이언트 측에서만 렌더링 되는 컴포넌트가 된다.
그리고 다시 빌드하면 정상적으로 이루어진다. 결과는 다음과 같다.
빌드결과를 살펴보면 not-found 페이지를 제외하고는 다 다이나믹 페이지로 되었는데, 그러면 풀 라우트 캐시가 적용되지 않기 때문에 이를 풀 라우트 캐시가 적용되도록 Static Page가 될 수 있게 코드를 수정해보자.
먼저 RootLayout.tsx 파일로 가보면 Footer 컴포넌트가 캐시설정이 되어 있지 않기 때문에 force-cache로 캐시설정을 해준다.
async function Footer() {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`,
{ cache: "force-cache" }
);
if (!response.ok) {
return <footer>제작 : @zzgh06</footer>
}
const books: BookData[] = await response.json();
const bookCount = books.length;
return (
(...)
)
}
그리고 인덱스 페이지로 설정된 페이지로 가면 마찬가지로 캐시가 설정되어 있지않아 캐시를 설정해준다.
async function AllBooks(){
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`,
{ cache: "force-cache" }
);
if(!response.ok) {
return <div>오류가 발생했습니다...</div>;
}
const allBooks : BookData[] = await response.json();
return (
<div>
{allBooks.map((book) =><BookItem key={book.id} {...book} />)}
</div>
)
};
그리고 다시 빌드를 해보면, 인덱스 페이지가 Static Page 로 변경되었다.
2. 풀 라우트 캐시 적용하기(동적 경로)
마지막으로 동적 경로를 갖는 페이지도 풀 라우트 캐시를 적용해보고자 한다.
import { notFound } from "next/navigation";
import style from "./page.module.css";
export default async function Page({
params,
}: {
params: { id: string | string[] };
}) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/${params.id}`
);
if (!response.ok) {
if(response.status === 404){
notFound();
}
return <div>오류가 발생했습니다...</div>
};
const book = await response.json();
const {
id,
title,
subTitle,
description,
author,
publisher,
coverImgUrl,
} = book;
return (
<div className={style.container}>
<div
className={style.cover_img_container}
style={{ backgroundImage: `url('${coverImgUrl}')` }}
>
<img src={coverImgUrl} />
</div>
<div className={style.title}>{title}</div>
<div className={style.subTitle}>{subTitle}</div>
<div className={style.author}>
{author} | {publisher}
</div>
<div className={style.description}>{description}</div>
</div>
);
}
book/[id]/page.tsx 처럼 동적인 경로를 갖는 페이지는 id 라는 url 파라미터를 갖는 여러 개의 동적 경로에 대응하는 페이지이기 때문에 Dynamic Page 로 설정이 된다.
그래서 이러한 동적 경로를 갖는 페이지를 Static Page로써 빌드 타임에 생성되도록 설정하려면
빌드 타임에 next 서버에게 이 북 페이지에 어떠한 경로가 존재할 수 있는지 알려야 한다.
이를 알려주기 위해서는
export function generateStaticParams() {
return [{ id: "1" }, { id: "2" }, { id: "3" }];
}
generateStaticParams 함수를 export로 내보내면 되는데, 이 함수는 정적인 파라미터를 생성하는 함수이다,
그래서 이런식으로 코드를 작성하면 빌드 타임시 book/1 book/2 book/3 페이지를 빌드 타임에 미리 생성하게 된다.
그래서 이 상태로 다시 빌드를 진행하면 그림과 같이 book/1, book/2, book/3 페이지가 생성됐음을 확인할 수 있다.
이제 book/1, book/2, book/3 경로에 해당하는 상세 페이지로 접속하면 빠른 속도로 응답하는 것을 확인할 수 있을 것이다.
그리고 그 외에도 설정하지 않은 경로에 페이지도 일단 렌더링이 되는데 이는 해당 경로로 페이지 접속 요청이 들어오면
실시간으로 다이나믹 페이지로써 생성되어 next 서버에 저장된다.
이런식으로 한번 만들어져서 서버에 저장된 페이지는 풀 라우트 캐시에 보관되어 있기 때문에, 다시 한번 해당 페이지로 접속하게 되면 처음 페이지가 생성되었을 때보다 더 빠르게 응답하게 된다.
그림처럼 book/8 번 상세 페이지의 처음 응답 속도는 110ms 였지만 해당 페이지로 다시 접속했을 때, 67ms 로 단축되어
정상적으로 풀 라우트 캐시가 적용되었음을 확인할 수 있다.
출처 : [한 입 크기로 잘라먹는 Next.js(15+) 강의]
'Next.js' 카테고리의 다른 글
NextJS : Server Actions 에러 (0) | 2025.03.21 |
---|---|
NextJS : 클라이언트 라우터 캐시 (0) | 2024.10.01 |
NextJs : 데이터 캐시(Data Cache)와 Request Memoization (0) | 2024.09.26 |
NextJs : 앱 라우터의 데이터 페칭 (0) | 2024.09.26 |
NextJs : App Router 네비게이팅 (0) | 2024.09.25 |