서버 액션은 서버에서 수행되는 비동기 함수이다.
서버와 클라이언트 컴포넌트에서 form 제출과 data mutations 를 위해 사용된다.
"use server" directive를 사용한다.
1. async 함수의 상단에 위치할 수 있다.
2. 분리된 파일의 최상단에 위치 하여 해당 파일 전체를 서버 작업으로 표시할 수 있다.
서버 컴포넌트는 인라인 함수 레벨 혹은 모듈 레벨에서 "use server" 지시어를 사용할 수 있다.
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'
// ...
}
return (
// ...
)
}
Client 컴포넌트는 오직 모듈 수준에서 "user server" 지시어를 import 할 수 있다.
클라이언트 컴포넌트에서 서버 액션을 호출하기 위해서, 새로운 파일을 추가하고 해당 파일 최 상단에 "use server" 지시어를 추가하라.
파일 내의 모든 함수들은 서버 액션으로 표시된다.
'use server'
export async function create() {
// ...
}
import { create } from '@/app/actions'
export function Button() {
return (
// ...
)
}
Server Action을 클라이언트 컴포넌트 Props로 넘길 수 있다.
<ClientComponent updateItem={updateItem} />
'use client'
export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>
}
React 는 HTML form 이 action prop 와 함께 서버에서 호출되도록 한다.
form이 호출될때 action은 자동으로 FormData 객체를 받는다.
필드들을 관리하기 위해 useState를 사용할 필요가 없다. 데이터를 추출하기 위해 native FormData를 사용할 수 있다.
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>
}
useFormStatus hook을 사용하면 form이 제출되는 동안 pending state를 보여줄 수 있다.
//app/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
Add
</button>
)
}
SubmitButton은 form에 중첩될 수 있다.
//app/page.tsx
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
// Server Component
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
server-side validataion에서 data를 mutation 하기 전에 폼 필드를 검증하기 위해서 zod library를 사용할 수 있다.
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}
필드들이 서버에서 검증되면, 직렬화 가능한 객체를 리턴할 수 있다.그리고 useFormState 훅은 유저에게 메시지를 보여준다.
//app/actions.ts
'use server'
export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Please enter a valid email',
}
}
그리고 useFormState hook에 액션을 넘겨줄 수 있고 리턴 된 state를 error message에 display 할 수 있다.
'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}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Sign up</button>
</form>
)
}
React useOptimistic 훅을 사용해서 서버 액션이 끝나기 전에 응답을 기다리기 보다. UI를 업데이트 할 수 있다.
//app/page.tsx
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...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>
)
}
일반적으로 서버 액션은 form 에서 사용하지만 또한 useEffeect나 event handlers 에서도 사용할 수 있다.
서버 액션을 onClick과 같은 event handlers 에 사용할 수있다. count 를 증가시키는 예시이다.
//app/like-button.tsx
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}
사용자 경험을 향상 시키기 위해 useOptimistic이나 useTransition을 사용할 수 있다.
useEffect를 사용하면 컴포넌트가 마운트 되거나 의존성이 변할때 서버 액션을 호출 할 수 있다.
이것은 전역 이벤트 또는 자동으로 트리거가 필요한 작업에 유용한다.
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views: {views}</p>
에러가 던져지면 가장 가까운 error.js나 Suspense Boundary 에서 잡힌다.
UI에서 처리하는 에러를 반환하려면 try/catch를 사용하기를 권장한다.
예를 들어 서버 액션은 메세지를 반환하여 새로운 아이템을 생성할때 에러를 처리할 수 있다.
//app/actions.ts
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Failed to create task')
}
}
서버액션에서 revalidatePath를 사용하여 캐시를 revalidate 할 수 있다.
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts')
}
또는 revalidateTag를 사용하여 구체적인 데이터를 invalidate 할 수 있다.
//app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts')
}
서버 작업 완료 후 다른 경로로 redirect 시키기를 원한다면 redirect API를 사용할 수 있다.
redirect는 try/catch 블록 밖에서 사용할 필요가 있다.
//app/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
서버 액션에서 get, set, delete cookies를 할 수 있다.
//app/actions.ts
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// Get cookie
const value = cookies().get('name')?.value
// Set cookie
cookies().set('name', 'Delba')
// Delete cookie
cookies().delete('name')
}