iBetter Books
수정

Ch 06. Server Actions vs Route Handlers — 무엇을 쓸까

같은 듯 다른 둘

Server Actions와 Route Handlers는 둘 다 서버에서 실행되는 코드입니다. 데이터베이스에 접근하고, 환경변수를 사용하고, 클라이언트에서 호출됩니다. 그런데 왜 두 가지가 있을까요.

설계 의도가 다르기 때문입니다. Server Actions는 Next.js 앱 내부에서 사용하도록 설계되었습니다. Route Handlers는 외부에서 접근할 수 있는 HTTP API를 만들기 위해 설계되었습니다.

비교 표

항목 Server Actions Route Handlers
호출 방법 함수 직접 호출, form action HTTP 요청 (GET, POST 등)
반환 형태 직렬화 가능한 값 NextResponse (JSON, HTML 등)
외부 접근 불가 (Next.js 앱 내부 전용) 가능 (URL로 직접 접근)
TypeScript 타입 함수 인자/반환 타입 그대로 요청/응답 파싱 필요
폼 제출 자연스러움 (action prop) fetch + event.preventDefault() 필요
캐시 제어 revalidateTag, revalidatePath HTTP 캐시 헤더
인증 세션/쿠키 직접 접근 헤더/쿠키 직접 파싱

Server Actions — Next.js 앱 내부 폼/뮤테이션

게시글 작성, 댓글 달기, 좋아요 누르기, 설정 변경처럼 Next.js 앱 내부에서 발생하는 데이터 변경 작업에는 Server Actions를 씁니다.

// 파일: app/posts/[slug]/components/CommentForm.tsx
'use client';

import { useActionState } from 'react';
import { addComment } from '@/actions/comment';

const initialState = { error: '', success: false };

export default function CommentForm({ postId }: { postId: number }) {
  const [state, formAction, isPending] = useActionState(
    addComment.bind(null, postId),
    initialState
  );

  return (
    <form action={formAction}>
      <textarea name="content" placeholder="댓글을 입력하세요" required />
      <button type="submit" disabled={isPending}>
        {isPending ? '작성 중...' : '댓글 달기'}
      </button>
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
    </form>
  );
}
```text

### Route Handlers — 외부 클라이언트, 모바일 앱, 웹훅

Next.js 앱 밖에서 접근해야 한다면 Route Handlers를 씁니다.

```ts
// 파일: app/api/posts/route.ts
// 모바일 앱이나 외부 서비스가 호출하는 API
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const posts = await fetchPostsFromDB();
  return NextResponse.json(posts);
}
```text

모바일 앱에서는 `https://myapp.com/api/posts`를 직접 호출합니다. Server Actions는 이런 방식으로 외부에서 호출할 수 없습니다.

### 실무에서 함께 쓰는 패턴

Next.js 앱을 만들 때 두 가지를 함께 쓰는 경우가 많습니다.

**웹 앱의 폼 → Server Actions**

```tsx
// 웹 앱 내부의 게시글 작성 폼
<form action={createPost}>
  <input name="title" />
  <button type="submit">작성</button>
</form>
```text

**모바일 앱 또는 외부 서비스 → Route Handlers**

```ts
// 모바일 앱에서 호출
const response = await fetch('https://api.myapp.com/api/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${token}`,
  },
  body: JSON.stringify({ title, content }),
});
```text

**같은 비즈니스 로직을 공유하는 방법**

```ts
// 파일: lib/posts.ts
// 핵심 비즈니스 로직은 별도 파일에
export async function createPostInDB(title: string, content: string) {
  // 데이터베이스 저장 로직
  return { id: 1, title, content };
}
```text

```ts
// 파일: app/actions/post.ts
// Server Action에서 사용
'use server';
import { createPostInDB } from '@/lib/posts';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  return createPostInDB(title, content);
}
```text

```ts
// 파일: app/api/posts/route.ts
// Route Handler에서 동일한 로직 사용
import { createPostInDB } from '@/lib/posts';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { title, content } = await request.json();
  const post = await createPostInDB(title, content);
  return NextResponse.json(post, { status: 201 });
}

핵심 로직을 lib/ 폴더에 분리하면, Server Actions와 Route Handlers 모두에서 같은 코드를 재사용할 수 있습니다.

다음 챕터에서는

다음 챕터에서는 실제 데이터베이스와 연결하는 방법을 배웁니다. Prisma ORM으로 스키마를 정의하고, 마이그레이션을 실행하고, Server Component에서 데이터를 조회합니다.