[Next.js] 마크다운 블로그 만들기 (feat. getStaticProps, getStaticPaths)
개요
이번 달 초, Next.js 원티드 프리온보딩에 참여하며 진행했던 과제를 정리해보고자 한다. 우선 next.js 12기준으로 pages
라우팅으로 마크다운 블로그를 만들어보고, 이를 13버전의 app
라우팅으로 마이그레이션하는 과정을 정리한 글이 될 것이다.
1. 프로젝트 생성
간단하게 CNA를 사용했다.
npx create-next-app --typescript
2. styled-components 설정
Next.js에서 styled-components
을 사용하려면, 추가 설정이 필요하다. CSS-in-JS
라이브러리는 자바스크립트 런타임 시 적용이 되기 때문에, SSR에서 스타일 적용이 즉시 되지 않기 때문이다. 자바스크립트가 적용되기 전의 HTML 파일이 먼저 렌더링 되면서 스타일이 먹히지 않는 것처럼 보이는 것이다.
설치
yarn add styled-components@latest
npm i -D @types/styled-components
pages/_document.tsx
- 정상적인 동작을 위해
_document.tsx
를 생성하고 아래 코드를 넣어야 한다. - 이는 HTML head에 style태그를 주입하는 것으로 볼 수 있다. (참고)
import React from 'react';
import Document, { DocumentContext } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles} {sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}
export default MyDocument;
3. SSG 구조 잡기
이제 블로그의 포스트 리스트를 보여주는 홈 페이지를 만들 것이다. 일단 포스트는 정적으로 가지고 있다고 가정하고, 모든 포스트를 SSG로 가져와 사용자가 접근했을 때 데이터가 바로 보여지도록 할 것이다.
pages/index.tsx
- 홈 페이지에서 블로그의 포스트 리스트를 보여줄 것이다.
getStaticProps
에서 API로 posts를 가져와 반환한다.- 그렇게 반환된 posts는 페이지 컴포넌트의 props로 넘어온다.
export const getStaticProps = () => {
const posts = getAllPosts(); //API 가정
return { props: { posts } };
};
const Home: NextPage<{ posts: Post[] }> = ({ posts }) => {
return (
<PostBoxes>
{posts.map((post, index) => (
<PostBox key={`${post.title}-${index}`} post={post} />
))}
</PostBoxes>
);
};
export default Home;
4. 정적 마크다운 포스트 가져오기
위에서 있다치고 넘어간 부분인 정적 포스트를 만들고, 이를 가져오는 API를 만들어보자.
정적 마크다운 포스트 생성
- 우선 이 정적 리스트를 담아두기 위한
src/_posts
폴더를 생성한다. - 포스트의 형식은 Front Matter를 참고하면 된다.
예시) src/_posts/test.md
- 상단에
---
로 구분되어 있는 부분은 현재 포스트에 접근할 때 사용될 변수들이라고 생각하면 된다. (메타 데이터 느낌) - 이후 내용은 마크다운으로 작성하면 된다.
---
title: 제목
author: zeo
preview: Lorem Ipsum
date: '2023-07-09'
---
## What is Lorem Ipsum?
Lorem Ipsum is dummy text.
API 만들기
마크다운 파일 가져오기위한 코드를 찾아보면 다양한듯 비슷한 코드를 많이 발견할 수 있다. 그리고 대표적으로 Next에서 제공하는 blog-starter에서 아래 코드를 확인할 수 있다. 해당 코드를 기준으로 조금씩 변형해서 사용하면 된다. (나는 그냥 가져다 씀 😀)
src/api/index.ts
import fs from 'fs';
import { join } from 'path';
import matter from 'gray-matter';
const postsDirectory = join(process.cwd(), 'src/_posts'); //루트를 기준으로 _posts폴더 위치 알리기
export function getPostSlugs() {
return fs.readdirSync(postsDirectory);
}
export function getPostBySlug(slug: string, fields: string[] = []) {
const realSlug = slug.replace(/\.md$/, ''); //파일명
const fullPath = join(postsDirectory, `${realSlug}.md`); //해당 파일의 위치 찾기
const fileContents = fs.readFileSync(fullPath, 'utf8'); //파일 가져오기
const { data, content } = matter(fileContents); //마크다운 to 자바스크립트
type Items = {
[key: string]: string;
};
const items: Items = {}; //파일 데이터
//---에서 설정한 변수 중 인자로 들어온 값만 데이터에 추가
fields.forEach((field) => {
if (field === 'slug') {
items[field] = realSlug;
}
if (field === 'content') {
items[field] = content;
}
if (typeof data[field] !== 'undefined') {
items[field] = data[field];
}
});
return items;
}
export function getAllPosts(fields: string[] = []) {
const slugs = getPostSlugs();
const posts = slugs.map((slug) => getPostBySlug(slug, fields));
return posts;
}
src/utils/index.ts
import { remark } from 'remark';
import html from 'remark-html';
//이름에서 알 수 있듯, 마크다운을 HTML로 변환해주는 함수
export default async function markdownToHtml(markdown: string) {
const result = await remark()
.use(html as any)
.process(markdown);
return result.toString();
}
홈페이지에 API 제대로 연결
getAllPosts
부분만 수정하면 된다.- 필요한 정보(필드)만 인자로 넘겼다.
export const getStaticProps = () => {
const posts = getAllPosts(['slug', 'title', 'author', 'preview', 'date']);
return { props: { posts } };
};
cf) 레이아웃 및 스타일 참고
5. 정적 경로 설정하기
홈 페이지에서 포스트 하나를 클릭했을 때, /[slug]
경로를 가지는 상세 페이지를 만들자.
pages/[slug].tsx
- 정적 포스트 파일을 이용해 미리 경로를 생성할 수 있다.
getStaticPaths
를 활용해 각 포스트에 대응하는 경로를 만든다. getStaticPaths
에서 반환한 값은getStaticProps
로 넘어간다.getStaticProps
에서는 현재 경로(slug)를 받아, 해당 포스트를 가져온다.- 마크다운인 content를 HTML에 삽입할 때는
dangerouslySetInnerHTML
을 사용한다.
import type { NextPage } from 'next';
import { getAllPosts, getPostBySlug } from '../api';
import markdownToHtml from '../utils';
export const getStaticPaths = async () => {
const posts = getAllPosts(['slug', 'title']);
return {
paths: posts.map((post) => ({ params: { slug: post.slug } })),
fallback: false,
};
};
export const getStaticProps = async ({ params: { slug } }: Parmas) => {
const post = getPostBySlug(slug, ['slug', 'title', 'content']);
const content = await markdownToHtml(post.content);
return { props: { post: { ...post, content } } };
};
const Post: NextPage<{ post: Post }> = ({ post }) => {
return (
<>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</>
);
};
export default Post;
결과 확인
test.md
파일이/test
경로로 보여지는 것을 확인할 수 있다.- 마크다운도 잘 보인다!
Reference
- https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props
- https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-paths
- https://jekyllrb.com/docs/front-matter/
- https://github.com/vercel/next.js/tree/canary/examples/blog-starter/lib
- https://nextjs.org/docs/pages/building-your-application/routing/custom-document#customizing-renderpage
'Frontend > Next' 카테고리의 다른 글
[Next.js] URL에 상태 저장하기 (useSearchParams, URLSearchParams) (0) | 2024.05.09 |
---|---|
[Next.js] pages에서 app 라우터으로 마이그레이션 (feat. fetch, generateStaticParams) (0) | 2023.07.31 |
[NextJS] NextJS 시작하기 - 8. URL에 데이터 전달하고 가져오기(Catch All URL) (0) | 2022.01.16 |
[NextJS] NextJS 시작하기 - 7. 다이나믹 라우팅 & 페이지 이동 (0) | 2022.01.14 |
[NextJS] NextJS 시작하기 - 6. Server-Side Rendering 설정하기 (getServerSideProps) (0) | 2022.01.13 |