[Next.js 스터디] Server Actions and Mutations

공효은·2025년 3월 23일
0

서버 액션은 서버에서 수행되는 비동기 함수이다.
서버와 클라이언트 컴포넌트에서 form 제출과 data mutations 를 위해 사용된다.

Convention

"use server" directive를 사용한다.
1. async 함수의 상단에 위치할 수 있다.
2. 분리된 파일의 최상단에 위치 하여 해당 파일 전체를 서버 작업으로 표시할 수 있다.

Server Components

서버 컴포넌트는 인라인 함수 레벨 혹은 모듈 레벨에서 "use server" 지시어를 사용할 수 있다.

// Server Component
export default function Page() {
  // Server Action
  async function create() {
    'use server'
 
    // ...
  }
 
  return (
    // ...
  )
}

Client Components

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>
}

Behavior

  • 서버 액션은 form 의 action attribute에서 호출된다.
  • 서버 액션은 form으로 한정되지 않는다. event handlers, useEffect, third-party libraries 그리고 다른 form elements(button) 에서도 사용된다,
  • 서버 액션은 Next.js의 캐싱, 재검증 아키텍쳐와 통합된다. Netx.js는 업데이트된 UI, 새로운 데이터를 한번에 서버 왕복으로 리턴한다.
  • actions는 POST method를 사용한다. 그리고 오직 이 HTTP 메서드만 호출할 수 있다.
  • 서버 액션의 파라미터와 리턴 값은 직렬화 되어야만한다.
  • 서버 액션은 함수다. 이것은 application 어디에서도 재사용 될 수 있다는것을 의미한다.
  • 서버액션은 page, layout의 런타임을 상속한다.
  • 서버액션은 page, layout의 Route Segment Config를 상속한다.

Examples

Forms

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>
}

pending states

useFormStatus hook을 사용하면 form이 제출되는 동안 pending state를 보여줄 수 있다.

  • useFormStatus는 구체적인 form의 상태를 리턴하고, 이것은 form 요소의 하위로 정의 되어야한다.
  • useFormStatus는 리액트 hook 이므로 Client Component에 정의되어야한다.
//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 and error handling

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 훅은 유저에게 메시지를 보여준다.

  • useFormState 액션을 전달 하면 action의 function signature이 변화되고 새로운 prevState, initialState를 파라미터로 받을 수 있다.
  • useFormState는 리액트 훅이므로 Client Component에서 사용해야한다.
//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>
  )
}

Optimistic updates

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>
  )
}

Non-form Elements

일반적으로 서버 액션은 form 에서 사용하지만 또한 useEffeect나 event handlers 에서도 사용할 수 있다.

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

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 Handling

에러가 던져지면 가장 가까운 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')
  }
}

Revalidating data

서버액션에서 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')
}

Redirecting

서버 작업 완료 후 다른 경로로 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
}

Cookies

서버 액션에서 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')
}
profile
잼나게 코딩하면서 살고 싶어요 ^O^/

0개의 댓글