데이터 변경
데이터 변경 (Mutating Data)
Next.js에서는 React 서버 함수(Server Functions)로 데이터를 변경할 수 있어요. 서버 함수는 서버에서 실행되는 비동기 함수로, action이나 mutation 맥락에서는 서버 액션(Server Actions) 이라고도 불러요. 이 글에서는 서버 함수를 만들고 호출하는 방법을 살펴볼게요.
서버 함수(Server Functions)란?
서버 함수는 서버에서 실행되는 비동기 함수예요. 클라이언트에서 네트워크 요청을 통해 호출하기 때문에 반드시 비동기 함수여야 해요.
서버 함수가 폼 제출·mutation 같은 액션에 쓰이면 서버 액션(Server Actions) 이라고 해요. 관례상 서버 액션은 startTransition과 함께 쓰이는 비동기 함수예요. 함수가 다음 경우일 때 자동으로 이리 동작해요.
<form>에actionprop으로 전달될 때<button>에formActionprop으로 전달될 때
액션이 호출되면 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>
}
서버 함수 호출하기
서버 함수를 호출하는 방법은 크게 두 가지예요.
- 서버·클라이언트 컴포넌트의 폼(Forms)
- 클라이언트 컴포넌트의 이벤트 핸들러와 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/cache의 refresh를 호출하면 돼요. 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 캐시를 재검증하고 갱신된 데이터를 보여주려면 서버 함수 안에서 revalidatePath나 revalidateTag를 호출하면 돼요.
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는 프레임워크가 처리하는 제어 흐름 예외를 던지므로, 그 뒤의 코드는 실행되지 않아요. 최신 데이터가 필요하면 그 전에 revalidatePath나 revalidateTag를 호출하세요.
쿠키 (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 레퍼런스