Next.js
React 기반 풀스택 프레임워크. SSR, SSG, ISR, App Router 등을 제공한다.
렌더링 방식
| 방식 | 설명 | 사용 사례 |
|---|---|---|
| SSG (Static) | 빌드 타임에 HTML 생성 | 블로그, 문서 |
| SSR (Server) | 요청마다 서버에서 렌더링 | 개인화 페이지, 실시간 데이터 |
| ISR (Incremental) | SSG + 일정 주기 재생성 | 상품 목록, 뉴스 |
| CSR (Client) | 브라우저에서 렌더링 | 대시보드, 인터랙티브 UI |
App Router 구조 (Next.js 13+)
app/
├── layout.tsx 루트 레이아웃 (전체 공통)
├── page.tsx / 페이지
├── loading.tsx 로딩 UI (Suspense)
├── error.tsx 에러 UI
├── not-found.tsx 404
├── globals.css
├── (auth)/ 라우트 그룹 (URL에 영향 없음)
│ ├── login/page.tsx
│ └── register/page.tsx
├── dashboard/
│ ├── layout.tsx /dashboard 전용 레이아웃
│ └── page.tsx
└── api/
└── users/
└── route.ts API Route
Server vs Client Component
// Server Component (기본값) — 서버에서만 실행
// DB 접근, API 키 사용 가능
// useState, useEffect, 이벤트 핸들러 사용 불가
async function UserList() {
const users = await db.query('SELECT * FROM users')
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
// Client Component — 브라우저에서 실행
// 파일 최상단에 'use client' 선언 필수
'use client'
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c+1)}>{count}</button>
}데이터 페칭
// Server Component에서 직접 fetch
async function Page() {
// SSG (기본) — 빌드 타임 캐시
const data = await fetch('https://api.example.com/data')
// SSR — 매 요청마다 새로 가져옴
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// ISR — 60초마다 재생성
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
return <div>{JSON.stringify(data)}</div>
}API Route
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const users = await db.findAll()
return NextResponse.json(users)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const user = await db.create(body)
return NextResponse.json(user, { status: 201 })
}
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.findById(Number(params.id))
if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(user)
}동적 라우팅
app/
├── users/[id]/page.tsx → /users/1
├── blog/[...slug]/page.tsx → /blog/a/b/c
└── [[...slug]]/page.tsx → / 또는 /a/b/c
// app/users/[id]/page.tsx
export default function UserPage({ params }: { params: { id: string } }) {
return <div>User {params.id}</div>
}
// 정적 경로 미리 생성 (SSG)
export async function generateStaticParams() {
const users = await fetch('/api/users').then(r => r.json())
return users.map((u: User) => ({ id: String(u.id) }))
}미들웨어
// middleware.ts (루트)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}Server Actions
// 서버에서 실행되는 함수를 Client Component에서 직접 호출
'use server'
async function createUser(formData: FormData) {
const name = formData.get('name') as string
await db.create({ name })
revalidatePath('/users') // 캐시 갱신
}
// Client Component에서 사용
'use client'
export function UserForm() {
return (
<form action={createUser}>
<input name="name" />
<button type="submit">추가</button>
</form>
)
}next.config.ts 주요 설정
const config = {
images: {
remotePatterns: [
{ hostname: 'example.com' },
],
},
redirects: async () => [
{ source: '/old', destination: '/new', permanent: true },
],
env: {
CUSTOM_VAR: 'value',
},
}
export default config환경 변수
# .env.local
DATABASE_URL=... # 서버 전용
NEXT_PUBLIC_API_URL=... # 클라이언트에서도 접근 가능 (NEXT_PUBLIC_ 접두사)