Server Actions
서버에서 실행되는 함수를 클라이언트에서 직접 호출합니다.
// app/actions/user.ts
"use server"; // 서버에서만 실행
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
// 서버에서 직접 DB 접근
await db.users.create({ name, email });
// 캐시 무효화 후 페이지 재검증
revalidatePath("/users");
}
export async function deleteUser(id: string) {
await db.users.delete(id);
revalidatePath("/users");
}
폼에서 Server Action 사용
// app/users/new/page.tsx
import { createUser } from "@/actions/user";
export default function NewUserPage() {
return (
<form action={createUser}>
<input name="name" required placeholder="이름" />
<input name="email" type="email" required placeholder="이메일" />
<button type="submit">생성</button>
</form>
);
}
useFormState로 응답 처리
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { createUser } from "@/actions/user";
interface State {
message: string | null;
errors: Record<string, string[]>;
}
const initialState: State = { message: null, errors: {} };
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "저장 중..." : "저장"}</button>;
}
function UserForm() {
const [state, formAction] = useFormState(createUser, initialState);
return (
<form action={formAction}>
<input name="name" />
{state.errors.name && <p>{state.errors.name}</p>}
<input name="email" type="email" />
{state.errors.email && <p>{state.errors.email}</p>}
<SubmitButton />
{state.message && <p>{state.message}</p>}
</form>
);
}
API Routes
외부에서 호출 가능한 REST API를 만듭니다.
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
// GET /api/users
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page") ?? "1");
const users = await db.users.findMany({
skip: (page - 1) * 20,
take: 20,
});
return NextResponse.json({ users, page });
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
// 유효성 검사
if (!body.name || !body.email) {
return NextResponse.json(
{ error: "name과 email은 필수입니다." },
{ status: 400 }
);
}
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts
interface Params {
params: { id: string };
}
export async function GET(request: NextRequest, { params }: Params) {
const user = await db.users.findById(params.id);
if (!user) return NextResponse.json({ error: "없음" }, { status: 404 });
return NextResponse.json(user);
}
export async function DELETE(request: NextRequest, { params }: Params) {
await db.users.delete(params.id);
return new NextResponse(null, { status: 204 });
}
미들웨어
모든 요청에 앞서 실행됩니다.
// middleware.ts (루트에 위치)
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token")?.value;
const pathname = request.nextUrl.pathname;
// 인증 보호 경로
if (pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// 요청 헤더 추가
const headers = new Headers(request.headers);
headers.set("x-pathname", pathname);
return NextResponse.next({ request: { headers } });
}
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};
Server Actions vs API Routes
| 항목 | Server Actions | API Routes |
|---|---|---|
| 호출 방식 | 폼/클라이언트 직접 | HTTP 요청 |
| 외부 접근 | 불가 | 가능 |
| 타입 안전성 | TypeScript 자동 | 수동 |
| 적합한 경우 | 폼 제출, 뮤테이션 | 공개 API, 외부 연동 |
정리
- Server Actions: 폼 처리, 데이터 변경 → Next.js 전용, 타입 안전
- API Routes: REST API → 외부 서비스, 모바일 앱 연동에 적합
- 미들웨어: 인증, 로깅, 헤더 조작 등 전처리
다음 편에서는 이미지와 폰트 최적화 — next/image와 next/font로 성능을 높이는 방법을 배웁니다.