Next.js로 정적 사이트 만들기: 완벽 가이드
정적 사이트 생성기(SSG)는 블로그, 포트폴리오, 문서 사이트에 최적의 선택지입니다. Next.js는 React 기반이면서도 강력한 정적 내보내기 기능을 제공하여, 개발 경험과 성능 모두를 잡을 수 있습니다. 이 글에서는 Next.js App Router를 활용해 정적 사이트를 처음부터 끝까지 구축하는 방법을 다룹니다.
왜 정적 사이트에 Next.js를 선택하는가
정적 사이트 생성기는 Hugo, Gatsby, Astro 등 다양한 선택지가 있습니다. 그 가운데 Next.js를 선택하는 이유는 명확합니다.
첫째, React 생태계를 그대로 활용할 수 있습니다. 컴포넌트 기반 개발, 풍부한 라이브러리, TypeScript 지원 등 React의 모든 장점을 누릴 수 있습니다. 둘째, 프로젝트가 성장하여 동적 기능이 필요해지더라도 프레임워크를 바꿀 필요 없이 서버 컴포넌트, API 라우트 등을 점진적으로 추가할 수 있습니다. 셋째, Vercel을 통한 배포가 매우 간편하고 무료 티어도 충분히 넉넉합니다.
App Router 기초 설정
Next.js 15 이상에서는 App Router가 기본입니다. 프로젝트를 시작해 봅시다.
npx create-next-app@latest my-static-site --typescript --tailwind --app
App Router의 핵심은 파일 시스템 기반 라우팅입니다. app/ 디렉토리 안에 폴더를 만들고 page.tsx를 배치하면 해당 경로가 자동으로 라우트가 됩니다.
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── layout.tsx → 공통 레이아웃
layout.tsx는 해당 경로와 하위 경로에 공통으로 적용되는 레이아웃을 정의합니다. 네비게이션, 푸터 같은 공통 UI를 한 곳에서 관리할 수 있어 유지보수가 편리합니다.
generateStaticParams로 동적 경로 정적 생성
블로그처럼 동적 경로가 필요한 경우, generateStaticParams 함수로 빌드 시점에 모든 경로를 미리 생성할 수 있습니다.
// app/blog/[slug]/page.tsx
import { getAllPosts, getPostBySlug } from '@/lib/posts';
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
이 함수가 반환하는 모든 slug에 대해 Next.js가 빌드 시 HTML 파일을 미리 생성합니다. 빌드 결과물은 완전한 정적 파일이므로 CDN에서 직접 제공할 수 있습니다.
Metadata API로 SEO 최적화
App Router의 Metadata API는 페이지별 메타 정보를 타입 안전하게 관리할 수 있게 해줍니다.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: 'article',
publishedTime: post.date,
images: [{ url: post.ogImage }],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.description,
},
};
}
정적 메타데이터와 동적 메타데이터를 모두 지원하며, Open Graph와 Twitter 카드까지 한 곳에서 정의할 수 있어 매우 편리합니다.
정적 내보내기 설정
next.config.js에서 정적 내보내기를 활성화합니다.
// next.config.js
const nextConfig = {
output: 'export',
images: {
unoptimized: true, // 정적 내보내기에서는 이미지 최적화 비활성화
},
trailingSlash: true, // 정적 호스팅 호환성
};
module.exports = nextConfig;
output: 'export'를 설정하면 next build 실행 시 out/ 디렉토리에 완전한 정적 파일이 생성됩니다. 주의할 점은 정적 내보내기 모드에서는 서버 사이드 기능(API 라우트, 서버 액션, 미들웨어 등)을 사용할 수 없다는 것입니다.
이미지 최적화의 경우, Next.js의 내장 이미지 최적화는 서버가 필요하므로 정적 모드에서는 unoptimized: true로 설정해야 합니다. 대신 빌드 파이프라인에서 sharp 같은 도구로 이미지를 미리 최적화하거나, 외부 이미지 CDN 서비스를 활용할 수 있습니다.
이미지와 에셋 관리
정적 사이트에서 이미지 최적화는 직접 처리해야 합니다. 저는 빌드 스크립트에서 sharp를 사용해 이미지를 여러 크기와 포맷으로 미리 변환합니다.
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/images/hero.webp"
alt="히어로 이미지"
width={1200}
height={600}
priority // LCP 대상 이미지에 사용
/>
);
}
priority 속성은 LCP에 해당하는 이미지에 설정하여 preload를 적용합니다. 나머지 이미지는 자동으로 레이지 로딩됩니다.
다국어 설정 (i18n)
정적 사이트에서 다국어를 지원하려면 경로 기반 방식이 가장 깔끔합니다.
app/
├── [locale]/
│ ├── page.tsx
│ ├── about/
│ │ └── page.tsx
│ └── layout.tsx
generateStaticParams에서 지원할 로케일 목록을 반환하면, 각 언어별로 정적 페이지가 생성됩니다. hreflang 태그를 메타데이터에 포함시켜 검색 엔진에 다국어 페이지 관계를 알려주는 것도 잊지 마세요.
Vercel 배포
Vercel 배포는 GitHub 저장소를 연결하는 것만으로 끝납니다. 커밋을 푸시하면 자동으로 빌드와 배포가 진행되고, 프리뷰 배포로 PR마다 별도 URL이 생성되어 변경 사항을 미리 확인할 수 있습니다.
정적 내보내기를 사용하면 Vercel 외에도 Netlify, GitHub Pages, Cloudflare Pages 등 어떤 정적 호스팅 서비스에서든 배포할 수 있다는 장점이 있습니다.
마무리
Next.js의 정적 내보내기 기능은 React 개발자에게 가장 자연스러운 정적 사이트 구축 방법입니다. App Router의 파일 기반 라우팅, Metadata API, generateStaticParams를 조합하면 SEO에 최적화된 빠른 정적 사이트를 효율적으로 만들 수 있습니다. 프로젝트가 커지더라도 서버 기능을 점진적으로 추가할 수 있으므로, 처음부터 확장 가능한 구조를 갖추게 되는 셈입니다.