Next.js 14 App Router 완벽 가이드 — 서버 컴포넌트부터 라우팅까지
App Router 디렉토리 구조
app/
├── layout.tsx # 루트 레이아웃
├── page.tsx # / 경로
├── loading.tsx # 로딩 UI
├── error.tsx # 에러 UI
├── blog/
│ ├── page.tsx # /blog 경로
│ └── [slug]/
│ └── page.tsx # /blog/:slug 경로
└── api/
└── posts/
└── route.ts # API 라우트
서버 컴포넌트 vs 클라이언트 컴포넌트
기본적으로 모든 컴포넌트는 서버 컴포넌트입니다.
// 서버 컴포넌트 (기본) — DB 직접 조회 가능
async function PostList() {
const posts = await db.posts.findMany() // 서버에서만 실행
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)
}
// 클라이언트 컴포넌트 — 'use client' 지시어 필요
'use client'
import { useState } from 'react'
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
)
}
레이아웃
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="ko">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
)
}
동적 라우트
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
// 정적 생성 — 빌드 시 모든 경로 사전 생성
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}
로딩 및 에러 처리
// app/blog/loading.tsx — 자동으로 Suspense 적용
export default function Loading() {
return <div className="animate-pulse">로딩 중...</div>
}
// app/blog/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<p>오류가 발생했습니다: {error.message}</p>
<button onClick={reset}>다시 시도</button>
</div>
)
}
API Route Handler
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await db.posts.findMany()
return NextResponse.json(posts)
}
export async function POST(request: Request) {
const body = await request.json()
const post = await db.posts.create({ data: body })
return NextResponse.json(post, { status: 201 })
}
메타데이터 설정
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.description,
openGraph: { images: [post.ogImage] },
}
}