Next.jsNext.js 기초강의 8개 중 4번째난이도 기초

Server Actions와 API Routes — 서버 로직 처리

발행 읽는 시간 약 1 분

  • Next.js
  • ServerActions
  • APIRoutes
  • route.ts
  • 폼처리

강의 소개

Server Actions로 폼 제출과 데이터 변이를 처리하고, API Routes로 REST API를 만드는 방법을 배웁니다. 두 방식의 차이와 선택 기준도 이해합니다.

Server Actions#

서버에서 실행되는 함수를 클라이언트에서 직접 호출합니다.

TSX
// 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 사용#

TSX
// 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로 응답 처리#

TSX
"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를 만듭니다.

TypeScript
// 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 });
}
TypeScript
// 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 });
}

미들웨어#

모든 요청에 앞서 실행됩니다.

TypeScript
// 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 ActionsAPI Routes
호출 방식폼/클라이언트 직접HTTP 요청
외부 접근불가가능
타입 안전성TypeScript 자동수동
적합한 경우폼 제출, 뮤테이션공개 API, 외부 연동

정리#

  • Server Actions: 폼 처리, 데이터 변경 → Next.js 전용, 타입 안전
  • API Routes: REST API → 외부 서비스, 모바일 앱 연동에 적합
  • 미들웨어: 인증, 로깅, 헤더 조작 등 전처리

다음 편에서는 이미지와 폰트 최적화 — next/image와 next/font로 성능을 높이는 방법을 배웁니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.