Next.js 교재 · 26편 · 완결

실전 미니 프로젝트 — 블로그 사이트 완결

25편의 모든 개념이 한 프로젝트에서 만난다. 끝까지 따라온 모두에게 박수.

블로그 사이트 완성 컨셉 일러스트

25편을 차례로 본 사람의 다음 질문 — "이걸 다 어떻게 한 프로젝트에 묶지?". 마지막 챕터는 그 답. 인증 있는 블로그를 처음부터 끝까지 만들면서 시리즈 모든 개념을 한 번 더 짚는다.

완성 후 결과는 — 누구나 가입·로그인할 수 있고, 글을 쓰고 수정/삭제할 수 있고, SEO 가 잡혀 검색에 노출되며, Vercel 에 배포된 진짜 사이트. 회사 면접 포트폴리오에 그대로 들어갈 수준.

1. 기능과 기술 스택

스펙을 먼저 정한다.

기능기술참조 챕터
회원가입·로그인Auth.js v5 (Google OAuth)19
글 목록·상세App Router·dynamic [slug]3·5
글 쓰기·수정·삭제Server Actions + Zod9·10
DBPostgreSQL + Drizzle ORM18 (Node)
이미지next/image + Vercel Blob15
SEOgenerateMetadata + sitemap7·21
인증 가드middleware13
배포Vercel + Neon Postgres20

2. 디렉토리 구조

my-blog/ ├── app/ │ ├── (marketing)/ │ │ ├── layout.tsx ← 마케팅 페이지 layout │ │ ├── page.tsx ← 홈 (랜딩) │ │ └── about/page.tsx │ ├── (app)/ │ │ ├── layout.tsx ← 앱 페이지 layout (헤더에 사용자명) │ │ ├── posts/ │ │ │ ├── page.tsx ← 글 목록 │ │ │ ├── [slug]/page.tsx ← 글 상세 │ │ │ └── new/page.tsx ← 글 쓰기 │ │ └── dashboard/page.tsx ← 내 글 관리 │ ├── api/ │ │ └── auth/[...nextauth]/route.ts │ ├── layout.tsx ← 루트 layout (Inter + Noto KR) │ ├── sitemap.ts │ ├── robots.ts │ └── globals.css ├── lib/ │ ├── auth.ts ← Auth.js 설정 │ ├── db/ │ │ ├── index.ts ← Drizzle 클라이언트 │ │ └── schema.ts ← users·posts 테이블 │ ├── actions.ts ← Server Actions │ └── env.ts ← Zod 환경변수 검증 ├── components/ │ ├── PostCard.tsx │ ├── PostForm.tsx ← 'use client' │ └── ... ├── middleware.ts ← /dashboard 인증 가드 └── .env.local ← 비밀 (.gitignore)

3·4편 폴더 구조 + 4편 라우트 그룹 (marketing)·(app) 으로 layout 분리. 마케팅 페이지는 큰 hero, 앱 페이지는 깔끔한 헤더.

3. 핵심 파일 4개 미리보기

app/posts/[slug]/page.tsx — 글 상세

import { notFound } from 'next/navigation'; import { db } from '@/lib/db'; import type { Metadata } from 'next'; export async function generateMetadata({ params }): Promise<Metadata> { const { slug } = await params; const post = await db.posts.findBySlug(slug); if (!post) return {}; return { title: post.title, description: post.excerpt, openGraph: { images: [post.coverImage] }, }; } export default async function PostPage({ params }) { const { slug } = await params; const post = await db.posts.findBySlug(slug); if (!post) notFound(); return ( <article> <h1>{post.title}</h1> <p className="meta">{post.author.name} · {post.publishedAt}</p> <div dangerouslySetInnerHTML={{ __html: post.bodyHtml }} /> </article> ); }

5편 동적 라우팅 + 7편 generateMetadata + 8편 async 데이터 페칭.

lib/actions.ts — 글 쓰기 액션

