plogger

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] },
  }
}