[블로그 만들기 #1] Next.js & Vecel 이용해서 30분만에 블로그를 만들어보자
![Cover Image for [블로그 만들기 #1] Next.js & Vecel 이용해서 30분만에 블로그를 만들어보자](/assets/blog/vercel-blog-01/cover.webp)
백엔드 개발자의 초간단 개인 블로그 구축기 시리즈. Next.js blog-starter-kit을 이용해서 일단 띄우고 하나씩 개선해 나가는 과정을 알아봅시다.
들어가며
왜 자체 블로그인가?
개발자라면 블로그 하나 정도는 있어야 한다는 생각으로 여러 플랫폼 고민하던 중...
- Notion: 뭔가 안 끌림
- Naver Blog: 개발자가...네이버 블로그...?
- Tistory: fancy하지 않음
- Medium: 국산품 애용해야지!
이런 저런 핑계를 대며 개발자의 낭만을 위해 직접 구축하기로 결심했습니다.
어떻게 만들지?
근데 또 직접 다 만들기에는 너무 귀찮고, 서버 돌리기에는 돈 아까워서 비용 들어가지 않고, (MD 파일만 잘 관리하면 나중에 옮기기 쉬울거라고 행복 회로를 돌리며) 사람들이 배포 쉽다고 말하는 Vercel에서 밀어주는 Next.js를 이용해서 만들어보기로 결정했습니다.
테마 선택
React, Next.js, Tailwind CSS등 백엔드 개발자에게 익숙하지 않은 부분들 다 공부하고 시작하면 평생 글 못쓰고, 하루 한명 올까말까한 블로그에 디자인보다는 내용이 우선이다라는 마음으로 그나마 봐줄만하고, 유지보수 편하게 폴더 구조 간단한 blog-starter-kit을 선택했습니다. (저희같은 작고 소중한 블로그는 일단 돌려보고 고쳐나가는게 가장 빠르고 효율적입니다.)
설치 및 테스트
npx create-next-app --example blog-starter blog-test --typescript
Need to install the following packages:
create-next-app@15.4.3
Ok to proceed? (y) y
cd blog-test
폴더 구조 확인 한번 해주고
blog-test
├── _posts #이 폴더 내부에 md 파일 작성하면 게시글이 됩니다.
├── next-env.d.ts
├── node_modules
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── README.md
├── src
├── tailwind.config.ts
└── tsconfig.json
package.json
파일에서 react, next.js 버전들 최신으로 바꿔서 설치 후 개발용으로 실행해서 테스트 해보겠습니다.
// packate.json
{
"private": true,
...
"dependencies": {
...
"next": "15.3.0",
"react": "19.1.0",
"react-dom": "19.1.0",
},
"devDependencies": {
...
"@types/node": "^22.13.0",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
}
}
npm install && npm run dev
> dev
> next dev --turbopack
▲ Next.js 15.3.3 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.0.69:3000
이제 브라우저에서 Local 주소인 http://localhost:3000
접속하면
일단 뭔가 블로그답게 렌더링된 화면을 보실 수 있습니다.
다크모드 없애기
화면 왼쪽 아래에 error 표시는 다크모드 설정 오류 때문에 나오는건데요, 지금은 테마 설정에 신경쓸 단계가 아닙니다. 블로그 전체 레이아웃을 담당하는 src/layout.tsx
파일에서
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
...
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
</head>
<body className={cn(inter.className, "dark:bg-slate-900 dark:text-slate-400")}>
<ThemeSwitcher /> // 다크모드 테마 설정하는 친구
<div className="min-h-screen">{children}</div>
<Footer />
</body>
</html>
);
}
다크모드 테마 설정해주는 ThemeSwitcher
컴포넌트를 없애버리면
이렇게 쓸데없이 관리 포인트만 많아지는 다크모드 꺼버릴 수 있습니다.
작동 구조 파악하기
저희같이 블로그 시작 단계에서는 함수 세부 구현 원리보다는 블로그에서 가장 중요한 포스팅 정보를 가지고 있는 md 파일들이 어떻게 생겼고, 어떻게 렌더링 되는지에 집중해야 합니다. 포스팅 파일들을 보관하는 _posts
폴더 보시면 이렇게 3개의 파일들이 보이실텐데요
_posts
├── dynamic-routing.md
├── hello-world.md
└── preview.md
preview.md
파일을 예시로 알아보겠습니다.
---
title: "preview.md 파일 제목입니다."
excerpt: "preview.md 파일 설명입니다."
coverImage: "/assets/blog/preview/cover.jpg"
date: "2025-01-01T05:35:07.322Z"
author:
name: Joe Haddad
picture: "/assets/blog/authors/joe.jpeg"
ogImage:
url: "/assets/blog/preview/cover.jpg"
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...
---
구분선을 기준으로 위쪽에 메타데이터들을 Key-Value 구조로 표시하고, 아래쪽에는 포스팅 본문 내용들이 들어가 있는데요, 루트 페이지 렌더링 담당하는 src/page.tsx
파일을 보시면
// src/page.tsx
...
import { getAllPosts } from "@/lib/api";
export default function Index() {
// 파싱된 포스팅 정보들을 작성일 기준 내림차순으로 가져오는 함수
const allPosts = getAllPosts();
const heroPost = allPosts[0]; // 가장 최신 포스팅 1개
const morePosts = allPosts.slice(1); // 나머지 포스팅 목록들
return (
<main>
<Container>
<Intro />
<HeroPost
title={heroPost.title}
coverImage={heroPost.coverImage}
date={heroPost.date}
author={heroPost.author}
slug={heroPost.slug}
excerpt={heroPost.excerpt}
/>
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</Container>
</main>
);
}
- getAllPosts: 파싱된 포스팅 정보들을 작성일 기준 내림차순으로 가져오고
- heroPost: 최신 포스팅 1개
- morePosts: 나머지 포스팅들
이것들을 렌더링 담당하는 각각의 컴포넌트에 전달해주면 아래 화면처럼 나오는데요
루트 페이지가 어떻게 렌더링 되는지 직관적으로 알 수 있습니다.
이제 각 포스팅 페이지 렌더링 담당하는 posts/[slug]/page.tsx
파일도 확인해 보겠습니다.
...
export default async function Post(props: Params) {
const params = await props.params;
const post = getPostBySlug(params.slug);
if (!post) {
return notFound();
}
// md 파일의 포스팅 본문을 html 문법으로 바꿔주는 함수
const content = await markdownToHtml(post.content || "");
return (
<main>
<Alert preview={post.preview} />
<Container>
<Header />
<article className="mb-32">
<PostHeader title={post.title} coverImage={post.coverImage} date={post.date} author={post.author} />
<PostBody content={content} />
</article>
</Container>
</main>
);
}
...
이것 또한 Post 컴포넌트에 메타데이터, 본문 내용들이 어떻게 연결되어서 렌더링 되는지 쉽게 확인하실 수 있습니다.
참고사항
Next.js에서 public
폴더는 이미지나 JS 파일 같은 정적 자산을 서비스하기 위한 특수한 예약 폴더입니다. 이 폴더 내부의 파일들은 **루트 URL("/")**를 기준 경로로 삼아 접근할 수 있는데요
예를 들어, preview.md
파일에서 coverImage로 제공할 public/assets/blog/preview/cover.jpg
라는 파일이 있다면, 실제 웹에서 접근할 경로는 /assets/blog/preview/cover.jpg
가 됩니다. 즉, public 폴더는 실제 URL 경로에서는 드러나지 않고, 마치 루트 디렉토리에 있는 것처럼 동작한다는 것을 알 수 있습니다.
배포하기
이제 md 파일에 포스팅 작성했다고 가정하고, 누구나 저희 블로그에 접근할 수 있도록 전 세계에 배포해 보도록 하겠습니다.
Github
깃허브에 repository 하나 만들어주고 기존 프로젝트 깔끔하게 초기화 후 push 해주시면 됩니다. (저는 private으로 진행했습니다.)
rm -rf .git && \
git init && \
git add . && \
git commit -m "first commit" && \
git remote add origin https://github.com/YOUR_ACCOUNT/YOUR_REPO.git && \
git push origin main
Vercel
vercel에 회원가입 & 로그인 후 저희 github repo에 접근할 수 있도록 Github App 설치해주시고
방금 만든 repo 선택, vercel에 import
프로젝트 Framework Preset 부분에 Next.js 설정해주시고 deploy 버튼 클릭해주시면
알아서 저장소 클론해서 빌드 후, 웹에서 접근 가능한 도메인들을 발급해 줍니다.
도메인 아무거나 하나 클릭해보면 정상적으로 접속 가능한 것을 보실 수 있습니다.
마무리
이렇게 아주 심플하고 빠르게 md 파일만으로 개인 블로그를 운영할 수 있도록 전체적인 뼈대를 만들고 작동 구조를 살펴보았습니다.
테마도 뭔가 애매하고, 도메인 주소도 마음에 들진 않지만, 일단 배포하고 고쳐나가는게 뭔가를 시작할 땐 빠르고 효율적인 것 같습니다.
다음 포스팅들을 통해서 개인 도메인 연결, 테마 변경 등 그나마 봐줄만한 수준으로 블로그 고쳐나가는 방법들을 알아보도록 하겠습니다.