Next.js 기초 완벽 가이드 - 2025년 최신 버전으로 시작하기
Next.js 15의 최신 기능으로 현대적인 웹 애플리케이션을 구축하는 완벽한 가이드입니다. App Router, React Server Components, Turbopack을 활용한 빠른 개발부터 프로덕션 배포까지, 실전 예제와 함께 단계별로 학습하세요.
📋 핵심 내용 요약
- 최신 버전: Next.js 15 + React 19로 최첨단 웹 개발
- App Router: 파일 기반 라우팅과 Server Components로 최적의 성능
- Turbopack: Rust 기반의 초고속 번들러로 개발 속도 향상
- 완벽한 풀스택: 프론트엔드와 백엔드를 하나의 프레임워크로
- 프로덕션 준비: Vercel, AWS, Docker 등 어디든 배포 가능
🤖 AI 요약
Next.js는 React 기반의 풀스택 웹 프레임워크로, Vercel이 개발하고 있으며 현대적인 웹 애플리케이션 개발의 표준이 되었습니다.
왜 Next.js인가? Next.js 15는 React 19 지원, Turbopack Dev의 안정화, 그리고 혁신적인 캐싱 전략을 도입했습니다. Turbopack은 Webpack보다 최대 96.3% 빠른 Hot Module Replacement를 제공하며, 대규모 프로젝트에서도 즉각적인 피드백을 받을 수 있습니다.
App Router와 Server Components: Next.js 13부터 도입된 App Router는 React Server Components(RSC)를 활용하여 서버에서 렌더링되는 컴포넌트를 기본으로 제공합니다. 이를 통해 JavaScript 번들 크기를 줄이고 초기 페이지 로딩 속도를 크게 개선할 수 있습니다. 클라이언트 컴포넌트가 필요한 경우에만 ‘use client’ 지시문을 사용하면 됩니다.
파일 기반 라우팅: app/ 디렉토리의 폴더 구조가 곧 라우트입니다. page.tsx는 페이지를, layout.tsx는 공통 레이아웃을, loading.tsx는 로딩 상태를 정의합니다. 동적 라우트는 [id] 폴더로 간단하게 만들 수 있습니다.
데이터 페칭:
Server Components에서는 직접 async/await로 데이터를 가져올 수 있습니다. 데이터베이스 쿼리, API 호출을 컴포넌트 내에서 바로 수행하며, 이 코드는 클라이언트에 전송되지 않습니다. 자동 중복 제거와 캐싱으로 최적의 성능을 제공합니다.
최신 기능 (2025): Next.js 15.5는 프로덕션 Turbopack 빌드(베타), TypeScript 타입 라우트, Node.js 미들웨어 안정화를 포함합니다. 빌드 시간이 2배에서 5배까지 개선되었으며, 타입 안전성이 크게 향상되었습니다.
배포: Vercel에 원클릭 배포는 물론, AWS, Google Cloud, Docker 컨테이너, 심지어 정적 사이트로도 내보낼 수 있습니다. 엣지 런타임을 활용하면 전 세계 어디서나 빠른 응답 속도를 보장합니다.
Next.js는 React로 웹 애플리케이션을 만드는 가장 현대적이고 효율적인 방법입니다. 이 글에서는 Next.js 15의 최신 기능을 활용하여 기초부터 실전까지 완벽하게 배워보겠습니다.
Next.js란?
Next.js는 Vercel이 개발한 React 기반의 풀스택 프레임워크로, 다음과 같은 강력한 기능을 제공합니다:
- 📁 App Router: 직관적인 파일 기반 라우팅 시스템
- ⚡ Server Components: 서버에서 렌더링되는 React 컴포넌트
- 🚀 Turbopack: Rust 기반의 초고속 번들러
- 🎯 자동 최적화: 이미지, 폰트, 코드 스플리팅 자동화
- 🔌 API Routes: 프론트엔드와 백엔드를 하나로
- 🌍 국제화: 다국어 지원 내장
- 📊 분석: 실시간 성능 모니터링
왜 Next.js를 선택해야 할까?
1. 최첨단 성능
Turbopack Dev (Next.js 15):
- 로컬 서버 시작: 76.8% 더 빠름
- Fast Refresh: 96.3% 더 빠름
- 초기 라우트 컴파일: 최대 45.8% 더 빠름
React Server Components:
- 서버에서 렌더링되어 JavaScript 번들 크기 0
- 데이터베이스 직접 접근으로 API 레이어 불필요
- 자동 코드 분할로 필요한 것만 로드
2. 개발 경험
// 데이터 페칭이 이렇게 간단합니다!
async function BlogPost({ params }) {
const post = await db.posts.findOne({ id: params.id });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
} 컴포넌트가 곧 데이터 페칭입니다. 별도의 getServerSideProps나 useEffect 없이 직접 데이터를 가져올 수 있습니다.
3. 프로덕션 준비
- 자동 최적화: 이미지, 폰트, 스크립트 자동 최적화
- SEO 친화적: 서버 사이드 렌더링으로 완벽한 SEO
- 보안: API 키와 민감한 로직을 서버에서만 실행
- 확장성: 엣지 런타임으로 전 세계 빠른 응답
시작하기
사전 준비사항
- Node.js 18.18 이상 (LTS 버전 권장)
- 코드 에디터 (VS Code + ESLint, Prettier 권장)
- 기본 지식: JavaScript, React, TypeScript
새 프로젝트 생성
Next.js 프로젝트를 만드는 가장 빠른 방법:
# 프로젝트 생성
npx create-next-app@latest my-app
# 프로젝트 디렉토리로 이동
cd my-app
# 개발 서버 시작
npm run dev 설치 중 다음을 선택하게 됩니다:
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a src/ directory? … Yes
✔ Would you like to use App Router? … Yes
✔ Would you like to use Turbopack for next dev? … Yes
✔ Would you like to customize the import alias? … No 권장 설정:
- TypeScript: ✅ Yes (타입 안전성)
- ESLint: ✅ Yes (코드 품질)
- Tailwind CSS: ✅ Yes (빠른 스타일링)
- App Router: ✅ Yes (최신 기능)
- Turbopack: ✅ Yes (빠른 개발)
프로젝트 구조
생성된 프로젝트의 구조를 이해해봅시다:
my-app/
├── src/
│ └── app/
│ ├── layout.tsx # 루트 레이아웃 (모든 페이지 공통)
│ ├── page.tsx # 홈페이지 (/)
│ ├── globals.css # 전역 스타일
│ └── favicon.ico # 파비콘
├── public/ # 정적 파일
│ ├── images/
│ └── fonts/
├── package.json # 프로젝트 설정 및 의존성
├── tsconfig.json # TypeScript 설정
├── next.config.ts # Next.js 설정
└── tailwind.config.ts # Tailwind CSS 설정 주요 디렉토리 설명
src/app/
- 모든 페이지와 라우트가 위치
- 폴더 구조 = URL 구조
- 특수 파일들로 다양한 기능 구현
public/
- 이미지, 폰트 등 정적 파일
/경로로 직접 접근 가능- 빌드 시 그대로 복사됨
App Router 완벽 가이드
특수 파일 규칙
Next.js App Router는 특수한 파일 이름으로 다양한 기능을 제공합니다:
| 파일 | 용도 | 설명 |
|---|---|---|
layout.tsx | 레이아웃 | 여러 페이지에서 공유되는 UI |
page.tsx | 페이지 | 라우트의 고유 UI |
loading.tsx | 로딩 UI | Suspense 기반 로딩 상태 |
error.tsx | 에러 UI | 에러 처리 화면 |
not-found.tsx | 404 페이지 | 찾을 수 없음 에러 |
route.ts | API 엔드포인트 | HTTP 메서드 핸들러 |
template.tsx | 템플릿 | 재마운트되는 레이아웃 |
기본 라우팅
파일 시스템이 곧 라우팅입니다:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── dashboard/
├── layout.tsx → 대시보드 공통 레이아웃
├── page.tsx → /dashboard
├── settings/
│ └── page.tsx → /dashboard/settings
└── analytics/
└── page.tsx → /dashboard/analytics 동적 라우트
기본 동적 라우트:
// app/blog/[slug]/page.tsx
interface PageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default async function BlogPost({ params }: PageProps) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// 정적 경로 생성 (SSG)
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// 메타데이터 생성
export async function generateMetadata({ params }: PageProps) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
} Catch-all 라우트:
// app/docs/[...slug]/page.tsx
// /docs/a → params.slug = ['a']
// /docs/a/b → params.slug = ['a', 'b']
// /docs/a/b/c → params.slug = ['a', 'b', 'c']
export default function DocsPage({ params }: { params: { slug: string[] } }) {
return <div>문서: {params.slug.join(' / ')}</div>;
} 레이아웃 시스템
루트 레이아웃 (필수):
// app/layout.tsx
import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'My Next.js App',
description: 'Built with Next.js 15',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko" className={inter.className}>
<body>
<header>
<nav>{/* 네비게이션 */}</nav>
</header>
<main>{children}</main>
<footer>{/* 푸터 */}</footer>
</body>
</html>
);
} 중첩 레이아웃:
// app/dashboard/layout.tsx
import { Sidebar } from '@/components/Sidebar';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 overflow-auto p-8">
{children}
</div>
</div>
);
} 로딩과 에러 처리
로딩 상태:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
</div>
);
} 에러 처리:
// app/dashboard/error.tsx
'use client'; // Error components는 Client Component여야 합니다
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-2xl font-bold mb-4">문제가 발생했습니다!</h2>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
다시 시도
</button>
</div>
);
} Server Components vs Client Components
Next.js 15의 가장 중요한 개념입니다.
Server Components (기본)
서버에서만 실행되는 컴포넌트입니다:
// app/posts/page.tsx (Server Component)
import { db } from '@/lib/db';
export default async function PostsPage() {
// 데이터베이스 직접 접근 (서버에서만 실행됨)
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
});
return (
<div>
<h1>블로그 글 목록</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={'/posts/' + post.slug}>{post.title}</a>
</li>
))}
</ul>
</div>
);
} 장점:
- ✅ 데이터베이스 직접 접근
- ✅ API 키 안전하게 사용
- ✅ JavaScript 번들 크기 0
- ✅ 초기 페이지 로딩 빠름
- ✅ SEO 최적화
Client Components
브라우저에서 실행되는 인터랙티브 컴포넌트입니다:
// components/counter.tsx
'use client'; // 이 지시문이 Client Component를 만듭니다
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>
증가
</button>
</div>
);
} 언제 Client Component를 사용할까:
- ✅ 상태 관리 (
useState,useReducer) - ✅ 이벤트 핸들러 (
onClick,onChange) - ✅ 브라우저 API (
localStorage,window) - ✅ React Hooks (
useEffect,useContext) - ✅ 인터랙티브 UI 라이브러리
베스트 프랙티스
❌ 나쁜 예:
'use client'; // 전체 페이지를 Client Component로 만들면 성능 저하
export default function Page() {
return (
<div>
<Header />
<Content />
<Footer />
</div>
);
} ✅ 좋은 예:
// Server Component (기본)
import { Counter } from './counter'; // Client Component
export default function Page() {
return (
<div>
<Header />
<Content />
{/* 필요한 부분만 Client Component */}
<Counter />
<Footer />
</div>
);
} 데이터 페칭
Next.js 15의 데이터 페칭은 혁신적으로 간단합니다.
Server Components에서
// app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
// 캐싱 옵션
next: { revalidate: 3600 } // 1시간마다 재검증
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
{products.map((product) => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
))}
</div>
);
} 캐싱 전략
Next.js 15는 캐싱 기본값을 변경했습니다:
// 캐시하지 않음 (기본값 - Next.js 15)
fetch('https://api.example.com/data');
// 재검증 시간 설정
fetch('https://api.example.com/data', {
next: { revalidate: 60 } // 60초
});
// 무기한 캐시
fetch('https://api.example.com/data', {
cache: 'force-cache'
});
// 캐시하지 않음 (명시적)
fetch('https://api.example.com/data', {
cache: 'no-store'
}); 병렬 데이터 페칭
export default async function Page() {
// 병렬로 실행 - 더 빠름!
const [products, categories, reviews] = await Promise.all([
getProducts(),
getCategories(),
getReviews(),
]);
return (
<div>
<Products data={products} />
<Categories data={categories} />
<Reviews data={reviews} />
</div>
);
} 순차적 데이터 페칭
export default async function Page({ params }) {
// 1. 먼저 사용자 정보 가져오기
const user = await getUser(params.id);
// 2. 사용자 ID로 게시물 가져오기
const posts = await getUserPosts(user.id);
return (
<div>
<UserProfile user={user} />
<UserPosts posts={posts} />
</div>
);
} API Routes
Next.js에서 백엔드 API를 만드는 것도 매우 간단합니다.
GET 요청
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function GET() {
try {
const posts = await db.post.findMany();
return NextResponse.json(posts);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
} POST 요청
// app/api/posts/route.ts
export async function POST(request: Request) {
try {
const body = await request.json();
const { title, content } = body;
const post = await db.post.create({
data: { title, content },
});
return NextResponse.json(post, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
} 동적 라우트 API
// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db.post.findUnique({
where: { id: params.id },
});
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json(post);
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const post = await db.post.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.post.delete({
where: { id: params.id },
});
return NextResponse.json({ message: 'Deleted successfully' });
} 스타일링
Tailwind CSS (권장)
Next.js는 Tailwind CSS를 완벽하게 지원합니다:
export default function Button({ children }: { children: React.ReactNode }) {
return (
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors">
{children}
</button>
);
} CSS Modules
컴포넌트 스코프 CSS:
// Button.module.css
.button {
background-color: blue;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.button:hover {
background-color: darkblue;
} // Button.tsx
import styles from './Button.module.css';
export default function Button({ children }) {
return <button className={styles.button}>{children}</button>;
} Global Styles
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1 {
@apply text-4xl font-bold;
}
h2 {
@apply text-3xl font-semibold;
}
}
@layer components {
.btn-primary {
@apply bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600;
}
} 이미지 최적화
Next.js의 Image 컴포넌트는 자동으로 이미지를 최적화합니다:
import Image from 'next/image';
export default function Profile() {
return (
<div>
{/* 로컬 이미지 */}
<Image
src="/profile.jpg"
alt="프로필 사진"
width={500}
height={500}
priority // LCP 이미지는 priority 설정
/>
{/* 외부 이미지 */}
<Image
src="https://example.com/photo.jpg"
alt="사진"
width={800}
height={600}
placeholder="blur" // 블러 플레이스홀더
blurDataURL="data:image/jpeg..." // 또는 자동 생성
/>
</div>
);
} 자동 최적화:
- ✅ WebP/AVIF 자동 변환
- ✅ 반응형 이미지 생성
- ✅ Lazy loading 기본 적용
- ✅ CLS 방지 (누적 레이아웃 이동)
폰트 최적화
// app/layout.tsx
import { Inter, Noto_Sans_KR } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
const notoSansKr = Noto_Sans_KR({
subsets: ['korean'],
weight: ['400', '700'],
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="ko" className={inter.className + ' ' + notoSansKr.className}>
<body>{children}</body>
</html>
);
} 메타데이터와 SEO
정적 메타데이터
// app/about/page.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: '회사 소개',
description: '우리 회사를 소개합니다',
openGraph: {
title: '회사 소개',
description: '우리 회사를 소개합니다',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: '회사 소개',
description: '우리 회사를 소개합니다',
images: ['/twitter-image.jpg'],
},
};
export default function AboutPage() {
return <div>회사 소개 페이지</div>;
} 동적 메타데이터
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.date,
authors: [post.author],
},
};
} 환경 변수
# .env.local
DATABASE_URL="postgresql://..."
API_KEY="your-secret-key"
# 클라이언트에서 접근 가능 (NEXT_PUBLIC_ 접두사)
NEXT_PUBLIC_API_URL="https://api.example.com" // Server Component (안전)
export default async function Page() {
const apiKey = process.env.API_KEY; // 서버에서만 접근 가능
// ...
}
// Client Component
'use client';
export default function ClientPage() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // 클라이언트에서 접근 가능
// ...
} 배포
Vercel (가장 쉬운 방법)
- GitHub에 코드 푸시
- Vercel에 로그인
- 프로젝트 임포트
- 배포 완료!
자동으로 제공되는 것들:
- ✅ HTTPS
- ✅ 글로벌 CDN
- ✅ 자동 스케일링
- ✅ 프리뷰 배포
- ✅ 분석 및 모니터링
Docker
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"] // next.config.ts
module.exports = {
output: 'standalone',
}; # 빌드 및 실행
docker build -t my-nextjs-app .
docker run -p 3000:3000 my-nextjs-app 정적 사이트 내보내기
// next.config.ts
module.exports = {
output: 'export',
}; npm run build
# out/ 폴더가 생성됩니다 성능 최적화 팁
1. Code Splitting
자동으로 적용되지만, 필요시 수동 제어:
import dynamic from 'next/dynamic';
// 클라이언트에서만 로드
const DynamicComponent = dynamic(() => import('@/components/Heavy'), {
ssr: false,
loading: () => <p>로딩 중...</p>,
}); 2. Streaming과 Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>대시보드</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<FastComponent />
</div>
);
} 3. 부분 프리렌더링 (Partial Prerendering)
Next.js 15의 실험적 기능:
// next.config.ts
module.exports = {
experimental: {
ppr: true,
},
}; 4. 데이터 캐싱
import { unstable_cache } from 'next/cache';
const getCachedData = unstable_cache(
async () => {
return await db.data.findMany();
},
['data-cache'],
{ revalidate: 3600 }
); 디버깅
React DevTools
브라우저 확장 프로그램 설치:
Next.js DevTools
개발 모드에서 자동으로 활성화됩니다.
환경별 코드
if (process.env.NODE_ENV === 'development') {
console.log('개발 환경에서만 실행');
}
if (process.env.NODE_ENV === 'production') {
console.log('프로덕션 환경에서만 실행');
} 실전 예제: 블로그 만들기
완전한 블로그를 만들어봅시다:
1. 프로젝트 구조
src/
└── app/
├── layout.tsx
├── page.tsx
├── blog/
│ ├── page.tsx
│ └── [slug]/
│ └── page.tsx
└── api/
└── posts/
└── route.ts 2. 블로그 목록 페이지
// app/blog/page.tsx
import Link from 'next/link';
import { getAllPosts } from '@/lib/posts';
export default async function BlogPage() {
const posts = await getAllPosts();
return (
<div className="max-w-4xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">블로그</h1>
<div className="grid gap-8">
{posts.map((post) => (
<article key={post.slug} className="border-b pb-8">
<Link href={'/blog/' + post.slug}>
<h2 className="text-2xl font-bold mb-2 hover:text-blue-600">
{post.title}
</h2>
</Link>
<time className="text-gray-600">{post.date}</time>
<p className="mt-4 text-gray-700">{post.excerpt}</p>
<Link
href={'/blog/' + post.slug}
className="text-blue-600 hover:underline mt-2 inline-block"
>
더 읽기 →
</Link>
</article>
))}
</div>
</div>
);
} 3. 블로그 상세 페이지
// app/blog/[slug]/page.tsx
import { getPost, getAllPosts } from '@/lib/posts';
import { notFound } from 'next/navigation';
import { Metadata } from 'next';
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) return {};
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article className="max-w-4xl mx-auto px-4 py-12">
<header className="mb-8">
<h1 className="text-5xl font-bold mb-4">{post.title}</h1>
<time className="text-gray-600">{post.date}</time>
</header>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
} 4. 데이터 레이어
// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';
const postsDirectory = path.join(process.cwd(), 'content/posts');
export async function getAllPosts() {
const fileNames = fs.readdirSync(postsDirectory);
const posts = fileNames.map((fileName) => {
const slug = fileName.replace(/.md$/, '');
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContents);
return {
slug,
title: data.title,
date: data.date,
excerpt: data.excerpt,
};
});
return posts.sort((a, b) => (a.date < b.date ? 1 : -1));
}
export async function getPost(slug: string) {
try {
const fullPath = path.join(postsDirectory, slug + '.md');
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const htmlContent = marked(content);
return {
slug,
title: data.title,
date: data.date,
excerpt: data.excerpt,
content: htmlContent,
};
} catch {
return null;
}
} 마무리
Next.js 15는 현대적인 웹 개발의 모든 것을 제공하는 강력한 프레임워크입니다.
핵심 요점:
- ⚡ Turbopack: 초고속 개발 경험
- 🎯 App Router: 직관적인 파일 기반 라우팅
- 🚀 Server Components: 최적의 성능
- 🔧 풀스택: 프론트엔드 + 백엔드
- 🌍 배포 자유: 어디든 배포 가능
다음 단계:
- 작은 프로젝트로 시작하세요
- 공식 문서를 참고하세요
- 커뮤니티에 참여하세요
- 실전 프로젝트를 만들어보세요
더 알아보기
- 📚 Next.js 공식 문서
- 🎓 Next.js Learn
- 💻 Next.js GitHub
- 📺 Vercel YouTube
- 💬 Next.js Discord
- 🐦 Next.js Twitter
추천 라이브러리:
- 🎨 Tailwind CSS - 유틸리티 CSS
- 🎭 Framer Motion - 애니메이션
- 📝 React Hook Form - 폼 관리
- 🔍 Zod - 스키마 검증
- 🗄️ Prisma - ORM
- 🔐 NextAuth.js - 인증
Next.js로 멋진 웹 애플리케이션을 만들어보세요!
Happy coding! 🚀
질문이나 피드백이 있으신가요?
이 튜토리얼에 대한 질문이나 제안사항이 있다면 언제든 연락주세요!
📧 이메일: [email protected]