Next.js의 On-Demand ISR을 구현하는 중에 revalidate 후 해당 page에 새 data가 반영되지 않는 문제를 겪었다.
아래와 같이 특정 path의 page를 revalidate 하기 위한 API route를 구현하고,
// 📂 pages/api/revalidate.ts
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
try {
const { paths } = req.body;
if (!paths || !Array.isArray(paths) || paths.length === 0) {
return res.status(400).json({ message: "Paths are required" });
}
await Promise.all(paths.map((path) => res.revalidate(path)));
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).json({ message: "Revalidation error" });
}
}
이 API route로 요청을 보내는 함수를 구현한 다음,
// 📂 apis/revalidate.ts
export async function revalidate(paths: string[]) {
await fetch("/api/revalidate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ paths }),
});
}
Mouse click event가 발생하면 data를 변경한 뒤 revalidate를 호출해서 page를 재생성한다.
// 📂 pages/items/[id].tsx
// '수정하기' 버튼을 클릭하면 할 일 data를 변경한 뒤 할 일 목록 페이지(`/`)로 이동
const handleEditClick = async () => {
await executeAsync(async () => {
// ...
const result = await editTodo(todo.id, updateValues);
if (result) {
// 할 일 목록 페이지와 상세 페이지 모두 SSG 방식으로 rendering 하므로,
// 변경된 data로 page를 재생성하기 위해 두 page 모두 revalidate
await revalidate(["/", `/items/${todo.id}`]);
router.push("/");
}
});
};
이 때, 할 일 목록 페이지(/)로 이동하면 기대와 달리 변경된 data가 반영되지 않는다.
npm run start로 실행했을 때만 문제가 발생한다.문제의 원인을 파악하기 위해 몇 가지 동작을 검증해 보았다.
가장 먼저 의심해 볼 부분은 revalidate한 page가 실제로 재생성 되었는지 여부이다.
Revalidate 후 .next/server/pages에 생성된 HTML 및 JSON 파일을 확인해 보면 변경된 data가 반영되어 있는 것을 쉽게 확인할 수 있다.
// `api/revalidate` 요청 전
{
"pageProps": {
"todo": {
"id": 8205,
"name": "은행 다녀오기 123",
// ...
},
"__N_SSG": true
}
// `api/revalidate` 요청 후
{
"pageProps": {
"todo": {
"id": 8205,
"name": "은행 다녀오기",
// ...
},
"__N_SSG": true
}
Page가 정상적으로 재생성 되었으므로 문제가 발생한 후 새로고침하면 변경된 data가 정상적으로 반영된다.
Page는 정상적으로 재생성 되고 있으므로, 다음으로 page 이동 전에 재생성이 완료되는지 여부를 의심해 볼 수 있다. Page를 이동할 때 재생성 이전 HTML을 먼저 렌더링한다면 이와 같은 문제가 발생할 수 있다.
Revalidate API route에서 res.revalidate(path) 호출 전후로 .next/server/pages에서 생성되는 page의 파일 내용을 출력하여 revalidate 직후 정상적으로 재생성 되는 것을 확인한다.
아래와 같이 파일 내용을 출력하는 함수를 만들고,
import { exec } from "child_process";
import { promisify } from "util";
async function printJson(path: string) {
try {
const { stdout } = await execAsync(`cat .next/server/pages${path}.json`);
console.log(`${path}.json 파일 내용:`, stdout);
} catch (error) {
console.log("파일 읽기 오류:", error);
}
try {
const { stdout } = await execAsync(`cat .next/server/pages${path}.html`);
console.log(`${path}.html 파일 내용:`, stdout);
} catch (error) {
console.log("파일 읽기 오류:", error);
}
}
res.revalidate 호출 전후로 파일을 출력한다.
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// ...
printJson(paths[1]);
await Promise.all(paths.map((path) => res.revalidate(path)));
printJson(paths[1]);
// ...
}
출력 결과 revalidate 직후 HTML 및 JSON 파일이 변경된 data를 사용해서 재생성 된 것을 확인할 수 있다.
// `res.revalidate` 호출 전
{
"pageProps": {
"todo": {
"id": 8205,
"name": "은행 다녀오기1",
// ...
},
"__N_SSG": true
}
// `res.revalidate` 호출 후
{
"pageProps": {
"todo": {
"id": 8205,
"name": "은행 다녀오기",
// ...
},
"__N_SSG": true
}
unstable_skipClientCache 옵션의 존재를 알게 되었다.next/router를 사용할 때 client cache를 통해 이동하려는 page의 data를 저장해 둔다.