지난 시간에는 댓글 기능까지 완성했습니다. 우리 블로그는 이제 글쓰기/보기/분류/검색/댓글까지 모든 핵심 기능을 갖췄어요. 마지막 글에서는 검색 엔진 최적화, sitemap/RSS, Vercel 배포까지 마무리하고, 이 시리즈와 리액트 콘텐츠 전체를 회고하며 정리하겠습니다.
Metadata API — 검색 엔진을 위한 정보
각 페이지에 적절한 <title>, <meta description>, OpenGraph 등이 있어야 검색 엔진과 SNS가 우리 글을 잘 표시해줍니다. Next.js의 Metadata API가 이 작업을 우아하게 처리합니다.
정적 metadata
src/app/layout.js의 metadata는 사이트 전체 기본값입니다.
export const metadata = {
metadataBase: new URL('https://your-blog.example.com'),
title: {
default: '나의 블로그',
template: '%s | 나의 블로그',
},
description: '리액트와 웹 개발에 대한 글들',
openGraph: {
type: 'website',
locale: 'ko_KR',
url: 'https://your-blog.example.com',
siteName: '나의 블로그',
},
};title.template은 자식 페이지에서 title을 정할 때 자동 적용되는 형식이에요. 자식이 '안녕'을 title로 두면 최종 title이 안녕 | 나의 블로그가 됩니다.
metadataBase는 OG 이미지 같은 절대 URL을 만들 때 기준이 되는 도메인. 배포 도메인으로 바꿔주세요.
동적 metadata — 글 상세 페이지
각 글 페이지의 metadata는 글의 frontmatter를 기반으로 동적으로 생성합니다. generateMetadata 함수를 export하면 됩니다.
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return {};
return {
title: post.frontmatter.title,
description: post.frontmatter.description,
openGraph: {
title: post.frontmatter.title,
description: post.frontmatter.description,
type: 'article',
publishedTime: post.frontmatter.date,
tags: post.frontmatter.tags,
},
};
}이제 각 글 페이지가 자기만의 title/description을 갖게 되고, 그게 그대로 검색 결과와 SNS 미리보기에 반영됩니다.
태그 페이지도
export async function generateMetadata({ params }) {
const { tag } = await params;
const decodedTag = decodeURIComponent(tag);
return {
title: `#${decodedTag}`,
description: `${decodedTag} 태그가 붙은 글들`,
};
}목록 페이지는 layout의 default metadata가 그대로 쓰이니 별도 설정 없어도 OK.
Sitemap — 검색 엔진에 페이지 알리기
검색 엔진 크롤러가 사이트의 모든 페이지를 빠짐없이 찾아가도록 sitemap.xml을 제공합니다. App Router에서는 sitemap.js 파일로 자동 생성할 수 있어요.
src/app/sitemap.js:
import { getAllPosts, getAllTags } from './lib/posts';
const SITE_URL = 'https://your-blog.example.com';
export default function sitemap() {
const posts = getAllPosts();
const tags = getAllTags();
const staticPages = ['', '/tags', '/search'].map(path => ({
url: `${SITE_URL}${path}`,
lastModified: new Date(),
}));
const postPages = posts.map(post => ({
url: `${SITE_URL}/posts/${post.slug}`,
lastModified: new Date(post.frontmatter.date),
}));
const tagPages = tags.map(({ tag }) => ({
url: `${SITE_URL}/tags/${encodeURIComponent(tag)}`,
lastModified: new Date(),
}));
return [...staticPages, ...postPages, ...tagPages];
}이 파일이 있으면 /sitemap.xml이 자동으로 동작합니다. Next.js가 함수 반환값을 XML로 변환해주거든요. 빌드해서 결과를 확인하면 표준 sitemap 포맷으로 출력됩니다.
Robots.txt
검색 엔진 크롤러에게 어떤 페이지를 크롤링해도 되는지 알리는 파일입니다.
src/app/robots.js:
const SITE_URL = 'https://your-blog.example.com';
export default function robots() {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}/robots.txt로 자동 노출됩니다. 모든 크롤러가 모든 페이지를 자유롭게 수집할 수 있게 허용하면서, sitemap 위치를 알려주는 표준 설정이에요.
RSS Feed
블로그를 구독하는 사용자를 위한 RSS feed도 만들어봅시다. RSS 리더에서 우리 글을 받아볼 수 있게 해주는 기능이에요.
App Router에는 RSS를 위한 자동 파일 컨벤션이 없으니, route handler로 직접 만듭니다.
src/app/feed.xml/route.js:
import { getAllPosts } from '../lib/posts';
const SITE_URL = 'https://your-blog.example.com';
const SITE_TITLE = '나의 블로그';
const SITE_DESCRIPTION = '리액트와 웹 개발에 대한 글들';
function escapeXml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
export async function GET() {
const posts = getAllPosts().slice(0, 50);
const buildDate = new Date().toUTCString();
const items = posts.map(post => `
<item>
<title>${escapeXml(post.frontmatter.title)}</title>
<link>${SITE_URL}/posts/${post.slug}</link>
<guid isPermaLink="true">${SITE_URL}/posts/${post.slug}</guid>
<pubDate>${new Date(post.frontmatter.date).toUTCString()}</pubDate>
<description>${escapeXml(post.frontmatter.description ?? '')}</description>
</item>
`).join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escapeXml(SITE_TITLE)}</title>
<link>${SITE_URL}</link>
<description>${escapeXml(SITE_DESCRIPTION)}</description>
<language>ko</language>
<lastBuildDate>${buildDate}</lastBuildDate>
<atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml" />
${items}
</channel>
</rss>
`;
return new Response(xml, {
headers: {
'Content-Type': 'application/rss+xml; charset=utf-8',
},
});
}/feed.xml로 접근하면 RSS 형식의 XML이 반환됩니다.
route.js 파일은 페이지가 아니라 API 엔드포인트를 정의하는 특별한 파일입니다. 우리 사이트의 app/feed.xml/route.ts도 거의 같은 패턴으로 RSS를 제공해요.
레이아웃의 <head>에 RSS 자동 발견 링크도 추가하면 RSS 리더가 알아서 찾아갑니다.
export const metadata = {
// ...기존 설정...
alternates: {
types: {
'application/rss+xml': '/feed.xml',
},
},
};빌드해서 확인하기
이제 한 번 production 빌드를 돌려봅시다.
npm run build빌드 로그에 각 페이지가 어떻게 생성됐는지(정적/동적), 페이지 크기, 어떤 라우트들이 만들어졌는지가 출력됩니다. /sitemap.xml, /robots.txt, /feed.xml이 라우트에 포함된 게 보일 거예요.
빌드 결과를 로컬에서 띄워보려면:
npm starthttp://localhost:3000이 production 모드로 동작합니다. dev 모드보다 훨씬 빠르고 가볍게 느껴질 겁니다 — 모든 정적 페이지가 미리 생성됐고 자바스크립트가 최소화돼 있으니까요.
Vercel 배포
이제 진짜로 인터넷에 띄울 차례입니다. Vercel이 Next.js의 공식 호스팅 플랫폼이라 배포가 가장 매끄러워요.
1. GitHub에 코드 푸시
cd my-blog
git init
git add .
git commit -m "Initial blog"GitHub에 빈 저장소를 만들고 push합니다.
git remote add origin https://github.com/<당신>/my-blog.git
git branch -M main
git push -u origin main2. Vercel 가입 + 저장소 연결
https://vercel.com에 GitHub 계정으로 가입하고, "New Project" → 방금 푸시한 저장소를 선택하면 끝입니다.
Vercel은 Next.js 프로젝트를 자동으로 감지해 적절한 빌드 설정을 적용합니다. 추가 설정 없이 첫 배포가 진행돼요.
배포가 끝나면 your-blog.vercel.app 형태의 도메인이 생기고, 그곳에서 우리 블로그가 동작합니다. 인스턴스 살아있는 동안만 댓글이 유지된다는 점은 메모리 저장소 한계 그대로지만, 글 표시는 완벽하게 동작해요.
3. 도메인 연결 (선택)
자기 도메인이 있다면 Vercel 프로젝트 설정의 "Domains" 탭에서 연결할 수 있습니다. DNS 안내가 단계별로 나와서 따라하면 됩니다.
4. 글 추가 워크플로우
이제 새 글을 쓰는 흐름은 이렇게 됩니다.
posts/new-article.mdx파일 작성git commit -m "feat(content): publish ..."git push- Vercel이 자동 감지 → 자동 빌드 → 자동 배포
git push 한 번으로 새 글이 인터넷에 올라가는 셈이에요. 이게 파일 기반 블로그의 큰 매력 중 하나입니다.
환경별 설정 — 개발 vs 프로덕션
지금까지 사이트 URL을 your-blog.example.com으로 하드코딩했는데, 환경에 따라 동적으로 설정하는 게 좋습니다.
.env.local:
NEXT_PUBLIC_SITE_URL=http://localhost:3000Vercel 프로젝트 설정의 Environment Variables에는:
NEXT_PUBLIC_SITE_URL=https://your-actual-domain.com코드에서 사용:
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000';이렇게 분리해두면 dev/preview/production 환경마다 적절한 URL이 적용됩니다.
시리즈 회고 — Next.js로 블로그 만들기
이 시리즈에서 우리는 빈 프로젝트에서 시작해 실제 운영 가능한 수준의 블로그까지 만들었습니다.
| # | 추가된 기능 | 등장한 핵심 패턴/도구 |
|---|---|---|
| 1 | 데이터 모델, 셋업 | MDX 셋업, frontmatter, 폴더 구조 |
| 2 | 글 목록 + 상세 | Server Component fs 직접 읽기, compileMDX, generateStaticParams |
| 3 | 태그 + 검색 | 동적 폴더 vs searchParams, decodeURIComponent |
| 4 | 댓글 | Server Actions, useActionState, useFormStatus, bind, revalidatePath |
| 5 | SEO + 배포 | Metadata API, sitemap, robots, RSS, Vercel |
같은 5편 분량인데 Todo 시리즈와 다른 톤이었죠. Todo는 클라이언트 사이드 패턴 위주였고, 블로그는 서버 사이드(RSC + Actions)가 중심이었습니다. 두 빌드를 모두 따라오신 분은 클라이언트와 서버 양쪽의 실전 패턴이 손에 잡혔을 거예요.
리액트 콘텐츠 31편 전체 회고
이 시리즈를 마치며 리액트 카테고리 전체 그림을 정리해보겠습니다.
A. 리액트 기초 강좌 (#1~#15) — 클라이언트 사이드 펀더멘털
JSX, 컴포넌트, props, state, useEffect, 커스텀 훅, 성능 최적화, 라우팅까지. 이게 이 위에 쌓이는 모든 것의 베이스입니다.
B. 리액트로 Todo 앱 만들기 (#1~#5) — 첫 실전 빌드
기초를 점진적으로 합쳐 작은 인터랙티브 앱을 만드는 경험. 추가 / 토글 / 필터 / 편집 / 영속화까지 자연스러운 진화 흐름.
C. 모던 리액트 + Next.js (#1~#6) — 서버/클라이언트 패러다임
Server Components, Server Actions, Suspense, streaming까지. "이 코드는 어디서 실행되나?"라는 멘탈 모델 전환.
D. Next.js로 블로그 만들기 (#1~#5) — 두 번째 실전 빌드 (이번 시리즈)
A~C에서 배운 모든 것을 합친 풀스택 블로그. 운영 가능한 수준의 결과물 + 실제 배포까지.
총 31편. 이걸 다 따라오신 분은 React 생태계의 거의 모든 핵심 흐름을 한 번씩은 만나본 셈입니다.
다음으로 갈 만한 곳
이제 본인 프로젝트를 시작할 베이스가 충분히 갖춰졌습니다. 다음 단계로 갈 만한 방향들:
즉시 도전할 만한 것
- 이 블로그를 자기 것으로 — 글을 쓰고 운영하기. 가장 빠른 학습은 진짜로 사용해보는 것
- TypeScript 마이그레이션 —
.js→.tsx. 큰 코드베이스 안전성 ↑ - 테스팅 도입 — Vitest + React Testing Library로 핵심 동작 단위 테스트
좀 더 큰 영역
- DB 연동 — Prisma 또는 Drizzle, Supabase 또는 Vercel Postgres
- 인증 — NextAuth.js / Clerk / Lucia
- 상태 관리 라이브러리 — 큰 앱에서 Zustand, Jotai, Redux Toolkit
- 데이터 페칭 라이브러리 — 클라이언트 사이드 페칭이 필요할 때 TanStack Query
또 다른 빌드
- 쇼핑몰 — 카트, 결제, 재고, 주문 — 가장 종합적인 도전
- 소셜 앱 — 게시판, DM, 알림 — 실시간성과 데이터 정합성
- 대시보드/관리자 — 차트, 테이블, 필터 — 데이터 시각화
마무리
여기까지 따라와주셔서 정말 감사합니다. 이 사이트의 기존 콘텐츠 위에 31편의 리액트 글이 더해졌습니다. 작은 컴포넌트 하나에서 시작해, 클래스 컴포넌트도 안 쓰고 (현대 React는 함수 컴포넌트 + 훅이 표준), 클라이언트 사이드의 모든 기본기 → 모던 RSC 패러다임 → 두 종류의 실전 앱까지 다뤘어요.
기억하실 한 가지가 있다면 — **"본인이 만들고 싶은 작은 것을 직접 만들어보세요"**입니다. 이 블로그를 가져다 자기 것으로 운영해도 좋고, 일상 도구를 React로 만들어봐도 좋습니다. 무엇을 만들든 막히는 곳에서 진짜 학습이 일어나요.
리액트는 빠르게 진화하는 라이브러리지만, 이 시리즈에서 다룬 펀더멘털(컴포넌트 단위 사고, 단방향 데이터 흐름, 선언적 UI, "코드가 어디서 실행되나")은 변하지 않습니다. 그 위에 새 도구가 계속 쌓일 뿐이에요. 본질을 알고 있으면 어떤 새 도구가 나와도 빠르게 익힐 수 있습니다.
다음 글에서 또 만나요!