Next.js 14부터 Server Actions를 사용하면 별도의 API Route를 만들지 않아도 서버에서 실행되는 함수를 클라이언트에서 직접 호출할 수 있다. 폼 제출, 데이터 변경(mutation) 같은 작업을 훨씬 적은 코드로 처리할 수 있다는 점이 핵심이다.
Server Action을 만들려면 함수 최상단 또는 파일 최상단에 'use server' 지시어를 선언한다.
// app/actions/post.ts
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const body = formData.get('body') as string
await db.post.create({ data: { title, body } })
revalidatePath('/posts')
}
'use server' — 해당 함수가 서버에서만 실행됨을 Next.js에게 알리는 React 지시어. 클라이언트 번들에 포함되지 않는다.
파일 단위로 선언하면 해당 파일의 모든 export 함수가 Server Action이 된다. 함수 단위로 선언하면 같은 파일 안에서 Server Action과 일반 함수를 혼용할 수 있다.
HTML <form>의 action prop에 Server Action 함수를 직접 넘길 수 있다.
// app/posts/new/page.tsx
import { createPost } from '@/actions/post'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="제목" required />
<textarea name="body" placeholder="내용" required />
<button type="submit">저장</button>
</form>
)
}
JavaScript가 비활성화된 환경에서도 폼이 동작한다. 브라우저가 기본 폼 제출을 수행하고, 서버가 Server Action을 실행한다.
'use client' 컴포넌트에서도 Server Action을 import해 이벤트 핸들러처럼 쓸 수 있다.
'use client'
import { createPost } from '@/actions/post'
import { useTransition } from 'react'
export function PostForm() {
const [isPending, startTransition] = useTransition()
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
startTransition(() => createPost(formData))
}
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<button disabled={isPending}>
{isPending ? '저장 중...' : '저장'}
</button>
</form>
)
}
useTransition — 상태 업데이트를 비긴급(non-urgent)으로 표시해 UI가 블로킹되지 않게 한다. isPending으로 로딩 상태를 추적할 수 있다.
Server Action 안에서 revalidatePath 또는 **revalidateTag**를 호출하면 Next.js 캐시를 즉시 무효화한다.
| 함수 | 대상 | 사용 예 |
|---|---|---|
revalidatePath('/posts') | 특정 경로의 캐시 | 목록 페이지 갱신 |
revalidatePath('/posts/[id]', 'page') | 동적 경로 | 상세 페이지 갱신 |
revalidateTag('posts') | 태그 기반 캐시 | fetch 시 태그를 지정한 경우 |
// 태그 기반 fetch
const res = await fetch('/api/posts', { next: { tags: ['posts'] } })
// Action에서 태그 무효화
revalidateTag('posts')
클라이언트 서버
│ │
│─── form submit / 함수 호출 ──>│
│ │ Server Action 실행
│ │ DB 변경 / revalidate
│<── 리다이렉트 or 응답 ─────────│
│ │
API Route(/api/*)를 별도로 만들 필요 없이 서버 로직이 하나의 함수로 응집된다. mutation 중심의 기능(폼 제출, 삭제, 수정)에 Server Actions를 적용하면 코드량과 네트워크 왕복이 모두 줄어든다.