NextJS 블로그 만들기 1: NextJS blog-starter
2025.03.23,
7 min.
왜 만들까?
그래도 개발자인데 블로그 한번 만들어볼까?
블로그를 시작하려고 마음먹었을 때 가장 먼저 떠올린 생각입니다.
하지만 velog, tistory, medium 등 이미 많은 템플릿 기반 플랫폼이 잘 갖춰져 있어 처음엔 그런 서비스들을 사용하는 것을 고려했습니다.
하지만, velog
는 개발 중심의 글을 올리는 분위기가 강했습니다.
따라서 일상이나 다른 주제에 대해서도 자유롭게 다루고 싶은 제게는 약간의 제약처럼 느껴졌습니다.
그리고, tistory
는 광고가 많아 글을 읽는 흐름이 끊기는 점이 아쉬웠습니다.
결국 여러 플랫폼을 둘러봐도 제 마음에 딱 맞는 공간을 찾을 수는 없었습니다.
그래서 조금 허접해보일 수는 있겠지만, 토이 프로젝트라 생각하고 직접 블로그를 만들기로 결심했습니다.
처음엔 마음에 드는 디자인을 고치는 데 시간도 걸리고 기능을 하나하나 구현하는 게 쉽지는 않았지만, 오히려 나만의 것을 만들어가는 것에 의미를 두었습니다.
또한 앞으로 다크 모드와 같은 다양한 기능을 추가하며 블로그를 계속 발전시키고, 이런 과정 자체도 하나의 이야기로 블로그에 담아보면 어떨까 하여 직접 만들어보기로 결정하였습니다.
레퍼런스
가장 먼저, NextJS 공식 문서에는 NextJS를 활용해서 만든 여러 예시들을 제공해줍니다.
그중에서 blog-starter는 NextJS로 만든 간단한 블로그 예시입니다. (GitHub)
따라서 저는 이 예시를 참고하여 직접 블로그를 직접 만들어보고자 하였습니다.
또한, 다른 분들의 개발 블로그도 많이 둘러보았었는데요,
저는 두 분의 블로그를 주로 참고하였습니다.
개발 환경
언어는 TypeScript
, 그리고 프레임워크는 Next 15
기반으로 구현하였습니다.
Metadata를 통해 쉽게 SEO
를 적용할 수 있기도 하고, SSR, SSG, ISR 등 다양한 렌더링 방식을 적용할 수 있습니다.
무엇보다 폴더 구조로 페이지 라우팅을 확인할 수 있는 것이 개인적으로는 가장 직관적인 방법이라 생각하였습니다.
이외에도 이미지 최적화와 같은 여러 기능들을 제공하기 때문에 선택하게 되었습니다.
스타일링은 이전 디프만 15기에서 사용했던 zero-runtime css인 Panda CSS
를 선택하였습니다.
디자이너분들께서 정의해주신 디자인 시스템을 Token이나 Text Style 등으로 편하게 정의해서 활용하기 좋았고, 스타일 병합 함수인 cx를 제공하는 등 개인적으로 편리하게 사용했었기 때문입니다.
또한 스타일 코드를 분리하여 관리하는 것을 선호하여 Tailwind CSS는 후보에서 제외하였습니다.
NextJS blog-starter
이제 베이스가 될 예시 소스 코드를 분석해보았습니다.
라우팅 구조는 다음과 같이 매우 간단했어요.
- /
- /posts/[slug]
단순히 메인 페이지와 게시글 페이지인 /posts/[slug]
에서 slug에 해당하는 게시글을 가져와 마크다운을 파싱하여 화면에 보여줍니다.
그럼 게시글은 어떻게 가져올까요?
src 외부의 public 폴더에 정적 파일들을 담아두는 것과 마찬가지로, posts
폴더에 작성한 게시글들이 md 파일로 존재하고, 이를 lib/api 내에서 fs
를 활용하여 게시글을 가져오게 됩니다.
아래는 실질적으로 md 파일을 통해 게시글을 가져오는 lib
내부의 api.ts
의 함수들입니다.
export function getPostSlugs() {
return fs.readdirSync(postsDirectory);
}
export function getPostBySlug(slug: string) {
const realSlug = slug.replace(/\.md$/, "");
const fullPath = join(postsDirectory, `${realSlug}.md`);
const fileContents = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(fileContents);
return { ...data, slug: realSlug, content } as Post;
}
export function getAllPosts(): Post[] {
const slugs = getPostSlugs();
const posts = slugs
.map((slug) => getPostBySlug(slug))
// sort posts by date in descending order
.sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
return posts;
}
함수들의 동작은 다음과 같습니다.
getPostsSlugs
함수를 통해 posts
폴더에 존재하는 모든 게시글들의 파일 이름을 불러옵니다.
getPostBySlug
함수에서는 slug를 파라미터로 받아 해당 slug와 일치하는 md 파일을 불러와 파싱합니다.
이때 파싱은 gray-matter
의 matter
함수를 통해 진행됩니다.
matter
함수는 아래의 Before와 같이 md 파일 상단에 작성된 파일의 정보를 분석하여 After와 같이 게시글 정보 객체를 생성해줍니다.
/* Before */
---
slug: Blog-Start
title: "블로그 시작!"
description: "이제 글쓰기를 곁들인..."
tags: ["life", "essay"]
createdAt: "2025.02.22"
---
...
/* After */
{
slug: "Blog-Start",
title: "블로그 시작!",
description: "이제 글쓰기를 곁들인...",
tags: ["life", "essay"],
createdAt: "2025.02.22",
content: 본문 내용
}
마지막으로 getAllPosts
함수는 위의 두 함수를 호출하여 모든 게시글들의 정보를 가져와 날짜순으로 정렬합니다.
어떻게 활용될까?
먼저, getAllPosts
를 통해 메인 페이지에서 모든 글들의 썸네일과 날짜, 제목 등의 간단한 정보들을 보여줍니다.
blog-starter 1
그리고 위의 게시글들 중 하나를 클릭했을때, getPostBySlug
함수를 통해 /posts/[slug]
의 경로에서 해당 게시글의 대한 정보를 파싱하여 아래와 같이 게시글 정보와 본문을 보여줍니다.
blog-starter 2
구현
전반적으로 Next-blog-starter 예시를 베이스로 구현하였습니다.
API의 경우에 따로 추가한 기능은 아래와 같습니다.
-
일반적으로 블로그들을 돌아다니면서 태그나 카테고리에 대한 사용성이 좋다고 느꼈습니다.
모든 글이 뭉탱이로 있는 것보다 원하는 정보를 빠르게 찾는데 용이하였기 때문입니다. -
그리고 각 태그 별로 글이 몇개 존재하는지 함께 보여주면 블로그 주인분께서 어디에 관심이 많고 또 집중하고 계신지 먼저 은근히 파악이 되어 좋았습니다.
따라서 위 두 기능에 대한 API 함수를 추가하고 재사용하는 함수만 분리하였습니다. ("/lib/api.ts")
.DS_Store
파일로 인해 에러가 발생할 수 있습니다.게시글 페이지 또한 마찬가지로 위의 API 함수를 통해 파싱된 데이터를 통해 화면을 구성하였습니다.
본문은 next-mdx-remote
라이브러리를 활용하여 본문이 마크다운 형식으로 나타나게 하였습니다.
해당 라이브러리가 좋았던 것은 아래와 같이 components에 blockquote나 img 등과 같이 자동 변환되는 태그에 대응되는 커스텀 컴포넌트를 추가하거나, 원하는 컴포넌트를 추가하여 mdx 파일 내에서 사용할 수 있다는 점이였습니다.
const PostBody = ({ post }) => {
/* ... */
return (
<MDXRemote
source={post.content}
components={{
code: Code,
blockquote: BlockQuote,
img: Image,
a: CustomLink,
CallOut,
}}
options={mdxOptions}
/>
)
}
추가적으로 remark와 rehype를 위해 사용된 mdx 플러그인들은 아래와 같습니다.
- remark
- remarkGfm : Github Flavored Markdown로 GitHub 맛 마크다운 사용
- remarkA11yEmoji : 마크다운을 HTML으로 변환 시 이모티콘 최적화 적용
- remarkBreaks : new line 한번으로 줄바꿈이 가능
- rehype
- rehypeSlug : h1 ~ h6, heading에 id를 추가
- rehypePrettyCode : 코드 블록 커스텀
rehypeSlug
는 TOC 구현에 유용하게 사용되어 다음 블로그 제작기 2편에서 등장할 예정입니다.
그리고 rehypePrettyCode
.. 이 친구를 커스텀하다 시간을 다 쓴 것 같습니다.
이렇게 첫 구현을 끝냈습니다.
원하는대로 블로그를 만들다보니 커스텀과 색상, 그리고 디자인 등에 대해 생각하다 시간이 오래 걸리는 부분들이 많았지만..
끝내고 잘 동작하는 것을 보니 너무나 뿌듯했습니다.
이후 TOC, Giscus를 통한 댓글 기능, 마크다운 컴포넌트 추가 등 여러 기능을 추가해 나가고 있습니다.
이 또한 글로 남기고 주기적으로 계속 유지보수, 새로운 기능을 추가를 하면서 기록해보려고 합니다 :)