개발 환경을 배포 환경으로 변경하기 위해 개발 서버를 종료하고, 다른 터미널에 npm run build
명령어를 입력하여 NextJS 어플리케이션이 배포 환경에서 동작할 수 있도록 준비한다.
npm start
명령어로 최적화된 코드의 배포 서버를 실행할 수 있는데, 같은 주소로 서버가 열리지만 이는 개발 환경 서버가 아닌 배포 서버로 최적화된 코드로 서버가 열린다.
해당 서버에서 새로운 게시글을 업로드하면 새로 업로드한 게시글은 확인할 수 없다.
// lib> meals.js
export async function getMeals() {
await new Promise((resolve) => setTimeout(resolve, 2000));
// throw new Error("Loading meals failed");
return db.prepare("SELECT * FROM meals").all();
}
반면, 이처럼 게시글을 가져오는데 딜레이가 걸리도록 설정해뒀음에도, 전체 게시글을 가져오는데는 딜레이가 걸리지 않는다. 해당 페이지를 새로고침해도 거의 바로 새로고침된다.
이처럼 배포 환경에서 새로 등록한 게시글이 보이지 않고, 딜레이가 발생하지 않는 것은 NextJS가 꽤 공격적인 캐싱을 하기 때문이다.
배포 환경 에서의 앱을 준비하기 위해 npm run build
명령어를 실행하게 되면 NextJS는 실제로 앱에서 사전 생성될 수 있는 모든 페이지를 모두 사전 렌더링하고 생성하여 기본적으로는 동적 웹페이지가 아니게 만든다.
즉, 빌드 프로세스에서 모든 데이터를 불러오고 렌더링 한다. NextJS는 모든 페이지를 사전 렌더링 함으로써 배포된 직후부터 모든 페이지가 동작할 수 있게 한다.
그래서 웹 사이트의 가장 첫 방문자도 렌더링을 기다릴 필요 없이 즉시 완성된 페이지를 볼 수 있게 되는 것이다.
이처럼 NextJS의 공격적인 캐싱은 사용자에게 더 나은 경험을 줄 수도 있지만, 사전 랜더링된 페이지와 달라진 화면을 제공해야 하는 경우, 새로운 게시글을 업로드한 경우에는 적합하지 않다.
이러한 점을 고치지 위해 새로운 게시글을 등록할 때마다 NextJS에게 캐시의 전체나 일부를 비우라고 해야 한다.
// lib> action.js
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { saveMeal } from "./meals";
// 유효성 검사 (빈 문자열)
function isInvalidText(text) {
return !text || text.trim() === "";
}
export async function shareMeal(prevState, formData) {
const meal = {
title: formData.get("title"),
summary: formData.get("summary"),
instructions: formData.get("instructions"),
image: formData.get("image"),
creator: formData.get("name"),
creator_email: formData.get("email"),
};
// 입력값에 대한 유효성 검사
if (
isInvalidText(meal.title) ||
isInvalidText(meal.summary) ||
isInvalidText(meal.instructions) ||
isInvalidText(meal.creator) ||
isInvalidText(meal.creator_email) ||
!meal.creator_email.includes("@") ||
!meal.image ||
meal.image.size === 0
) {
return {
message: "Invalid input",
};
}
await saveMeal(meal);
revalidatePath("/meals"); // 캐시 유효성 재확인 트리거
redirect("/meals");
}
NextJS가 제공하는 내장함수인 revalidatePath
를 활용할 수 있다. revalidatePath
는 특정 경로에 속하는 캐시의 유효성 재검사를 하게 한다. 유효성 재검사를 한다는 것은 NextJs가 캐싱된 페이지 같은, 해당 페이지에 연관된 캐시를 비우는 것을 의미한다.
즉, 해당 경로에 변화하는 데이터가 포함된 경우 해당 경로에 대하여 유효성 재검사를 한다. 이때 중요한 것은 중첩 경로는 영향을 받지 않는다는 것이다.
import { revalidatePath } from "next/cache";
revalidatePath("/meals", 'page'); // 기본값
revalidatePath("/meals", 'layout');
// 웹사이트의 모든 페이지를 재검사 하고 싶다면
revalidatePath("/", 'layout');
한편, revalidatePath
의 두 번째 인수의 기본값은 page
로 이 경로의 이 페이지만 재검사하겠다는 뜻이다. 이를 layout
으로 설정하게 되면 재검사되는 것은 layout
으로, 중첩된 모든 페이지를 재검사하게 된다.
이제 다시 모든 페이지를 빌드 및 실행하면, 캐시가 재검사되고 일부가 비워질 것이며 새로 등록한 게시물을 확인할 수 있다.
하지만 여전히 이미지는 확인할 수 없다. 이는 현재 어플리케이션에서 이미지를 public 폴더에 저장하기 때문이다. NextJS 배포 환경에서 public
폴더에 관여하지 않는다. 따라서 배포 시 public
폴더는 배포하지 않아도 되며, 파일 저장은 AWS S3과 같은 파일 저장 서비스를 이용해야 한다.
루트의 layout.js
파일을 보시면 metadata
상수가 export 되고 있다. NextJS는 metadata 라는 이름의 export 되고 있는 변수 또는 상수를 모든 page 및 layout 파일에서 찾아낸다. 이 metadata
객체에서 메타데이터 필드를 지정할 수 있다.
메타데이터에 대한 NextJS 공식 문서
공식 문서를 살펴보면 metadata 객체에서 지정할 수 있는 필드에 대해 확인할 수 있다.
만약 이 metadata
를 layout에 추가한다면, 그 layout이 감싸고 있는 모든 페이지에 자동으로 적용되며, 페이지에 metadata
가 존재한다면 페이지 메타데이터가 우선 적용된다.
// app> layout.js
import MainHeader from "@/components/main-header/main-header";
import "./globals.css";
// 메타데이터 설정
export const metadata = {
title: "NextLevel Food",
description: "Delicious meals, shared by a food-loving community.",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<MainHeader />
{children}
</body>
</html>
);
}
이처럼 layout에 메타데이터를 설정하면 모든 페이지는 적어도 이 기본 metadata는 가지게 된다.
// app> meals> page.js
export const metadata = {
title: "All Meals",
description: "Browse the delicious meals shared by our vibrant community.",
};
또한, 이처럼 특정 페이지에 메타 데이터를 추가하면, 해당 페이지에서는 설정한 메타데이터를 확인할 수 있다.
한편, 동적 페이지에서는 generateMetadata
라는 async 함수를 export하여 메타데이터를 설정할 수 있다.
NextJS는 만약 아무런 메타데이터를 찾지 못한다면generateMetadata
함수가 있는지를 확인하고, 만약 함수가 존재한다면 NextJS가 대신 실행시켜 주며 generateMetadata
함수는 metadata 객체를 반환해야 한다.
// app> meals> [mealSlug]> page.js
export async function generateMetadata({ params }) {
const meal = getMeal(params.mealSlug);
// 유효하지 않은 경우
if (!meal) {
notFound();
}
// metadata 객체 반환
return {
title: meal.title,
description: meal.summary,
};
}
generateMetadata
함수는 페이지 컴포넌트가 속성으로 받는 것과 동일한 데이터를 받는다. 따라서 동일하게 params 라는 이름으로 객체를 받으면 metadata를 만들 음식 데이터를 가져올 수 있다. 이제 meal detail 페이지를 방문해보면 title이 탭에 보여지게 된다.
한편, 유효하지 않은 동적 페이지를 방문하면 not-found 페이지가 아닌 에러 페이지가 나타나는데, 이는 메타데이터는 처음에 만들어지기 때문이다. meal 아래의 title에 접근하면 meal이 undefined 상태이므로 실패하는 것이다.
따라서 여기에 if 조건문으로 검사를 해주어야 한다. meal에 값이 할당되었는지를 검사하고, 값이 없다면 generateMetadata
함수 안에서 notFound 함수를 호출하여 해당 meal을 찾지 못해서 메타데이터 생성에 실패할 경우 not found 페이지가 보여지도록 한다.
우앙... 꾸준한 윤빈나...