서버 액션은 서버에서 실행되는 비동기 함수로써, 서버 및 클라이언트 컴포넌트에서 form 제출 및 데이터 변경을 처리하는데 사용할 수 있다.

즉 API를 생성하지 않더라도 함수 수준에서 서버에 직접 접근해 데이터 요청 등을 수행할 수 있다.

규칙

서버 액션을 만들려면 async 함수 내부 또는 파일 상단에 use server 지시어로 선언해야한다.

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

또는 파일 상단에 선언하면 클라이언트 컴포넌트나 서버 컴포넌트에서 가져다 사용할 수 있다.

// app/actions.ts
'use server'
 
export async function create() {
  // ...
}
 
// app/ui/button.tsx
import { create } from '@/app/actions'
 
export function Button() {
  return (
    // ...
  )
}

Forms

React는 HTML form 요소를 확장하여 action 프로퍼티로 서버 액션을 호출할 수 있다.

form에서 action이 호출되면 FormData 객체를 자동으로 전달 받고, 필드를 관리하기 위해 useState를 사용할 필요가 없이 FormData 메서드를 사용하여 데이터를 추출할 수 있다.

export default function Page() {
  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>
}

추가 인수 전달

만약 서버 액션에 추가 인수를 전달하기 위해서는 bind메서드를 사용해서 전달할 수 있다.

function AddToCart({productId}) {
  async function addToCart(productId, formData) {
    "use server";
    //...
  }
  const addProductToCart = addToCart.bind(null, productId);
  return (
    <form action={addProductToCart}>
      <button type="submit">Add to Cart</button>
      //<input type="hidden" name="productId" value={productId} />
    </form>
  );
}

useFormStatus

Form이 제출되는 동안 보류 중인 상태를 표시하기 위해 useFormStatus 훅을 사용할 수 있다.

해당 훅은 특정 Form의 상태를 반환하기 때문에 <form> 요소의 자식으로 정의되어야 한다.

또한 훅은 클라이언트 컴포넌트에서 사용해야한다.

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

Network

서버 액션을 통해 구현한 데모 사이트 (opens in a new tab)가 있다.

여기서 todo title과 body를 입력하고 Add 버튼을 클릭하고 네트워크 탭을 보면 다음처럼 보인다.

network

페이로드를 보면 ACTION_ID라는 서버 액션과 입력한 데이터를 볼 수 있다.

서버 액션을 실행하면 클라이언트에서 현재 라우트 주소와 ACTION_ID, 입력한 데이터를 보내게 된다.

그러면 서버에서 요청 받은 데이터를 바탕으로 실행해야 하는 서버 액션을 찾고 서버에서 직접 실행한다.

use server로 선언 되어 있는 내용은 클라이언트 번들링에 포함되지 않고 서버에서만 실행되고, 따라서 CORS 이슈가 발생하지 않는다.

Form이 아닌 요소

보통 <form> 요소 내에서 서버 액션을 사용하지만, 이벤트 핸들러나 useEffect와 같은 코드 내에서도 호출할 수 있다.

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

onClick과 같은 이벤트 핸들러에서 서버 액션을 호출하여 좋아요 수를 늘릴 수 있다.

Revalidating

revalidatePath API를 사용하여 서버 액션 내에서 Next.js 캐시 유효성을 재검증할 수 있다.

'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
  revalidatePath('/posts')
}

또한 revalidateTag API도 동일하게 동작한다.

'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
  revalidateTag('posts')
}

참고