데이터 변경

데이터 변경 (Mutating Data)

Next.js에서는 React 서버 함수(Server Functions)로 데이터를 변경할 수 있어요. 서버 함수는 서버에서 실행되는 비동기 함수로, action이나 mutation 맥락에서는 서버 액션(Server Actions) 이라고도 불러요. 이 글에서는 서버 함수를 만들고 호출하는 방법을 살펴볼게요.

출처: Next.js 공식 문서 - Mutating Data

서버 함수(Server Functions)란?

서버 함수는 서버에서 실행되는 비동기 함수예요. 클라이언트에서 네트워크 요청을 통해 호출하기 때문에 반드시 비동기 함수여야 해요.

서버 함수가 폼 제출·mutation 같은 액션에 쓰이면 서버 액션(Server Actions) 이라고 해요. 관례상 서버 액션은 startTransition과 함께 쓰이는 비동기 함수예요. 함수가 다음 경우일 때 자동으로 이리 동작해요.

  • <form>action prop으로 전달될 때
  • <button>formAction prop으로 전달될 때

액션이 호출되면 Next.js는 갱신된 UI와 새 데이터를 단일 서버 왕복(single roundtrip) 으로 돌려줄 수 있어요. 내부적으로 액션은 POST 메서드를 쓰고, 이 HTTP 메서드로만 호출할 수 있어요.

경고: 서버 함수는 애플리케이션 UI 너머에서도 직접 POST 요청으로 접근할 수 있어요. 모든 서버 함수 안에서 인증·인가를 반드시 확인하세요.

서버 함수 만들기

서버 함수는 use server 지시어로 정의할 수 있어요. 비동기 함수의 맨 위에 지시어를 놓으면 그 함수를 서버 함수로 표시하고, 별도 파일의 맨 위에 놓으면 그 파일의 모든 export를 서버 함수로 표시해요.

import { auth } from '@/lib/auth'

export async function createPost(formData: FormData) {
  'use server'
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }

  const title = formData.get('title')
  const content = formData.get('content')

  // Mutate data
  // Revalidate cache
}

서버 컴포넌트에서

서버 컴포넌트에서는 함수 본문 맨 위에 "use server" 지시어를 추가해 서버 함수를 인라인으로 정의할 수 있어요.

export default function Page() {
  // Server Action
  async function createPost(formData: FormData) {
    'use server'
    // ...
  }

  return <></>
}

알아두면 좋아요: 서버 컴포넌트는 기본적으로 점진적 향상(progressive enhancement)을 지원해요. 서버 액션을 호출하는 폼은 JavaScript가 아직 로드되지 않았거나 비활성화돼 있어도 제출돼요.

클라이언트 컴포넌트에서

클라이언트 컴포넌트에서는 서버 함수를 정의할 순 없지만, 맨 위에 "use server" 지시어가 있는 파일에서 import해서 호출할 수 있어요.

'use server'

export async function createPost() {}
'use client'

import { createPost } from '@/app/actions'

export function Button() {
  return <button formAction={createPost}>Create</button>
}

알아두면 좋아요: 클라이언트 컴포넌트에서 서버 액션을 호출하는 폼은 JavaScript가 아직 로드되지 않았다면 제출을 큐에 쌓아 두고, 하이드레이션에서 우선 처리돼요. 하이드레이션 후에는 폼 제출 시 브라우저가 새로고침되지 않아요.

액션을 props로 넘기기

액션을 클라이언트 컴포넌트에 prop으로 넘길 수도 있어요.

<ClientComponent updateItemAction={updateItem} />
'use client'

export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}

서버 함수 호출하기

서버 함수를 호출하는 방법은 크게 두 가지예요.

  1. 서버·클라이언트 컴포넌트의 폼(Forms)
  2. 클라이언트 컴포넌트의 이벤트 핸들러useEffect

알아두면 좋아요: 서버 함수는 서버 측 mutation을 위해 설계됐고, 클라이언트는 현재 이들을 하나씩 dispatch하고 await해요. 병렬 데이터 페칭이 필요하면 서버 컴포넌트에서 데이터 페칭을 하거나, 하나의 서버 함수·라우트 핸들러 안에서 병렬 작업을 수행하세요.

폼 (Forms)

React는 HTML <form> 요소를 확장해서, HTML action prop으로 서버 함수를 호출할 수 있게 해 줘요. 폼에서 호출되면 함수는 자동으로 FormData 객체를 받고, 네이티브 FormData 메서드로 데이터를 추출할 수 있어요.

