
Next 13 Forms and Mutations 기반으로 작성되었습니다.
Server Actions을 사용하면 API endpoint를 만들 필요가 없는 대신 컴포넌트에서 직접 호출할 수 있는 비동기 서버 함수를 정의해야한다.
Server Actions은 서버 컴포넌트에서 정의하거나 클라이언트 컴포넌트에서 호출할 수 있다. 서버 컴포넌트에서 action을 정의하면 form이 자바스크립트 없이 작동하여 점진적인 향상을 제공할 수 있다.
next.config.js 파일에서 Server Actions를 허용해야한다.
// next.config.js
module.exports = {
experimental: {
serverActions: true,
},
}
Server Actions
Server-only Form을 만들려면 서버 컴포넌트에서 Server Action을 정의해야 하는데 action은 함수의 상단에 'use server'가 정의되거나 파일 상단에 있는 별도의 파일에서 정의될 수 있다.
// app/page.tsx
// 함수 상단에 정의되어 있는 Server Action
export default function Page() {
async function create(formData: FormData) {
'use server'
// mutate data
// revalidate cache
}
return <form action={create}>...</form>
}
<form action={create}>는 FormData 데이터 형식을 취한다. 위 예에서 HTML form을 통해 제출된 FormData는 server actioncreate에서 접근할 수 있다.
// app/action.ts
'use server'
import { revalidatePath } from 'next/cache'
import { sql } from '@vercel/postgres'
import { z } from 'zod'
export async function createTodo(prevState: any, formData: FormData) {
const schema = z.object({
todo: z.string().nonempty(),
})
const data = schema.parse({
todo: formData.get('todo'),
})
try {
await sql`
INSERT INTO todos (text)
VALUES (${data.todo})
`
revalidatePath('/')
return { message: `Added todo ${data.todo}` }
} catch (e) {
return { message: 'Failed to create todo' }
}
}
Server Actions을 사용하면 Next.js Cache를 revalidatePath 또는 revalidateTag를 사용하여 무효화할 수 있다.
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { revalidateTag } from 'next/cache'
export async function submit1() {
await submitForm()
// 전체 경로 세그먼트 무효화
revalidatePath('/')
}
export async function submit2() {
await addPost()
// 캐시 태그로 특정 데이터 fetch 무효화
revalidateTag('posts')
}
Server Action이 완료된 후 사용자를 다른 경로로 리다이렉트하려면 redirect와 함께 절대 경로 URL이나 상대 경로 URL을 사용할 수 있다.
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export default async function submit() {
const id = await addPost()
revalidateTag('posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to new route
}
required와 같은 HTML 유효성 검사를 사용하고 기본 Form 유효성 검사를 위해 type='email'을 사용하는 것을 추천한다.
더욱 고도화된 서버측 유효성 검사를 하기 위해서는 zod와 같은 스키마 유효성 검사 라이브러리를 사용하면 구문 분석된 Form 데이터의 구조를 검사한다.
// app/actions.ts
import { z } from 'zod'
const schema = z.object({
// ...
})
export default async function submit(formData: FormData) {
const parsed = schema.parse({
id: formData.get('id'),
})
// ...
}
서버에서 Form을 제출할 때 useFormStatus 훅을 사용하여 로딩 상태를 보여줄 수 있다. useFormStatus 훅은 Server Action을 사용하는 form 요소의 자식으로만 사용할 수 있다.
// app/submit-button.tsx
'use client'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
작성된 SubmitButton 컴포넌트를 Server Action과 form에서 사용할 수 있다.
// app/page.tsx
import { SubmitButton } from '@/app/submit-button'
export default async function Home() {
return (
<form action={...}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
Server Actions은 직렬화 가능한 객체를 반환할 수 있다. 예를 들어, Server Action에 새 항목을 만들 때 발생하는 오류를 처리할 수 있다.
// app/actions.ts
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
await createItem(formData.get('todo'))
return revalidatePath('/')
} catch (e) {
return { message: 'Failed to create' }
}
}
그리고나서 클라이언트 컴포넌트에서 해당 값을 읽고 오류 메시지를 보여줄 수 있다.
// app/add-form.tsx
'use client'
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
const initialState = {
message: null,
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)
return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
응답을 기다리지 않고 useOptimistic을 사용하여 Server Action이 완료되기 전에 UI를 최적으로 업데이트한다.
// app/page.tsx
'use client'
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
messages,
(state: Message[], newMessage: string) => [
...state,
{ message: newMessage },
]
)
return (
<div>
{optimisticMessages.map((m, k) => (
<div key={k}>{m.message}</div>
))}
<form
action={async (formData: FormData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
cookies함수를 사용하여 Server Action 내부에 쿠키를 설정할 수 있다.
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
export async function create() {
const cart = await createCart()
cookies().set('cartId', cart.id)
}
cookies함수를 사용하여 Server Action 내부에 쿠키를 삭제할 수 있다.
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
export async function delete() {
cookies().delete('name')
// ...
}