Next.js + Github Pages 간단 블로그 만들기
TIL 컨셉의 정말 간단한 블로그를 새로 만들고 싶어졌다. 기존의 기술 블로그에는 정제된 글을 쓰고 싶다고 한다면 새로 만들 블로그에는 그날 그날의 한 일을 부담 없이 가볍게 편하게 적어보고 싶어졌다.
그래서 새로운 블로그를 만들어보기로 했다.
기술 블로그 구현을 재활용하는 것도 방법이지만, 기술 블로그와는 다르게 최대한 심플하게 구현하고 싶기도 했고 nextjs 의 정적 페이지 생성 기능에도 흥미가 있었기 때문에 이번에는 nextjs 로 새로 구현해보려고 한다.
작업은 두 단계로 진행한다.
- 먼저 마크다운 파일을 파싱해 정적 페이지를 만드는 기능을 구현한다.
- 그리고 그렇게 만들어진 정적 페이지를 GitHub Pages 에 배포한다.
참고로 21년 6월에 Next.js 로 GitHub Pages 배포하기를 이미 해 본 적이 있긴 하지만, 당시에 배포했던 앱은 블로그 용도가 아닌 당시 재직 중이었던 회사의 서비스의 특정 기능 프로토타이핑 용도였고 배포 방법 또한 다소 복잡했다. 이 글에서는 해당 글의 경험은 전혀 사용하지 않고 훨씬 간단한 새로운 방법으로 배포한다.
0. References
- Making a static site blog with the Next.js 'app' directory
- 배포 관련 코드를 제외한 대부분의 코드는 이 블로그의 코드를 기반으로 했다.
- nextjs-github-pages
- 배포 관련 코드는 이 레포지토리의 코드를 거의 그대로 가져다 썼다.
1. 정적 페이지 생성 기능 구현
1.1. 프로젝트 생성
먼저 next.js 프로젝트를 생성한다.
$ npx create-next-app@latest
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … Yes
✔ What import alias would you like configured? … ~/*
1.2. 마크다운 파일 파싱 기능 구현
그리고 아래 라이브러리들을 설치한 후
$ npm install gray-matter \
unified \
remark-gfm \
remark-parse \
remark-rehype \
rehype-stringify \
rehype-autolink-headings \
rehype-slug \
@leafac/rehype-shiki \
shiki
src/lib/api.ts
파일을 생성해 마크다운 파일 파싱을 위한 기능을 작성한다.
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeSlug from 'rehype-slug';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import { unified } from 'unified';
const postsDirectory = path.join(process.cwd(), '_posts');
function getPostFiles() {
return fs.readdirSync(postsDirectory);
}
function getParser() {
return unified()
.use(remarkParse)
.use(remarkRehype)
.use(remarkGfm)
.use(rehypeStringify)
.use(rehypeStringify)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, {
content: arg => ({
type: 'element',
tagName: 'a',
properties: {
href: `#${String(arg.properties?.id)}`,
style: 'margin-right: 10px',
},
children: [{ type: 'text', value: '#' }],
}),
});
}
const parser = getParser();
// 특정 글의 데이터를 가져온다
export async function getPostById(id: string) {
const realId = id.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, `${realId}.md`);
const { data, content } = matter(await fs.promises.readFile(fullPath, 'utf8'));
const html = await parser.process(content);
const date = data.date as Date;
return {
...data,
title: data.title as string,
id: realId,
date: `${date.toISOString().slice(0, 10)}`,
html: html.value.toString(),
};
}
// 모든 글의 데이터를 가져온다.
export async function getAllPosts() {
const posts = await Promise.all(getPostFiles().map(id => getPostById(id)));
// 날짜 내림차순 정렬
return posts.sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
}
1.3. 글 목록 페이지 구현
파싱해온 데이터로 글 목록 페이지를 구현해보자.
유의할 점은 현재 구현하려는 블로그가 "TIL 컨셉의 심플한 블로그" 이기 때문에, 글 목록 페이지에서 각 글의 내용도 전부 보여줄 것이다. 만약 글 목록에서 각 글의 내용을 초반부 약간만 보여주고 싶거나 아예 보여주고 싶지 않다면 코드 수정이 필요할 것이다.
import Link from 'next/link';
import { getAllPosts } from '~/lib/api';
import styles from './page.module.css';
const Page = async () => {
const posts = await getAllPosts();
return (
<div className={styles.container}>
<h1>Today I Learned</h1>
<div>
{posts.map(({ id, date, title, html }) => (
<article className={styles.post} key={id}>
<h2 className={styles.postTitle}>
<Link href={`/posts/${id}`}>
{date}
</Link>
<Link href={`/posts/${id}`}>
{title}
</Link>
</h2>
<div
className={styles.postContent}
dangerouslySetInnerHTML={{ __html: html }}
/>
</article>
))}
</div>
</div>
);
};
export default Page;
(./page.module.css
코드 내용은 생략한다.)
1.4. 글 상세 페이지 구현
글 목록 페이지를 만들었으니 글 상세 페이지도 만들어보자.
(사실 글 목록 페이지에서 각 글의 모든 내용을 보여주고 있으므로 글 상세 페이지는 없어도 그만이긴 하다.)
import { getPostById, getAllPosts } from '~/lib/api';
type Props = {
params: Promise<{ id: string }>
}
const Post = async ({ params }: Props) => {
const { id } = await params;
const { html, title, date } = await getPostById(id);
return (
<article>
<h1>{title}</h1>
<h4>{date}</h4>
<div dangerouslySetInnerHTML={{ __html: html }} />
</article>
);
};
export default Post;
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({
id: post.id,
}));
}
export async function generateMetadata({ params }: Props) {
const { id } = await params;
const { title } = await getPostById(id);
return {
title,
};
}
1.5. 마크다운 글 작성
마크다운 글은 _posts
폴더를 만들어 해당 폴더에 .md
파일을 만들어 작성한다.
---
title: "TIL - Next.js 프로젝트 생성"
date: 2025-02-10
---
내용...
유의할 점은 date
항목이 "2025-02-10"
이 아니라 2025-02-10
이어야 한다는 것이다.
글 작성까지 완료했다면 이제 정적 페이지 생성 기능으로 블로그 구현을 완료한 것이다.
yarn dev
로 개발 서버를 실행해서 글이 제대로 표시되는지 확인할 수 있다.
2. Github Pages 배포
정적 페이지 생성 기능 구현은 끝났으니 이제 생성한 정적 페이지를 Github Pages 에 배포를 해보자.
배포는 Making a static site blog with the Next.js 'app' directory 글의 내용을 따르지 않고 nextjs-github-pages 레포지토리의 내용을 따를 것이다.
nextjs-github-pages 에 워낙 잘 설명되어있긴 하지만 리마인드 개념으로 이 글에도 간단히 정리한다.
2.1. Custom Domain 지정
일단 이 글에서는 레포지토리의 Settings - Pages 에서 Custom Domian 을 지정한다고 가정한다.
Custom Domain 을 지정하지 않아도 설정이 크게 바뀌지는 않는다. 다만 바뀌어야 하는 부분이 있기는 하므로, 그 경우에는 nextjs-github-pages 를 확인해보자.
2.2. next.config.ts
수정
next.config.ts
파일도 간단히 수정하자.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
basePath: "",
images: {
unoptimized: true,
},
};
export default nextConfig;
2.3. .nojekyll
파일 생성
그리고 public
디렉토리에 .nojekyll
파일을 생성한다. 이는 GitHub Pages 에게 "우리는 Jekyll 을 사용하지 않는다"는 것을 명시하기 위함이며, 내용 없이 빈 파일이면 된다.
2.4. GitHub Actions 설정
마지막으로 .github/workflows/deploy.yml
파일을 생성해 https://github.com/gregrickaby/nextjs-github-pages/blob/main/.github/workflows/deploy.yml 파일 내용을 붙 여넣자.
2.5. 배포
지금까지의 모든 과정을 모두 완료했다면 main 브랜치로 지금까지 작업한 모든 내용을 푸시하자. 직접 푸시해도 되고 PR 로 머지해도 된다. main 에 코드가 올라가면 GitHub Actions 가 자동으로 배포를 시작한다.
배포가 끝난 뒤 Custom Domain 으로 지정한 주소에 접속하면 블로그가 제대로 배포된 것을 확인할 수 있다.
3. 결과
코드 및 배포 결과는 ricale/game-dev-study-log 에서 확인할 수 있다.
4. 이슈
위 과정에서 제대로 챙기지 못한 이슈가 남아있다.
- 글 상세 페이지에 스타일을 지정하지 않음
- 글 목록 페이지와 스타일을 통일하고 싶다면 공통 컴포넌트 작성이 필요
- 글 상세 페이지에서 새로고침 시 markdown 파일에 포함된 이미지 파일이 보이지 않는 현상이 있음
- 글 목록 페이지에서 정렬 순서를 변경할 수 없음
- 이건 일반적인 블로그 및 TIL 에서 필수 기능은 아니지만, 개인적으로 넣고 싶은 기능이다.
이슈들은 이후 시간이 되면 적용해볼 예정이다. 다만 우선순위가 굉장히 낮아서 언제 진행할지는 미지수이다.