NextJS에서 Server Actions는 서버에서 실행되는 비동기 함수로, 클라이언트와 서버 컴포넌트에서 데이터 변경(Mutation)과 폼 제출(Form Submission)을 효율적으로 처리할 수 있다. use server 키워드를 사용하여 간편하게 서버에서 실행되는 함수를 정의할 수 있으며, 클라이언트 컴포넌트와 서버 컴포넌트에서 모두 사용할 수 있다.
// 서버 컴포넌트에서 사용 방법
'use server'
export default function Page() {
async function create() {
'use server'
// 데이터 저장 또는 처리 로직
}
return <button onClick={create}>Create</button>
}
// 클라이언트 컴포넌트에서 사용 방법
// app/actions.ts
'use server'
export async function create() {
// 데이터 저장 또는 처리 로직
}
'use client'
import { create } from '@/app/actions'
export function Button() {
useEffect(() => {
create()
}, [])
return <button onClick={() => create()}>Create</button>
}
Form
Server Actions는 폼 처리를 매우 쉽게 만들어 준다. 폼 제출 시 자동으로 formData를 서버 액션 함수에 전달하며, 따로 useState로 상태를 관리할 필요 없다.
// app/page.tsx
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// 데이터 저장 또는 처리 로직
}
return (
<form action={createInvoice}>
<input type="text" name="customerId" />
<input type="text" name="amount" />
<input type="text" name="status" />
<button type="submit">Create Invoice</button>
</form>
)
}
Bind
Server Action에 특정 값을 전달하려면 bind()를 사용한다. 클라이언트 컴포넌트에서도 특정 인자를 전달할 수 있다.
// app/client-component.tsx
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}
// app/actions.ts (Server Action)
'use server'
export async function updateUser(userId: string, formData: FormData) {
const newName = formData.get('name')
// 데이터 저장 또는 처리 로직
}
Pending
폼 제출 시 로딩 상태를 표시하려면 useFormStatus()를 사용할 수 있다.
// app/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Processing...' : 'Submit'}
</button>
)
}
// app/page.tsx
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
export default function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
Validation and Error Handling
서버에서 데이터 유효성을 검사하고 에러 메시지를 반환할 수 있다.
// app/actions.ts
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
})
export async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
if (!validatedFields.success) {
return { message: 'Invalid email format' }
}
// 데이터 저장 및 처리 로직
}
// app/ui/signup.tsx
'use client'
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
const initialState = { message: '' }
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
return (
<form action={formAction}>
<input type="text" name="email" required />
<p>{state?.message}</p>
<button>Sign up</button>
</form>
)
}
Revalidate and Redirect
Server Actions에서 데이터를 변경한 후 Next.js 캐시를 무효화하고 특정 페이지로 리디렉션 할 수있다.
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(id: string) {
// 데이터 저장 및 처리 로직
revalidateTag('posts') // 캐시 무효화
revalidatePath('/posts') // 캐시 무효화
redirect(`/post/${id}`) // 해당 페이지로 이동
}
Optimistic UI
서버에서 데이터가 변경되기 전에 UI를 먼저 업데이트하는 Optimistic UI를 적용할 수 있다.
// app/page.tsx
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
)
return (
<div>
{optimisticMessages.map((m, k) => (
<div key={k}>{m.message}</div>
))}
<form
action={async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
Server Action 중첩 사용
<button>, <input type="submit">, <input type="image"> 같은 요소에서 formAction 속성을 활용해 개별 Server Action을 실행할 수 있다.
// app/ui/edit-post.tsx
'use client'
import { publishPost, saveDraft } from './actions'
export default function EditPost() {
return (
<form action={publishPost}>
<textarea name="content" />
<button type="submit">Publish</button>
{/* 게시물 초안을 저장하는 별도 버튼 */}
<button formAction={saveDraft}>Save Draft</button>
</form>
)
}
Cookies
Server Actions 내부에서 쿠키를 저장, 조회, 삭제할 수 있다.
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// 쿠키 가져오기
const value = cookies().get('name')?.value
// 쿠키 저장
cookies().set('name', 'Delba')
// 쿠키 삭제
cookies().delete('name')
}
CSRF 방어 및 도메인 제한
Server Actions는 내부적으로 CSRF 공격 방어 기능을 제공한다. 허용된 도메인만 Server Actions 실행 가능하다.
// next.config.js
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}