본 글은 Next.js 시리즈의 세번째 포스팅과 이어집니다.
네번째 글은 지난 포스팅에서 말했던 SSG로 Dynamic content를 만드는 법에 대해 좀 더 심도있게 다뤄봅니다.
빌드타임의 정적 페이지는 사용자에게 SSR보다 빨리 페이지를 그려보여줄 수 있다는 장점이 있습니다.
블로그 페이지를 제공하는 웹 사이트에서 블로그에 대한 글이 모여 있는 라우트가 /blog
,
각기 개별적인 블로그 글들은 /blog/:slug
로 접근할 수 있게 해야 한다면 어떻게 각각의 slug에 매핑되는 글들은 정적 페이지로 보여질 수 있는 것일까요?
반복해서 말하지만 정적 페이지는 빌드 타임에 페이지가 렌더링되기 때문에 사용자가 블로그 글 중 어떤 페이지를 접근할지는 미리 알 수 없습니다. 빌드 타임에 미리 여러 페이지를 렌더링 시켜놓고 캐싱해서 사용합니다.
getStaticPaths와 getStaticProps를 이용해 Dynamic route
에 따른 Dynamic content
를 제공할 수 있습니다.
getStaticPaths를 통해 어떤 slug들에 대한 페이지들을 렌더링 할 것인지 빌드타임에 정할 수 있습니다. 그리고
이때 리턴한 객체의 paths
속성에 담긴 객체 배열의 params
를 참조하여 어떤 api를 호출해 해당 페이지를 렌더링하는 재료로 사용할 것인지 getStaticProps
를 이용해 정합니다.
// pages/blog/[slug].tsx
...
export async function getStaticPaths() {
const postsPath = path.join(process.cwd(), "posts");
const filenames = fs.readdirSync(postsPath);
const slugs = filenames.map((name) => {
const fullPath = path.join(postsPath, name);
const file = fs.readFileSync(fullPath, "utf-8");
const { data } = matter(file);
return data;
})
return {
paths: slugs.map(s => ({ params: { slug: s.slug } })),
fallback: true,
}
}
getStaticPaths
에서 반환하는 객체의 paths 중 유저가 요청한 slug가 담겨있지 않는다면,
404 페이지를 보여줄 것인지, getStaticProps
를 다시 실행시켜 해당 페이지를 렌더링 할 것인지는 fallback
속성 값을 변경함으로 설정할 수 있습니다.
fallback
이 false라면 이미 사용자 요청 url이 렌더링 페이지의 범위 안에 들어가지 않을 시, 404 페이지를 보여줍니다.
fallback
이 true라면, getStaticProps
를 통해 보여질 페이지에 대한 처리를 할 수 있습니다.
사용자가 어떤 slug를 요청했는지는 getStaticProps
의 인자로 접근할 수 있는 params 속성을 통해 알 수 있으며, 만약 해당 slug에 대한 자료가 데이터베이스에 있지 않더라도, 이에 대해 어떻게 에러처리를 할지 설정할 수 있습니다.
다음의 getStaticProps는 위의 코드와 자연스럽게 이어지는 코드입니다.
서버 내부 파일에서 읽어오는 블로그 파일들 중 찾고자 하는 자료가 없더라도, 다른 곳에서 데이터를 불러와 사용자에게 보여줍니다.
export async function getStaticProps({ params }) {
let post
try {
const postsPath = path.join(process.cwd(), "posts", params.slug, ".mdx");
post = fs.readFileSync(postsPath, "utf-8");
}
catch (e) {
console.log({ slug: params.slug })
const cmsPosts = posts.published.map((p) => {
return matter(p)
});
const match = cmsPosts.find(p => p.data.slug === params.slug)
post = match.content;
}
const { data } = matter(post);
const mdxSource = await renderToString(post, { scope: data })
return {
props: {
source: mdxSource,
frontMatter: data,
}
}
}
사용자가 빌드된 페이지를 요청하지 않을 때, 새로운 페이지를 빌드할 동안 보여질 ui를 적용시키고 싶을때 사용합니다.
useRouter 리턴 값인 객체의 isFallback 값을 통해서 폴백 UI를 설정할 수 있습니다.
const Blog: FC<Post> = ({ source, frontMatter }) => {
const router = useRouter()
if (router.isFallback) {
return (
<Pane width="100%" height="100%">
<Spinner size={48} />
</Pane>
)
}
이 블로그 글과 마찬가지로, 작가들은 글을 공개하기전 프로덕션 환경에서 실제로 어떻게 내 글이 보여지는지 먼저 테스트를 진행하고 최종 출간합니다.
SSG는 빌드타임 시 미리 렌더링한 페이지를 캐싱해서 사용하기 때문에 이렇게 실시간으로 업데이트된 글을 테스트 환경에서 불러오기가 불편할 수 있습니다. 변경사항을 확인할 때마다 빌드타임을 거쳐야 하기 때문입니다.
이러한 불편함을 해소하기 위해 빌드타임이 아닌 리퀘스트 타임에 페이지를 렌더링 하도록 넥스트에서 지원해주는 기술이 preview mode
입니다.
앞선 넥스트 시리즈의 creating api routes에서 풀스택 프레임워크로 넥스트를 사용하는 방법을 알려드렸는데요, preview mode에서는 이 api routes를 사용합니다.
프로젝트의 pages/api
경로 아래에 preview.ts
파일을 다음과 같이 만들었습니다.
api/preview
경로로 요청이 들어오면 res 객체에서 setPreviewData
를 호출합니다.
이 함수 호출은 넥스트에서 알아차릴 수 있는 특별한 쿠키를 심어주어 평소와 다르게 정적페이지가 생성되게 합니다.
import { NextApiResponse } from "next"
export default (req, res: NextApiResponse) => {
res.setPreviewData({})
res.redirect(`/${req.query.route}`);
}
개발자 모드의 쿠키를 살펴보면
__prerender_bypass
, __next_preview_data
쿠키가 생성되었음을 알 수 있습니다.
예제 코드에서는 해당 쿠키를 설정해주는 함수를 호출 한 후 쿼리스트링으로 들어온 경로로 리다이렉션을 시켜주었습니다.
리다이렉션 한 페이지가 getStaticProps를 호출한다면, 빌드타임에 호출되던 해당 함수는 리퀘스트 타임에 호출이 됩니다.
인자인 context의 preview 속성을 통해, preview mode가 동작 중인지 확인할 수 있습니다.
export async function getStaticProps(context) {
// If you request this page with the preview mode cookies set:
//
// - context.preview will be true
const res = await fetch(`https://.../${context.preview ? 'preview' : ''}`)
// - context.previewData will be the same as
// the argument used for `setPreviewData`.
}
CMS는 21년도 매우 인기있는 정적사이트 생성 솔루션으로, 여러 산업에서 사용되고 있습니다. 넥스트의 SSG 기술이 CMS와 연관된 기술이라고 할 수 있습니다.
넥스트가 제공하는 getStaticPaths와 getStaticProps, preview mode를 통해 개인 블로그를 작성해보는 것은 좋은 연습이 될 것입니다.