'use server'; import { z } from 'zod'; import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; const PostSchema = z.object({ title: z.string().min(2), body: z.string().min(10), }); export async function createPost(_prev, formData: FormData) { const session = await auth(); if (!session) return { error: '로그인 필요' }; const parsed = PostSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors }; const post = await db.posts.create({ ...parsed.data, authorId: session.user.id, slug: slugify(parsed.data.title), }); revalidatePath('/posts'); redirect(`/posts/${post.slug}`); }

9편 Server Action + 10편 useActionState 패턴 + 19편 인증 + 17편 캐시 무효화.

middleware.ts — 가드

import { auth } from '@/lib/auth'; export default auth((req) => { const isProtected = req.nextUrl.pathname.startsWith('/dashboard') || req.nextUrl.pathname.startsWith('/posts/new'); if (isProtected && !req.auth) { const url = req.nextUrl.clone(); url.pathname = '/login'; return Response.redirect(url); } }); export const config = { matcher: ['/dashboard/:path*', '/posts/new'], };

13편 미들웨어 + 19편 Auth.js.

app/sitemap.ts

import type { MetadataRoute } from 'next'; import { db } from '@/lib/db'; export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const posts = await db.posts.findAll(); return [ { url: 'https://my-blog.com', priority: 1.0 }, { url: 'https://my-blog.com/posts', priority: 0.8 }, ...posts.map(p => ({ url: `https://my-blog.com/posts/${p.slug}`, lastModified: p.updatedAt, })), ]; }

21편 SEO 의 자동 sitemap.

4. 배포 — Vercel + Neon

20편의 흐름 그대로.

  1. GitHub pushvercel.com Import.
  2. Neon(neon.tech) 에서 무료 Postgres 인스턴스 생성 → 연결 문자열 복사.
  3. Vercel · Settings · Environment Variables 에 DATABASE_URL·AUTH_SECRET·AUTH_GOOGLE_ID·AUTH_GOOGLE_SECRET 입력.
  4. Redeploy → https://my-blog.vercel.app 라이브.
  5. 도메인 연결 → 무료 SSL 자동.
  6. Search Console 에 sitemap.xml 등록.

한 시간 안에 진짜 운영되는 블로그. 카페에서 자랑할 만하다.

5. 학습 여정 — 다음 단계

26편을 끝낸 시점에서 추천 다음 진로 4가지.

  • React 25편(같은 시리즈 React) — 안 봤다면 거꾸로 보기. 컴포넌트 본체를 더 깊게.
  • Node.js 26편(같은 시리즈 Node) — 백엔드를 본격적으로. JWT·DB·Express 다 별도 챕터.
  • TypeScript 심화·React Native — 모바일까지 한 언어로.
  • 실제 서비스 운영 — 부족한 게 가장 빨리 보이는 곳. 사이드 프로젝트 하나 발행.
26편을 끝낸 당신 — React 의 컴포넌트 모델, Next.js 의 풀스택 흐름, App Router 의 모든 특수 파일, 데이터 페칭 4층 캐시, Server Actions·Auth.js·미들웨어·SEO·배포까지 — 회사 신입~주니어 백엔드/풀스택의 표준 역량을 다 갖췄다. 면접에서 답 못 할 질문이 거의 없다. 다음은 실제 사용자가 쓰는 서비스를 만들면서 부족함을 배워가는 단계.

마치며

26편을 다 따라온 모두에게 진심으로 박수. 한 달간 매일 한 챕터씩 봤다면 — 그 자체가 흔치 않은 끈기다. 이 시리즈가 누군가의 첫 풀스택 프로젝트의 시작점이었으면 좋겠다. 코드는 만들어보면 늘고, 만들지 않으면 잊힌다. 오늘 create-next-app my-first-real-site 부터.

Next.js 시리즈 26편 완결 🎉

다음 시리즈는 — Node.js 26편(백엔드 본격). 함께 보면 풀스택 한 묶음.

© 2026 주나이테크(주) @JUNAITECH