25편을 차례로 본 사람의 다음 질문 — "이걸 다 어떻게 한 프로젝트에 묶지?". 마지막 챕터는 그 답. 인증 있는 블로그를 처음부터 끝까지 만들면서 시리즈 모든 개념을 한 번 더 짚는다.
완성 후 결과는 — 누구나 가입·로그인할 수 있고, 글을 쓰고 수정/삭제할 수 있고, SEO 가 잡혀 검색에 노출되며, Vercel 에 배포된 진짜 사이트. 회사 면접 포트폴리오에 그대로 들어갈 수준.
1. 기능과 기술 스택
스펙을 먼저 정한다.
| 기능 | 기술 | 참조 챕터 |
| 회원가입·로그인 | Auth.js v5 (Google OAuth) | 19 |
| 글 목록·상세 | App Router·dynamic [slug] | 3·5 |
| 글 쓰기·수정·삭제 | Server Actions + Zod | 9·10 |
| DB | PostgreSQL + Drizzle ORM | 18 (Node) |
| 이미지 | next/image + Vercel Blob | 15 |
| SEO | generateMetadata + sitemap | 7·21 |
| 인증 가드 | middleware | 13 |
| 배포 | Vercel + Neon Postgres | 20 |
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편의 흐름 그대로.
- GitHub push → vercel.com Import.
- Neon(
neon.tech) 에서 무료 Postgres 인스턴스 생성 → 연결 문자열 복사.
- Vercel · Settings · Environment Variables 에
DATABASE_URL·AUTH_SECRET·AUTH_GOOGLE_ID·AUTH_GOOGLE_SECRET 입력.
- Redeploy →
https://my-blog.vercel.app 라이브.
- 도메인 연결 → 무료 SSL 자동.
- 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편(백엔드 본격). 함께 보면 풀스택 한 묶음.