Next.js는 데이터 캐싱을 지원한다. Next.js의 'fetch'는 기존 Web 'fetch' API를 확장하여 구현한 것으로 기본적으로 응답 데이터를 캐시한다. 이때 캐시된 데이터는 Data Cache에 저장된다.
fetch 함수의 두 번째 인자로 'cache' 또는 'revalidate' 속성을 추가하면 데이터 캐시 옵션을 바꿀 수 있다.
또는 route segement config를 설정하여 특정 route segment에 존재하는 모든 'fetch'에 대한 캐시 옵션을 설정할 수 있다.
Next.js는 fetch 데이터뿐만 아니라 라우트도 캐싱한다. Full Route Cache(서버)와 Router Cache(클라이언트)에서 캐싱된다. Full Route Cache는 정적 렌더링 된 라우트만 캐싱이 적용되지만, Router Cache는 클라이언트 측에서 메모리에 캐싱하기 때문에 정적 및 동적 렌더링 라우트 모두 캐싱이 적용된다.
위와 같은 Next.js의 여러 캐싱 메커니즘은 데이터를 가져오고 렌더링 하는 데 서로 밀접하게 연관되어 있다.
Data Cache를 선택 해제 또는 재검증하는 것은 Full Route Cache를 무효화시킨다. 렌더링 결과는 데이터에 의존하기 때문이다. 그러나 Full Route Cache를 무효화 또는 선택 해제 하는 것은 Data Cache에 영향을 미치지 않는다.
캐싱 메커니즘에 대한 자세한 내용은 공식 문서에 상세하게 나와있다.
Next.js에 여러 캐싱 메커니즘이 있었고, 시간을 들여 머리로는 이해했지만 실제 눈으로 차이를 확인해 보는 것이 가장 잘 이해하는 거라 생각해 직접 실험을 해보았다.
리스트를 출력하는 홈 페이지와 글을 작성할 수 있는 form 페이지가 있다.
form을 제출하면 서버 POST 요청을 통해 DB에 저장한다.
1. 기본 동작
- fetch의 캐시 옵션을 설정하지 않음 (기본: 응답 데이터가 캐시됨. 캐시 선택 해제 또는 재검증을 따로 수행하지 않으면 계속 유지된다.)
- route segement 설정을 하지 않음 (기본: 정적 렌더링)
app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import Link from "next/link";
export const metadata: Metadata = {
title: "Test Data Caching",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<nav className="nav">
<Link href="/">Home</Link>
<Link href="/form">form</Link>
</nav>
{children}
</body>
</html>
);
}
app/page.tsx
import styles from "./page.module.css"
interface Post {
_id: string;
text: string;
}
export default async function Home() {
const res = await fetch("http://localhost:5500/posts")
const posts: Post[] = await res.json()
console.log(new Date())
console.log(posts)
return (
<main className={styles.main}>
<div>
<ul>
{posts.map((v) => (
<li key={v._id}>{v.text}</li>
))}
</ul>
</div>
</main>
);
}
app/form/page.tsx
"use client"
import { useState } from "react"
export interface Form {
text: string
}
export default function FormPage() {
const [form, setForm] = useState<Form>({ text: "" })
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target
setForm((prev) => ({ ...prev, text: value }))
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const res = await fetch("http://localhost:5500/post/new", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(form),
})
}
return (
<div style={{ textAlign: "center", marginTop: "60px"}}>
<form onSubmit={onSubmit}>
<input
id="text"
value={form.text}
placeholder="내용"
onChange={onChange}
></input>
<button type="submit">제출</button>
</form>
</div>
);
}
결과
form으로 새로운 글을 작성한 후 홈 페이지로 돌아와도 추가된 글이 목록에 보이지 않는다. 이는 새로고침을 해도 마찬가지다.
2. fetch 캐시 옵션 선택 해제 (cache: 'no-store' )
cache: 'no-store' 옵션은 next: { revalidate: 0 }과 동일하다.
-> fetch의 캐시 옵션 추가로 인해 route segement 설정을 하지 않아도 동적 렌더링으로 변경된다.
export default async function Home() {
const res = await fetch("http://localhost:5500/posts", { cache: "no-store" });
const posts: Post[] = await res.json();
console.log(new Date());
console.log(posts);
(...)
}
결과
form으로 새로운 글을 작성한 후 navbar를 통해 홈 페이지로 돌아와도 추가된 글이 목록에 '바로' 보이지 않는다. 당연히 페이지가 동적 렌더링 되었고 데이터 캐시도 선택 해제 되었으니 실시간으로 데이터가 변경될 줄 알았는데... 아주 짧은 간격으로 여러 번 페이지를 왔다 갔다 하며 timestamp를 찍어본 결과 약 30초마다 서버 컴포넌트 함수가 실행되며 목록이 업데이트되는 것을 확인할 수 있었다. 또한 새로고침을 수행하면 목록이 업데이트 됐다. 위에서 얘기한 Router Cache가 떠올랐다...
Router Cache는 클라이언트 측에서 메모리에 RSC Payload를 캐싱하는 것으로 선택 해제가 불가능하다. Router Cache는 페이지 새로 고침 시 또는 자동 무효화 주기(Automatic Invalidation Period)마다 무효화된다. Router Cache의 자동 무효화 주기는 Prefetch 옵션 값에 따라 다르다. 기본 설정의 경우 30초, Full Prefeching의 경우 5분 동안 지속된다. 바로 데이터를 업데이트하고 싶은 경우에는 클라이언트 컴포넌트에서 router.refresh를 호출하여 페이지를 새로고침 하거나 Server Action에서 revalidatePath 또는 revalidateTag를 사용하여 Router Cache를 무효화시킬 수 있다.
3. route segement config 설정 ( dynamic = 'force-dynamic' 또는 revalidate = 0 )
- fetch의 캐시 옵션을 설정하지 않음
- 동적 렌더링이 적용됨
export const dynamic = "force-dynamic";
// export const revalidate = 0;
export default async function Home() {
const res = await fetch("http://localhost:5500/posts");
const posts: Post[] = await res.json();
(...)
}
결과
- revalidate: 0의 경우
2번과 동일한 결과이다. route segement config를 설정하면 segment 내에 있는 모든 fetch에 cache: 'no-store'를 한 것과 동일하기 때문이다.
- dynamic = 'force-dynamic'의 경우
공식 문서에는 이걸 수행하면 Data Cache, Full Route Cache가 무효화된다고 나와있었다. 그런데 Full Route Cache는 무효화되었지만 Data Cache는 무효화되지 않았는지 이전 데이터를 계속 가져왔다. 원인을 열심히 찾아보다가 한 Github Issue를 발견했다. Next.js 14.2.0 ~ 14.2.3 (최신 버전)에서는 force-dynamic이 Data Cache를 무효화하지 않는다는 것이다. 나도 이슈 글을 보고 Next.js 버전을 14.1로 낮춰서 다시 실험했더니 캐시가 정상적으로 무효화되었다! 이슈 코멘트를 보니 Canary 버전에서 해결이 된 듯하고 아직 stable에는 반영되지 않았다고 한다.
4. fetch에 캐시 재검증 적용 ( next: { revalidate: 30} )
- 정적 렌더링
revalidate의 값은 초 단위이다. 30초에 한 번 데이터를 재검증하도록 한다. 이는 30초 후에 알아서 재검증이 되는 것이 아니라 30초가 지난 뒤 사용자가 다시 데이터를 요청할 때 재검증이 이루어진다. 재검증은 백그라운드에서 일어나고, 데이터의 갱신 전까지는 stale한 데이터를 계속 사용자에게 보여준다. 결론은 재검증 시간이 지난 후 접속 -> 사용자는 이전 데이터를 받고, 백그라운드에서 데이터 재검증이 이루어짐 -> 캐시 업데이트 후 다시 접속 -> 최신 데이터 받음! 인 것이다.
export default async function Home() {
const res = await fetch("http://localhost:5500/posts", { next: { revalidate: 30 } });
const posts: Post[] = await res.json();
(...)
}
결과
시간이 지난 후 목록이 갱신되었다. 여기에도 Router Cache가 적용되다 보니 어느 정도 시간이 지난 후에 확인할 수 있었다. 재검증이 일어난 뒤, Router Cache가 무효화되어 페이지가 갱신될 때 확인이 된다.
5. route segement config 설정 ( revalidate = 30 )
- fetch 옵션을 설정하지 않음
- 정적 렌더링
export const revalidate = 30;
export default async function Home() {
const res = await fetch("http://localhost:5500/posts");
const posts: Post[] = await res.json();
(...)
}
결과
4번과 동일하다.
6. Server Actions에서 revalidatePath 호출
- form submit 함수를 Server Actions에서 수행하고 API 요청 성공 시 revalidatePath 호출
export default async function Home() {
const res = await fetch("http://localhost:5500/posts");
const posts: Post[] = await res.json();
(...)
);
}
app/actions.ts
"use server"
import { revalidatePath } from "next/cache"
import { Form } from "./form/page"
export default async function submit(form: Form) {
const res = await fetch("http://localhost:5500/post/new", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(form),
})
if (res.status === 200) revalidatePath("/") // 캐시 무효화
}
app/form/page.tsx
"use client";
import { useState } from "react";
import submit from "@/app/actions";
export interface Form {
text: string;
}
export default function FormPage() {
const [form, setForm] = useState<Form>({ text: "" });
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setForm((prev) => ({ ...prev, text: value }));
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await submit(form);
};
(HTML 기존과 동일)
}
결과
바로 목록이 갱신된다. revalidatePath / revalidateTag를 사용하면 Data Cache, Full Route Cache, Router Cache가 모두 무효화되어 갱신된 데이터를 바로 받아볼 수 있다.
요약
- fetch 함수의 캐시 옵션을 설정하거나 route segment config 설정을 통해 Data Cache를 무효화 또는 선택 해제할 수 있다.
- fetch 함수에 캐시 옵션을 따로 설정하지 않으면 기본 동작으로 한 번의 요청 이후로는 쭉 Data Cache에 저장된 캐시 데이터를 받아온다.
- 캐시 옵션을 해제하고 동적 렌더링을 적용하여도 클라이언트에 Router Cache가 존재하기 때문에 새로고침 없이는 바로 변경된 데이터를 받아볼 수 없다. 새로고침 없이는 Router Cache의 자동 무효화 주기 이후에 새로 접속 시 변경된 데이터를 받아볼 수 있다.
변경된 데이터를 바로 받아보려면
- 캐시 옵션 해제 + router.refresh
- revalidatePath 또는 revalidateTag 사용
둘 중 한 방법을 사용하면 된다!
잘못된 부분이 있다면 코멘트 남겨주세요!
'Develop > NextJS' 카테고리의 다른 글
[Next.js] 스크롤 최상단 이동 버튼 구현하기 (0) | 2024.08.05 |
---|---|
[Next.js] 서버 & 클라이언트 컴포넌트에서 토큰 관리하기 (0) | 2024.07.24 |
[Next.js] Next.js 작업물 Vercel로 배포하기 (0) | 2023.07.21 |
[NextAuth] 커스텀 로그인 구현과 회원 정보 수정 (+ session update) (0) | 2023.07.14 |
[Next.js] Next.js에 MongoDB Atlas 연결하기 (+TypeScript) (0) | 2023.07.07 |