바이브 코딩으로 블로그를 만들어봤다

동기
바이브 코딩이 유행이다. AI에게 자연어로 요구사항을 던지면 코드가 나오는 방식. 근데 대부분 데모 수준이거나 TODO 앱 같은 간단한 예제에 머문다. 실제로 배포하고 운영할 수 있는 프로덕션 수준의 결과물이 나올 수 있는지가 궁금했다.
그래서 실험을 하나 해봤다. Claude Code CLI로 코드를 한 줄도 직접 치지 않고 블로그를 처음부터 끝까지 만들어보기로 했다. 콘텐츠 파이프라인, 댓글 시스템, 뉴스 자동화, 카테고리 시스템까지 전부. 결론부터 말하면, 됐다.
기술 스택 — 선택의 이유
기술 선택은 아무리 AI가 코드를 짜줘도 결국 사람이 해야 한다. 각 선택에는 이유가 있었다.
Next.js 16 (App Router + Turbopack)
React Server Components 기반의 정적 생성이 블로그에 딱 맞다. generateStaticParams로 빌드 타임에 모든 페이지를 미리 만들어두면 런타임 비용이 제로에 가깝다. Turbopack 덕에 개발 서버 시작도 빠르고, App Router의 레이아웃 시스템으로 공통 UI를 깔끔하게 관리할 수 있다.
Velite — Contentlayer를 대체한 이유
원래 MDX 블로그의 정석은 Contentlayer였는데, 유지보수가 중단됐다. Velite는 그 빈자리를 Zod 기반 스키마 검증과 MDX transform 파이프라인으로 채워준다. #site/content라는 alias로 빌드된 콘텐츠를 타입 안전하게 import할 수 있어서, 프론트매터 필드 하나 빠뜨리면 빌드 타임에 에러가 난다. 런타임에서 터지는 것보다 훨씬 낫다.
Tailwind CSS 4
v4부터 CSS-first 설정으로 바뀌면서 tailwind.config.js가 필요 없어졌다. @tailwindcss/typography 플러그인 하나면 MDX로 작성한 prose 콘텐츠가 예쁘게 스타일링된다. 별도 CSS 파일 없이 다크모드까지 처리되니 블로그 같은 콘텐츠 중심 사이트에 안성맞춤이다.
Supabase
댓글 같은 단순 CRUD에 Firebase를 쓰기엔 오버스펙이고, 직접 DB를 운영하기엔 사이드 프로젝트치고 부담스럽다. Supabase는 PostgreSQL 기반이라 나중에 쿼리가 복잡해져도 유연하게 대응할 수 있고, RLS 없이 API Route에서 서버사이드로만 접근하면 설정도 단순하다.
Vercel
Next.js를 만든 회사의 플랫폼이니 네이티브 지원은 당연하고, push만 하면 프리뷰 배포가 자동으로 뜬다. ISR도 별도 설정 없이 동작한다.
아키텍처와 핵심 구현
콘텐츠 파이프라인: Velite + MDX
블로그의 핵심은 콘텐츠 파이프라인이다. MDX 파일을 작성하면 Velite가 프론트매터를 검증하고, 코드 블록에 syntax highlighting을 입히고, 타입이 붙은 객체로 변환해준다.
velite.config.ts의 스키마 정의부를 보면 구조가 명확하다:
// velite.config.ts
collections: {
posts: {
name: "Post",
pattern: "posts/**/*.mdx",
schema: s
.object({
title: s.string().max(120),
slug: s.path(),
date: s.isodate(),
description: s.string().max(300),
category: s.string().default("uncategorized"),
tags: s.array(s.string()).default([]),
published: s.boolean().default(true),
body: s.mdx(),
})
.transform((data) => {
const slugAsParams = data.slug.split("/").slice(1).join("/");
return {
...data,
slug: slugAsParams,
permalink: `/posts/${slugAsParams}`,
};
}),
},
},s.path()가 파일 경로에서 슬러그를 자동 추출하고, .transform()에서 posts/ prefix를 제거해 URL 친화적인 형태로 변환한다. s.isodate()나 s.string().max(120) 같은 Zod 기반 검증이 빌드 타임에 돌아가니까, 프론트매터를 잘못 쓰면 배포 전에 잡힌다.
코드 블록 스타일링은 rehype-pretty-code + Shiki 조합으로 처리했다. 다크모드와 라이트모드에서 각각 GitHub 테마가 적용된다:
mdx: {
rehypePlugins: [
[rehypePrettyCode, {
theme: { dark: "github-dark", light: "github-light" },
}],
],
},뉴스 자동화 파이프라인
이 블로그에는 매일 아침 미국 뉴스를 영어 학습용으로 요약해서 자동 발행하는 기능이 있다. 전체 파이프라인은 이렇다:
RSS 파싱 → GPT-4o 요약 → DALL-E 3 이미지 생성 → MDX 파일 생성 → GitHub Actions 자동 커밋
GitHub Actions의 cron(0 0 * * *, KST 09:00)이 매일 이 스크립트를 실행한다. 프롬프트 엔지니어링이 핵심인데, 한 번의 API 호출로 B1-B2 레벨 영어 요약, 어휘 학습 섹션, 이미지 프롬프트까지 전부 뽑아낸다.
멱등성도 신경 썼다. 같은 날짜 파일이 이미 존재하면 스킵한다:
const filePath = join(postsDir, `${dateStr}-us-news.mdx`);
if (existsSync(filePath)) {
console.log(`[Skip] ${fileName} already exists`);
return;
}GitHub Actions 워크플로우도 변경 사항이 있을 때만 커밋하도록 했다:
- name: Check for new files
id: check
run: |
if [ -n "$(git status --porcelain content/posts/ public/images/news/)" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
- name: Commit and push
if: steps.check.outputs.has_changes == 'true'
run: |
git add content/posts/ public/images/news/
git commit -m "content: add daily US news brief $(date +%Y-%m-%d)"
git push댓글 시스템
인증 없는 익명 댓글을 구현했다. 로그인 벽을 세우면 사이드 프로젝트 블로그에 누가 댓글을 달겠나. 대신 한글 형용사 + 동물 조합으로 랜덤 닉네임을 생성한다:
// src/lib/nicknames.ts
const adjectives = [
"신나는", "졸린", "배고픈", "용감한", "수줍은",
"느긋한", "씩씩한", "다정한", "엉뚱한", "호기심많은",
// ...20개
];
const animals = [
"돌고래", "고양이", "강아지", "다람쥐", "토끼",
"펭귄", "부엉이", "여우", "판다", "코알라",
// ...20개
];
export function generateNickname(): { nickname: string; emoji: string } {
const adjIdx = Math.floor(Math.random() * adjectives.length);
const aniIdx = Math.floor(Math.random() * animals.length);
return {
nickname: adjectives[adjIdx] + animals[aniIdx],
emoji: animalEmojis[aniIdx],
};
}20 x 20 = 400가지 조합이 나온다. "배고픈판다"나 "엉뚱한해파리" 같은 닉네임이 랜덤으로 배정된다.
Supabase를 클라이언트에서 직접 호출하지 않고 Next.js API Route로 감쌌다. 서버사이드에서만 Supabase에 접근하니까 키가 클라이언트에 노출되지 않는다:
// src/app/api/comments/route.ts
export async function POST(request: NextRequest) {
const body = await request.json();
const { post_slug, nickname, emoji, content } = body;
const { data, error } = await supabase
.from("comments")
.insert({ post_slug, nickname, emoji, content })
.select("id, nickname, emoji, content, created_at")
.single();
return NextResponse.json(data, { status: 201 });
}카테고리 시스템
별도 설정 파일이나 CMS 없이 MDX 프론트매터의 category 필드만으로 자동 네비게이션을 생성한다. getAllCategories()가 빌드 타임에 전체 글 목록에서 카테고리를 추출하고, 이걸 루트 레이아웃에서 헤더에 주입한다:
// src/lib/categories.ts
export function getAllCategories(): string[] {
const categories = new Set<string>();
for (const post of posts) {
if (post.published && post.category !== "uncategorized") {
categories.add(post.category);
}
}
return Array.from(categories).sort();
}// src/app/layout.tsx
export default function RootLayout({ children }) {
const categories = getAllCategories();
return (
<html lang="ko" suppressHydrationWarning>
<body>
<ThemeProvider>
<Header categories={categories} />
{children}
</ThemeProvider>
</body>
</html>
);
}카테고리별 페이지도 generateStaticParams로 정적 생성한다. 새 카테고리가 필요하면? 그냥 글 프론트매터에 새 카테고리명을 쓰면 된다. 빌드할 때 자동으로 페이지가 생긴다:
// src/app/categories/[category]/page.tsx
export function generateStaticParams() {
return getAllCategories().map((category) => ({
category: encodeURIComponent(category),
}));
}다크모드 & UX
다크모드는 next-themes를 사용했다. SSR 환경에서 다크모드의 고질적인 문제는 hydration mismatch인데, suppressHydrationWarning과 ThemeProvider의 조합으로 해결했다.
스크롤 복원도 신경 쓴 부분이다. 브라우저 기본 scrollRestoration이 SPA에서 제대로 동작하지 않아서, manual로 전환하고 커스텀 ScrollRestore 컴포넌트로 직접 처리했다. 핵심은 이 설정이 FOUC보다 먼저 실행되어야 한다는 건데, inline script로 <head>에 넣어서 해결했다:
<head>
<script dangerouslySetInnerHTML={{
__html: `history.scrollRestoration="manual"`,
}} />
</head>바이브 코딩 회고
코드를 한 줄도 직접 안 치고 이 정도 수준의 블로그가 나왔다는 사실 자체가 인상적이다. 콘텐츠 파이프라인, 자동화 시스템, 댓글, 카테고리, 다크모드까지 — 사이드 프로젝트로 이걸 주말에 직접 다 짰으면 몇 주는 걸렸을 거다.
사이드 프로젝트가 항상 보일러플레이트 단계에서 지쳐 중단됐던 경험이 있는 개발자라면 공감할 텐데, 바이브 코딩은 아이디어에서 결과물까지의 거리를 극적으로 줄여준다. 초기 세팅, 반복적인 CRUD, 설정 파일 작성 같은 지루한 부분을 AI가 처리해주니까 정작 중요한 설계와 의사결정에 집중할 수 있었다.
"프롬프트 = 설계"라는 관점이 점점 와닿는다. 요구사항을 명확하게 전달하는 능력이 곧 개발 실력이 되는 시대다. "댓글 시스템 만들어줘"라고 하면 평범한 결과가 나오지만, "인증 없이 한글 형용사+동물 조합 닉네임을 랜덤 생성하고, Supabase를 API Route로 감싸서 서버사이드에서만 접근하게 해줘"라고 하면 전혀 다른 결과가 나온다.
그리고 이건 경력 개발자에게 오히려 더 강력하다. 아키텍처 감각이 있으니까 AI 출력물의 품질을 판단하고 방향을 잡을 수 있다. "이 구조는 나중에 문제가 될 거니까 이렇게 바꿔줘"라고 말할 수 있는 건, 그 문제를 직접 겪어본 사람만 가능하다.
바이브 코딩이 개발자를 대체하는 게 아니다. 개발자의 생산성을 폭발적으로 높여주는 도구다. 설계를 할 줄 아는 사람이 쓰면 그 설계대로 코드가 나오고, 설계를 모르는 사람이 쓰면 동작은 하지만 유지보수할 수 없는 코드가 나온다. 결국 도구가 강력해질수록 그 도구를 다루는 사람의 역량이 더 중요해진다.
댓글
불러오는 중...