Next.js로 개인 블로그 만들기
블로그를 만들게 된 계기
블로그를 써야겠다고 생각한 건 꽤 오래됐지만 실행은 늘 다음으로 미뤄졌다. 글로 내 생각을 정리하는 게 어렵게 느껴졌고 무엇보다 귀찮은 감정이 제일 컸다.
그러다 멘토링을 몇 번 진행하면서 블로그가 생각보다 큰 도움이 된다는 피드백을 자주 들었다. 누군가에게 설명하다 보면 내가 애매하게 알고 있던 부분이 드러나곤 했고, 그때의 고민과 배움을 글로 정리하는 과정이 곧 복습이자 학습이라는 게 더 와닿았다.
그래서 작게라도 시작해보려고 한다. 비록 좋은 글은 아니더라도 신입 때부터 기록하는 습관을 만들고, 작은 경험과 생각을 꾸준히 쌓아가면 그 자체가 성장이 될 거라고 생각한다.
직접 만든 이유
블로그 플랫폼은 이미 다양하게 존재한다. Velog, Tistory, Medium 같은 서비스를 쓰면 글쓰기에만 집중할 수 있다. 그런데 솔직히 말하면, 나는 글을 쓰고 싶었다기보다 블로그를 만들고 싶었다. 내 취향대로 디자인하고, 내가 원하는 기능만 넣고, 코드 한 줄까지 내 것인 공간. 만드는 과정 자체가 재미있을 것 같았다.
마침 예전에 Next.js로 만들어 둔 웹 포트폴리오가 있었다. 처음에는 랜딩 페이지 하나에 히어로 섹션과 소개 섹션을 스크롤로 전환하는 구조였는데, 여기에 블로그를 얹으면 새로 프로젝트를 파지 않아도 되겠다 싶었다. 기존 코드 위에 붙이니까 디자인 톤도 자연스럽게 이어졌고 도메인이나 배포 설정도 따로 건드릴 필요가 없어서 좋았다.
기술 스택 고민
Next.js
프레임워크를 고를 때 사실 여러 개를 찾아봤다. Astro가 블로그에 좋다는 글도 많았고, Gatsby, Jekyll 등.. 근데 생각해 보니까 이미 포트폴리오를 Next.js로 만들어 놨고, 최근까지 다른 프로젝트도 Next.js로 하고 있어서 제일 익숙한 상태였다. 새 프레임워크를 배우면서 블로그도 만드는 건 욕심인 것 같았고, 결국 기존 포트폴리오에 블로그를 얹는 게 가장 현실적이겠다 싶었다.
블로그에 Next.js를 쓰면서 좋다고 느낀 건 정적 생성(SSG)이다.
블로그 글은 한 번 쓰면 자주 바뀌지 않는데,
generateStaticParams로 빌드 타임에 모든 글을 미리 HTML로 만들어 둘 수 있다.
요청할 때마다 서버에서 렌더링하는 게 아니라 이미 만들어진 페이지를 바로 보여주는 거라
로딩이 빠르고 서버 부담도 없다.
블로그처럼 콘텐츠가 정적인 사이트에는 이 방식이 잘 맞는다고 느꼈다.
모노레포 + Turborepo
사실 모노레포나 Turborepo라는 단어를 처음 본 건 채용공고에서였다.
우대사항에 Monorepo, Turborepo 같은 문구가 자주 보이길래 그게 뭔지 대충만 알고 넘어가곤 했다.
그러다 최근 오픈소스 프로젝트에 기여하면서 처음으로 모노레포 구조를 직접 경험했다.
여러 패키지가 하나의 레포에 들어있고, workspace로 의존성을 관리하는 구조가
이런 거구나 하고 느꼈다.
그래서 이번에 블로그를 만들면서 학습할 겸 Turborepo로 직접 모노레포를 구성해 보기로 했다.
모노레포를 관리하는 도구로는 Nx, Lerna 같은 것들도 있었는데
Turborepo를 고른 건 설정이 제일 단순해 보여서다.
Turborepo는 모노레포에서 빌드, 린트 같은 태스크를 효율적으로 실행해 주는 빌드 시스템인데,
turbo.json 하나에 태스크를 정의하면 패키지 간 의존 관계를 보고 알아서 순서대로 돌려준다.
Next.js를 자주 쓰는 환경에서 많이 함께 언급되는 도구라 잘 맞을 것 같았다.
구조는 이렇게 잡았다.
minsu-dev/
├── apps/
│ └── web/ # Next.js 앱 (포트폴리오 + 블로그)
│ ├── src/
│ │ ├── app/ # 페이지 (about, blog)
│ │ ├── components/ # UI 컴포넌트
│ │ └── lib/ # 유틸리티
│ └── velite.config.ts # MDX 설정
├── packages/
│ └── tsconfig/ # 공유 TypeScript 설정
├── content/
│ └── blog/ # MDX 블로그 글
├── turbo.json # Turborepo 파이프라인 설정
└── pnpm-workspace.yaml # workspace 패키지 정의apps/에는 실제 앱이 들어가고, packages/에는 앱들이 공유하는 설정이나 라이브러리가 들어간다.
지금은 앱이 하나뿐이라 구조가 좀 과한 것 같기도 하지만,
나중에 앱이 추가되더라도 TypeScript 설정 같은 건 packages/tsconfig에서 한 번만 관리하면 된다.
포트폴리오 하나에 블로그를 붙이는 건데 모노레포까지 필요한가? 라고 하면 솔직히 지금은 아니다. 하지만 직접 설정해 보니까 문서만 읽었을 때보다 훨씬 이해가 잘 됐다. 한 번 빌드한 결과를 캐싱해서 코드가 바뀌지 않은 패키지는 다시 빌드하지 않는 것도 지금은 체감이 크지 않지만 패키지가 많아지면 꽤 유용할 것 같다.
스타일링 (Tailwind CSS 4)
Tailwind CSS는 웹 포트폴리오 때부터 쓰고 있었는데, 이번에 4 버전이 나와서 적용해 봤다.
3이랑 비교하면 설정이 꽤 많이 달라졌다.
tailwind.config.js가 없어지고 CSS 파일에서 @theme으로 직접 정의하는 방식인데,
처음엔 뭔가 어색했다. 근데 해보니까 설정 파일이랑 CSS를 왔다 갔다 안 해도 되니까 오히려 편했다.
:root {
--background: oklch(0.97 0.008 90);
--text-primary: oklch(0.15 0 0);
--text-secondary: oklch(0.45 0 0);
--accent: oklch(0 0 0);
--border: oklch(0.92 0.005 90);
}
@theme inline {
--color-background: var(--background);
--color-text-primary: var(--text-primary);
--color-text-secondary: var(--text-secondary);
}색상은 oklch라는 걸 써봤다.
솔직히 이게 hex나 rgb보다 뭐가 좋은지 처음엔 잘 몰랐는데,
써보니까 밝기를 바꿀 때 색이 이상하게 안 변해서 좋았다.
oklch(0.45 0 0)에서 첫 번째 숫자만 바꾸면 같은 무채색에서 밝기만 달라지는데,
rgb에서는 이게 잘 안 됐던 것 같다. 아직 완전히 이해한 건 아니고 써보면서 배우는 중이다.
MDX 라이브러리 (Velite)
블로그 글을 MDX로 쓰려면 그걸 읽어서 페이지로 변환해주는 도구가 필요한데 찾아보니까 선택지가 꽤 있었다.
먼저 Next.js 공식인 @next/mdx가 있었다.
별도 라이브러리 없이 바로 쓸 수 있어서 처음엔 이걸로 하려고 했는데,
frontmatter(제목, 날짜, 카테고리 같은 메타데이터)를 직접 파싱해야 하고
글 목록을 만들려면 파일 시스템을 직접 읽는 코드를 짜야 했다.
블로그 기능을 처음부터 다 구현하는 건 좀 부담스러웠다.
그다음으로 많이 나온 게 Contentlayer였다.
Next.js 블로그 만드는 글에서 거의 다 쓰고 있길래 나도 그걸로 하려고 했는데,
GitHub를 들어가 보니까 유지보수가 거의 멈춘 것 같았다.
아직 경험이 많지 않아서 이런 라이브러리를 골랐다가 나중에 문제가 생기면
혼자 해결하기 어려울 것 같았다.
그래서 대안을 찾다가 Velite라는 걸 발견했다.
- Zod 기반으로 스키마를 정의해서 타입이 안전하다고 한다
- Contentlayer랑 쓰는 방식이 비슷하다
- 최근까지 업데이트가 되고 있다
// velite.config.ts
const posts = defineCollection({
name: 'Post',
pattern: 'blog/**/*.mdx',
schema: s.object({
title: s.string().max(120),
slug: s.slug('posts'),
date: s.isodate(),
description: s.string().max(300),
category: s.enum(['thoughts', 'dev']),
tags: s.array(s.string()).default([]),
published: s.boolean().default(true),
body: s.mdx(),
}),
});category를 s.enum으로 정의해놓으면 오타를 내면 빌드할 때 에러가 나서
실수를 미리 잡을 수 있는 게 좋았다.
잘 안 됐던 게 있는데 Velite의 webpack 플러그인이 Turbopack이랑 같이 안 돌아간다.
처음에 이게 왜 안 되는 건지 한참 찾았다.
결국 next.config.ts에 플러그인을 넣는 게 아니라
velite dev랑 next dev --turbopack을 concurrently로 따로 돌리는 방식으로 해결했다.
{
"dev": "concurrently \"velite dev\" \"next dev --turbopack\""
}Velite가 MDX를 컴파일해서 .velite 폴더에 JSON으로 넣어주면
Next.js가 그걸 읽는 구조이다.
호스팅
지금은 Vercel에 배포하고 있다. 포트폴리오가 이미 Vercel에 올라가 있어서 그대로 이어서 쓰고 있다. Git push만 하면 알아서 빌드하고 배포해주니까 정말 편하다.
근데 Vercel이 너무 편해서 그냥 Vercel 쓰면 되지 하고 넘어가다 보면 인프라에 대해 아무것도 모르는 채로 가게 될 것 같다. 나중에는 배포 환경을 직접 구성해 보려고 한다. 가상 서버에 애플리케이션을 올려서 운영해 보거나, 정적 파일을 스토리지에 두고 CDN으로 서빙하는 구조로 배포해 보면 배울 게 많을 것 같다.
구현 과정
카테고리 필터
카테고리 필터를 만들 때 두 가지가 있었다.
- 클라이언트 상태 (
useState) — 간단하지만 URL 공유 불가, 새로고침하면 초기화 - URL searchParams (
/blog?category=dev) — URL 공유 가능, 뒤로가기 자연스러움
처음엔 useState가 쉬워서 그걸로 하려고 했는데,
생각해보니 "dev 카테고리만 보기" 같은 URL을 누군가에게 공유할 수 있어야 하지 않나 싶었다.
뒤로가기로 이전 필터 상태로 돌아가는 것도 searchParams 쪽이 더 자연스러웠다.
이게 맞는 선택인지는 아직 잘 모르겠지만 일단 searchParams로 했다.
코드 하이라이팅
기술 블로그인데 코드 블록이 안 이쁘면 좀 아쉬울 것 같았다.
rehype-pretty-code라는 걸 붙였는데, 내부적으로 Shiki라는 엔진을 써서
VS Code 같은 구문 강조를 해준다.
테마를 고르는 데 은근 시간을 썼다.
github-light, vitesse-light, one-light 같은 걸 하나씩 적용해 보면서 비교했는데,
min-light가 제일 깔끔하고 사이트 분위기랑 맞는 것 같아서 그걸로 했다.
배경색은 테마 기본값을 안 쓰고 CSS에서 직접 잡았다.
사이트 전체 색이 oklch 기반인데 코드 블록만 다른 색이면 눈에 띄어서
맞춰주는 게 낫겠다 싶었다.
마치며
포트폴리오에 블로그를 붙이는 거라 금방 끝날 줄 알았는데, 막상 해보니까 고를 게 많았다. 어떻게 깔끔하게 UI를 구성할지, MDX 라이브러리를 뭘 쓸지, Turbopack이랑 어떻게 같이 돌릴지, 작은 선택들이 쌓여서 결국 하나의 블로그가 됐다.
아직 모르는 게 훨씬 많고 더 좋은 방법이 있었을 수도 있지만 일단 아는 선에서 최선을 골랐다. 나중에 돌아보면 왜 이렇게 했지? 하는 부분도 있을 것 같은데 그것도 성장의 과정이라고 생각한다.
앞으로 추가하고 싶은 것들
- 다크모드
- 목차 자동 생성
- 댓글 기능
- 관리형 플랫폼 없이 배포