import { createPost } from '@/app/actions'

export function Form() {
  return (
    <form action={createPost}>
      <input type="text" name="title" />
      <input type="text" name="content" />
      <button type="submit">Create</button>
    </form>
  )
}
'use server'

import { auth } from '@/lib/auth'

export async function createPost(formData: FormData) {
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }

  const title = formData.get('title')
  const content = formData.get('content')

  // Mutate data
  // Revalidate cache
}

이벤트 핸들러

클라이언트 컴포넌트에서 onClick 같은 이벤트 핸들러로 서버 함수를 호출할 수 있어요.

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

예시

보류(pending) 상태 보여주기

서버 함수를 실행하는 동안 React의 useActionState 훅으로 로딩 인디케이터를 보여줄 수 있어요. 이 훅은 pending 불리언을 돌려줘요.

'use client'

import { useActionState, startTransition } from 'react'
import { createPost } from '@/app/actions'
import { LoadingSpinner } from '@/app/ui/loading-spinner'

export function Button() {
  const [state, action, pending] = useActionState(createPost, false)

  return (
    <button onClick={() => startTransition(action)}>
      {pending ? <LoadingSpinner /> : 'Create Post'}
    </button>
  )
}

데이터 갱신하기 (Refresh)

mutation 후 현재 페이지를 새로고침해 최신 데이터를 보여주고 싶다면, 서버 액션에서 next/cacherefresh를 호출하면 돼요. refresh()는 클라이언트 라우터를 갱신해 UI가 최신 상태를 반영하게 해 줘요. 다만 refresh()는 태그된 데이터를 재검증하지 않아요. 태그된 데이터를 재검증하려면 updateTag 또는 revalidateTag를 쓰세요.

'use server'

import { auth } from '@/lib/auth'
import { refresh } from 'next/cache'

export async function updatePost(formData: FormData) {
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }
  // Mutate data
  // ...

  refresh()
}

데이터 재검증하기 (Revalidate)

mutation 수행 후 Next.js 캐시를 재검증하고 갱신된 데이터를 보여주려면 서버 함수 안에서 revalidatePathrevalidateTag를 호출하면 돼요.

import { auth } from '@/lib/auth'
import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  'use server'
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }
  // Mutate data
  // ...

  revalidatePath('/posts')
}

mutation 후 리다이렉트

mutation 후 사용자를 다른 페이지로 보내고 싶다면 서버 함수 안에서 redirect를 호출하면 돼요.

'use server'

import { auth } from '@/lib/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }
  // Mutate data
  // ...

  revalidatePath('/posts')
  redirect('/posts')
}

redirect는 프레임워크가 처리하는 제어 흐름 예외를 던지므로, 그 뒤의 코드는 실행되지 않아요. 최신 데이터가 필요하면 그 전에 revalidatePathrevalidateTag를 호출하세요.

쿠키 (Cookies)

서버 액션 안에서 cookies API로 쿠키를 get, set, delete할 수 있어요. 서버 액션에서 쿠키를 set/delete하면, Next.js는 현재 페이지와 레이아웃을 서버에서 다시 렌더링해서 UI가 새 쿠키 값을 반영하게 해 줘요.

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  const cookieStore = await cookies()

  // Get cookie
  cookieStore.get('name')?.value

  // Set cookie
  cookieStore.set('name', 'Delba')

  // Delete cookie
  cookieStore.delete('name')
}

useEffect

React useEffect 훅으로 컴포넌트가 마운트되거나 의존성이 바뀔 때 서버 액션을 호출할 수 있어요. 전역 이벤트에 의존하거나 자동으로 트리거돼야 하는 mutation에 유용하죠. 예를 들어 앱 단축키의 onKeyDown, 무한 스크롤의 intersection observer, 컴포넌트 마운트 시 조회수 갱신 같은 경우예요.

'use client'

import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    startTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // `isPending`으로 사용자에게 피드백을 줄 수 있음
  return <p>Total Views: {views}</p>
}

더 알아보기 (Learn more)

  • Server Actions — 단일 왕복 응답 모델, 순차 dispatch, 보안, 캐싱 통합 등 Next.js에서 서버 액션의 동작 방식
  • revalidatePath — revalidatePath 함수 API 레퍼런스
  • revalidateTag — revalidateTag 함수 API 레퍼런스
  • redirect — redirect 함수 API 레퍼런스