본문 바로가기

Study/NextJS 공식문서

[4주 차] Server Action

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'],
    },
  },
}

'Study > NextJS 공식문서' 카테고리의 다른 글

[5주 차] Server Components  (0) 2025.02.08
[4주 차] Patterns and Best Practices  (1) 2025.01.31
[4주 차] Data Fetching, Caching, and Revalidating  (0) 2025.01.31
[3주 차] Middleware  (0) 2025.01.31
[3주 차] Route Handlers  (0) 2025.01.27