1. 사용자가 폼 제출
2. React가 fetch로 Server Action 호출
3. 페이지 새로고침 없이 UI 업데이트
You can update data in Next.js using React's Server Functions. This page will go through how you can create and invoke Server Functions.
A Server Function is an asynchronous function that runs on the server. They can be called from the client through a network request, which is why they must be asynchronous.
In an action or mutation context, they are also called Server Actions.
Good to know: A Server Action is a Server Function used in a specific way (for handling form submissions and mutations). Server Function is the broader term.
By convention, a Server Action is an async function used with startTransition. This happens automatically when the function is:
<form> using the action prop.<button> using the formAction prop.In Next.js, Server Actions integrate with the framework's caching architecture. When an action is invoked, Next.js can return both the updated UI and new data in a single server roundtrip.
Behind the scenes, actions use the POST method, and only this HTTP method can invoke them.
A Server Function can be defined by using the use server directive. You can place the directive at the top of an asynchronous function to mark the function as a Server Function, or at the top of a separate file to mark all exports of that file.
export async function createPost(formData: FormData) {
'use server';
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
export async function deletePost(formData: FormData) {
'use server';
const id = formData.get('id');
// Update data
// Revalidate cache
}
export async function createPost(formData) {
'use server';
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
export async function deletePost(formData) {
'use server';
const id = formData.get('id');
// Update data
// Revalidate cache
}
Server Functions can be inlined in Server Components by adding the "use server" directive to the top of the function body:
export default function Page() {
// Server Action
async function createPost(formData: FormData) {
'use server';
// ...
}
return <></>;
}
export default function Page() {
// Server Action
async function createPost(formData: FormData) {
'use server'
// ...
}
return <></>
}
Good to know: Server Components support progressive enhancement by default, meaning forms that call Server Actions will be submitted even if JavaScript hasn't loaded yet or is disabled.
It's not possible to define Server Functions in Client Components. However, you can invoke them in Client Components by importing them from a file that has the "use server" directive at the top of it:
'use server';
export async function createPost() {}
'use server';
export async function createPost() {}
'use client';
import { createPost } from '@/app/actions';
export function Button() {
return <button formAction={createPost}>Create</button>;
}
'use client';
import { createPost } from '@/app/actions';
export function Button() {
return <button formAction={createPost}>Create</button>;
}
Good to know: In Client Components, forms invoking Server Actions will queue submissions if JavaScript isn't loaded yet, and will be prioritized for hydration. After hydration, the browser does not refresh on form submission.
You can also pass an action to a Client Component as a prop:
<ClientComponent updateItemAction={updateItem} />
'use client';
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void;
}) {
return <form action={updateItemAction}>{/* ... */}</form>;
}
'use client';
export default function ClientComponent({ updateItemAction }) {
return <form action={updateItemAction}>{/* ... */}</form>;
}
There are two main ways you can invoke a Server Function:
Good to know: Server Functions are designed for server-side mutations. The client currently dispatches and awaits them one at a time. This is an implementation detail and may change. If you need parallel data fetching, use data fetching in Server Components, or perform parallel work inside a single Server Function or Route Handler.
React extends the HTML <form> element to allow a Server Function to be invoked with the HTML action prop.
When invoked in a form, the function automatically receives the FormData object. You can extract the data using the native FormData methods:
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>
);
}
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';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
'use server';
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
You can invoke a Server Function in a Client Component by using event handlers such as 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>
</>
);
}
'use client';
import { incrementLike } from './actions';
import { useState } from 'react';
export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike();
setLikes(updatedLikes);
}}
>
Like
</button>
</>
);
}
While executing a Server Function, you can show a loading indicator with React's useActionState hook. This hook returns a pending boolean:
'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>
);
}
'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>
);
}
After a mutation, you may want to refresh the current page to show the latest data. You can do this by calling refresh from next/cache in a Server Action:
'use server';
import { refresh } from 'next/cache';
export async function updatePost(formData: FormData) {
// Update data
// ...
refresh();
}
'use server';
import { refresh } from 'next/cache';
export async function updatePost(formData) {
// Update data
// ...
refresh();
}
This refreshes the client router, ensuring the UI reflects the latest state. The refresh() function does not revalidate tagged data. To revalidate tagged data, use updateTag or revalidateTag instead.
After performing an update, you can revalidate the Next.js cache and show the updated data by calling revalidatePath or revalidateTag within the Server Function:
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
'use server';
// Update data
// ...
revalidatePath('/posts');
}
import { revalidatePath } from 'next/cache';
export async function createPost(formData) {
'use server';
// Update data
// ...
revalidatePath('/posts');
}
You may want to redirect the user to a different page after performing an update. You can do this by calling redirect within the Server Function.
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
// Update data
// ...
revalidatePath('/posts');
redirect('/posts');
}
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData) {
// Update data
// ...
revalidatePath('/posts');
redirect('/posts');
}
Calling redirect throws a framework handled control-flow exception. Any code after it won't execute. If you need fresh data, call revalidatePath or revalidateTag beforehand.
You can get, set, and delete cookies inside a Server Action using the cookies API.
When you set or delete a cookie in a Server Action, Next.js re-renders the current page and its layouts on the server so the UI reflects the new cookie value.
Good to know: The server update applies to the current React tree, re-rendering, mounting, or unmounting components, as needed. Client state is preserved for re-rendered components, and effects re-run if their dependencies changed.
'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');
}
'use server';
import { cookies } from 'next/headers';
export async function exampleAction() {
// Get cookie
const cookieStore = await cookies();
// Get cookie
cookieStore.get('name')?.value;
// Set cookie
cookieStore.set('name', 'Delba');
// Delete cookie
cookieStore.delete('name');
}
You can use the React useEffect hook to invoke a Server Action when the component mounts or a dependency changes. This is useful for mutations that depend on global events or need to be triggered automatically. For example, onKeyDown for app shortcuts, an intersection observer hook for infinite scrolling, or when the component mounts to update a view count:
'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);
});
}, []);
// You can use `isPending` to give users feedback
return <p>Total Views: {views}</p>;
}
'use client';
import { incrementViews } from './actions';
import { useState, useEffect, useTransition } from 'react';
export default function ViewCount({ initialViews }) {
const [views, setViews] = useState(initialViews);
const [isPending, startTransition] = useTransition();
useEffect(() => {
startTransition(async () => {
const updatedViews = await incrementViews();
setViews(updatedViews);
});
}, []);
// You can use `isPending` to give users feedback
return <p>Total Views: {views}</p>;
}
Creating Server Functions
두 가지 Server Function 정의 패턴을 비교하고, JS 활성/비활성 시 Progressive Enhancement 동작을 확인해보세요.
왜 필요한가: Server Function 정의 위치에 따라 Client Component에서의 사용 가능 여부와 재사용성이 달라집니다.
언제 사용하는가: Server Component에서 데이터를 변경(mutate)하는 함수를 정의할 때
코드 패턴
export default function Page() {
// Server Action
async function createPost(formData: FormData) {
'use server'
// 서버에서만 실행됨
}
return <form action={createPost}>...</form>
}특성
Server Component 내부 함수 본문에 'use server'를 선언합니다. 해당 함수만 Server Function이 됩니다.
Progressive Enhancement 시뮬레이션
JS 활성 시
1. 사용자가 폼 제출
2. React가 fetch로 Server Action 호출
3. 페이지 새로고침 없이 UI 업데이트
JS 비활성 시
1. 사용자가 폼 제출
2. 브라우저 기본 HTML form POST
3. 전체 페이지 새로고침으로 결과 표시
핵심: Server Component에서 인라인으로 정의한 Server Function은 progressive enhancement를 지원합니다. JS가 로드되기 전이나 비활성화되어도 <form action>이 정상 동작합니다.
Creating Server Functions
파일 탭을 전환하여 Server Function을 별도 파일에 정의하고 Client Component에서 import하는 패턴을 확인해보세요.
왜 필요한가: 서버/클라이언트 경계를 명확히 분리하여 보안과 번들 크기를 최적화합니다.
언제 사용하는가: Client Component에서 Server Action을 호출해야 할 때
'use server'
export async function createPost() {
// 이 파일의 모든 export는 Server Function
// Client Component에서 import하여 사용 가능
}데이터 플로우
actions.ts
'use server'
Button.tsx
'use client'
네트워크 요청
POST (자동)
주의: Client Component에서 form으로 Server Action을 호출할 때, JS가 아직 로드되지 않았으면 제출이 큐에 저장됩니다. Hydration이 완료되면 큐의 제출이 순차 처리되고, 이후 브라우저의 기본 form 새로고침은 발생하지 않습니다.
핵심: Client Component에서 Server Function을 사용하려면 반드시 'use server' 지시어가 있는 별도 파일에서 import해야 합니다. 인라인 선언은 Server Component에서만 가능합니다.
Creating Server Functions
컴포넌트 트리를 클릭하여 Server Action이 prop으로 전달되는 과정을 단계별로 추적해보세요.
왜 필요한가: 컴포넌트 분리(Server/Client)를 유지하면서도 Server Action을 유연하게 재사용할 수 있습니다.
언제 사용하는가: Server Component의 action을 여러 Client Component에서 재사용하거나, 동적으로 action을 바인딩할 때
컴포넌트 트리 (클릭하여 단계 추적)
ServerPage
Server Component
ClientComponent
'use client'
Server Component (page.tsx)
Server// Server Component
async function updateItem(formData: FormData) {
'use server'
// 서버에서 실행
}
export default function Page() {
return (
<ClientComponent
updateItemAction={updateItem}
/>
)
}Client Component
Client'use client'
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (fd: FormData) => void
}) {
return (
<form action={updateItemAction}>
{/* ... */}
</form>
)
}핵심: Server Action을 prop으로 전달하면 Client Component가 서버 로직에 직접 의존하지 않으면서도 서버 경계를 안전하게 넘나들 수 있습니다. React가 직렬화를 자동 처리합니다.
Invoking Server Functions
폼 필드에 값을 입력하고 제출해보세요. FormData 객체에 어떤 key-value가 자동으로 수집되는지 실시간으로 확인할 수 있습니다.
왜 필요한가: Server Action과 HTML form의 통합은 progressive enhancement의 핵심이며, FormData가 데이터 전달의 표준 인터페이스입니다.
언제 사용하는가: 서버에서 데이터를 생성/수정/삭제하는 폼을 만들 때
인터랙티브 폼
FormData 추출 결과
대기 중코드 패턴
<form action={createPost}>
<input type="text" name="title" />
<input type="text" name="content" />
<button type="submit">Create</button>
</form>핵심: <form action={serverFn}>을 사용하면 React가 자동으로 FormData를 수집하여 Server Function에 전달합니다. input의 name 속성이 FormData의 key가 됩니다.
Invoking Server Functions
Like 버튼을 클릭하여 onClick 이벤트 핸들러에서 Server Action이 호출되는 비동기 흐름을 추적해보세요.
왜 필요한가: form 제출이 아닌 버튼 클릭, 드래그 앤 드롭 등 다양한 이벤트에서 서버 데이터를 변경해야 할 때 필요합니다.
언제 사용하는가: form 이외의 이벤트 핸들러에서 서버 데이터를 변경(mutate)할 때
인터랙티브 Like 버튼
Total Likes: 42
비동기 흐름 로그
코드 패턴
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}>
Like
</button>
</>
)
}핵심: onClick 핸들러에서 await serverAction()으로 호출하고, 응답값으로 useState를 업데이트하여 UI에 반영합니다. form 제출이 아닌 모든 이벤트 기반 mutation에 사용하세요.
Examples
버튼을 클릭하여 useActionState의 pending 상태 전환을 시뮬레이션해보세요. [state, action, pending] 반환값이 실시간으로 변합니다.
왜 필요한가: 사용자에게 서버 작업이 진행 중임을 알려 UX를 개선합니다. pending 없이는 버튼이 멈춘 것처럼 보입니다.
언제 사용하는가: Server Action 호출 중 로딩 인디케이터나 비활성 버튼을 보여줘야 할 때
상태 파이프라인
Idle
Pending
Complete
useActionState 반환값 실시간
state
false (초기값)
action
fn()
pending
false코드 패턴
const [state, action, pending] = useActionState(
createPost,
false // 초기값
)
return (
<button onClick={() => startTransition(action)}>
{pending ? <LoadingSpinner /> : 'Create Post'}
</button>
)핵심: useActionState는 [state, action, pending]을 반환합니다. pending이 true인 동안 로딩 UI를 표시하고, state에 서버 응답 결과가 반영됩니다.
Examples
시뮬레이션을 실행하여 데이터 변경 후 refresh()가 클라이언트 라우터를 갱신하는 과정을 타임라인으로 확인해보세요.
왜 필요한가: Server Action에서 데이터를 변경한 후, 페이지에 최신 상태를 즉시 반영해야 할 때 사용합니다.
언제 사용하는가: mutation 후 현재 페이지의 UI만 갱신하면 충분할 때 (캐시 태그 무효화가 불필요할 때)
실행 타임라인
데이터 변경
DB 업데이트 실행
refresh() 호출
클라이언트 라우터 갱신
UI 업데이트
최신 데이터 표시
서버 데이터
title: "원래 제목"
화면 표시 (UI)
title: "원래 제목"
코드 패턴
'use server'
import { refresh } from 'next/cache'
export async function updatePost(formData: FormData) {
// Update data
// ...
refresh() // 클라이언트 라우터 갱신
}핵심: refresh()는 클라이언트 라우터를 갱신하여 UI에 최신 상태를 반영합니다. 하지만 tagged data는 재검증하지 않으므로, 태그 기반 캐시에는 revalidateTag를 사용하세요.
Examples
path와 tag 방식을 전환하고 무효화 버튼을 눌러보세요. 어떤 캐시 노드가 무효화되는지 하이라이트로 확인할 수 있습니다.
왜 필요한가: 데이터 변경 후 관련된 캐시만 선택적으로 무효화하여 성능과 데이터 정합성을 동시에 확보합니다.
언제 사용하는가: Server Action에서 데이터를 변경한 후 캐시를 갱신해야 할 때
무효화할 경로 선택
캐시 노드 다이어그램
/posts (목록)
/posts/1
/posts/2
/posts/1/comments
코드 패턴
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
'use server'
// Update data...
revalidatePath('/posts')
}핵심: revalidatePath는 특정 경로 하나를, revalidateTag는 같은 태그를 가진 모든 캐시를 무효화합니다. 태그 방식이 더 세밀한 제어를 제공합니다.
Examples
시뮬레이션을 실행하여 revalidatePath → redirect 순서로 실행되는 과정을 관찰하세요. redirect 이후 코드가 실행되지 않는 것을 확인할 수 있습니다.
왜 필요한가: redirect 순서를 잘못 배치하면 revalidation이 실행되지 않아 오래된 데이터가 표시될 수 있습니다.
언제 사용하는가: Server Action에서 데이터 변경 후 다른 페이지로 이동시킬 때
코드 실행 시뮬레이션
실행 순서
주의: redirect()는 내부적으로 exception을 throw하므로 이후의 코드는 실행되지 않습니다. 반드시 revalidatePath/revalidateTag를 redirect보다 먼저 호출하세요.
핵심: redirect는 제어 흐름 exception을 throw합니다. 반드시 revalidatePath나 revalidateTag를 먼저 호출한 후 redirect를 호출하세요.
Examples
마운트 버튼을 눌러 컴포넌트가 마운트되면서 useEffect 내에서 Server Action이 자동 호출되는 과정을 타임라인으로 확인하세요.
왜 필요한가: 컴포넌트 마운트나 의존성 변경 시 자동으로 서버 데이터를 변경해야 하는 경우가 있습니다.
언제 사용하는가: 조회수 카운트, 앱 단축키 처리, intersection observer 등 글로벌 이벤트/마운트 시점의 mutation
컴포넌트 마운트 시뮬레이션
실행 타임라인
Component Mount
컴포넌트가 DOM에 마운트됨
useEffect → startTransition
startTransition 내에서 Server Action 호출
isPending = true
서버 응답 대기 중... (isPending 피드백)
Resolved
setViews(updatedViews) → UI 업데이트
코드 패턴
'use client'
import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'
export default function ViewCount({ initialViews }) {
const [views, setViews] = useState(initialViews)
const [isPending, startTransition] = useTransition()
useEffect(() => {
startTransition(async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
})
}, [])
return <p>Total Views: {views}</p>
}핵심: useEffect + startTransition으로 마운트 시 Server Action을 자동 호출합니다. isPending으로 로딩 피드백을 제공할 수 있습니다.
You can update data in Next.js using React's Server Functions. This page will go through how you can create and invoke Server Functions.
A Server Function is an asynchronous function that runs on the server. They can be called from the client through a network request, which is why they must be asynchronous.
In an action or mutation context, they are also called Server Actions.
Good to know: A Server Action is a Server Function used in a specific way (for handling form submissions and mutations). Server Function is the broader term.
By convention, a Server Action is an async function used with startTransition. This happens automatically when the function is:
<form> using the action prop.<button> using the formAction prop.In Next.js, Server Actions integrate with the framework's caching architecture. When an action is invoked, Next.js can return both the updated UI and new data in a single server roundtrip.
Behind the scenes, actions use the POST method, and only this HTTP method can invoke them.
A Server Function can be defined by using the use server directive. You can place the directive at the top of an asynchronous function to mark the function as a Server Function, or at the top of a separate file to mark all exports of that file.
export async function createPost(formData: FormData) {
'use server';
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
export async function deletePost(formData: FormData) {
'use server';
const id = formData.get('id');
// Update data
// Revalidate cache
}
export async function createPost(formData) {
'use server';
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
export async function deletePost(formData) {
'use server';
const id = formData.get('id');
// Update data
// Revalidate cache
}
Server Functions can be inlined in Server Components by adding the "use server" directive to the top of the function body:
export default function Page() {
// Server Action
async function createPost(formData: FormData) {
'use server';
// ...
}
return <></>;
}
export default function Page() {
// Server Action
async function createPost(formData: FormData) {
'use server'
// ...
}
return <></>
}
Good to know: Server Components support progressive enhancement by default, meaning forms that call Server Actions will be submitted even if JavaScript hasn't loaded yet or is disabled.
It's not possible to define Server Functions in Client Components. However, you can invoke them in Client Components by importing them from a file that has the "use server" directive at the top of it:
'use server';
export async function createPost() {}
'use server';
export async function createPost() {}
'use client';
import { createPost } from '@/app/actions';
export function Button() {
return <button formAction={createPost}>Create</button>;
}
'use client';
import { createPost } from '@/app/actions';
export function Button() {
return <button formAction={createPost}>Create</button>;
}
Good to know: In Client Components, forms invoking Server Actions will queue submissions if JavaScript isn't loaded yet, and will be prioritized for hydration. After hydration, the browser does not refresh on form submission.
You can also pass an action to a Client Component as a prop:
<ClientComponent updateItemAction={updateItem} />
'use client';
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void;
}) {
return <form action={updateItemAction}>{/* ... */}</form>;
}
'use client';
export default function ClientComponent({ updateItemAction }) {
return <form action={updateItemAction}>{/* ... */}</form>;
}
There are two main ways you can invoke a Server Function:
Good to know: Server Functions are designed for server-side mutations. The client currently dispatches and awaits them one at a time. This is an implementation detail and may change. If you need parallel data fetching, use data fetching in Server Components, or perform parallel work inside a single Server Function or Route Handler.
React extends the HTML <form> element to allow a Server Function to be invoked with the HTML action prop.
When invoked in a form, the function automatically receives the FormData object. You can extract the data using the native FormData methods:
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>
);
}
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';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
'use server';
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
// Update data
// Revalidate cache
}
You can invoke a Server Function in a Client Component by using event handlers such as 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>
</>
);
}
'use client';
import { incrementLike } from './actions';
import { useState } from 'react';
export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike();
setLikes(updatedLikes);
}}
>
Like
</button>
</>
);
}
While executing a Server Function, you can show a loading indicator with React's useActionState hook. This hook returns a pending boolean:
'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>
);
}
'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>
);
}
After a mutation, you may want to refresh the current page to show the latest data. You can do this by calling refresh from next/cache in a Server Action:
'use server';
import { refresh } from 'next/cache';
export async function updatePost(formData: FormData) {
// Update data
// ...
refresh();
}
'use server';
import { refresh } from 'next/cache';
export async function updatePost(formData) {
// Update data
// ...
refresh();
}
This refreshes the client router, ensuring the UI reflects the latest state. The refresh() function does not revalidate tagged data. To revalidate tagged data, use updateTag or revalidateTag instead.
After performing an update, you can revalidate the Next.js cache and show the updated data by calling revalidatePath or revalidateTag within the Server Function:
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
'use server';
// Update data
// ...
revalidatePath('/posts');
}
import { revalidatePath } from 'next/cache';
export async function createPost(formData) {
'use server';
// Update data
// ...
revalidatePath('/posts');
}
You may want to redirect the user to a different page after performing an update. You can do this by calling redirect within the Server Function.
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
// Update data
// ...
revalidatePath('/posts');
redirect('/posts');
}
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData) {
// Update data
// ...
revalidatePath('/posts');
redirect('/posts');
}
Calling redirect throws a framework handled control-flow exception. Any code after it won't execute. If you need fresh data, call revalidatePath or revalidateTag beforehand.
You can get, set, and delete cookies inside a Server Action using the cookies API.
When you set or delete a cookie in a Server Action, Next.js re-renders the current page and its layouts on the server so the UI reflects the new cookie value.
Good to know: The server update applies to the current React tree, re-rendering, mounting, or unmounting components, as needed. Client state is preserved for re-rendered components, and effects re-run if their dependencies changed.
'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');
}
'use server';
import { cookies } from 'next/headers';
export async function exampleAction() {
// Get cookie
const cookieStore = await cookies();
// Get cookie
cookieStore.get('name')?.value;
// Set cookie
cookieStore.set('name', 'Delba');
// Delete cookie
cookieStore.delete('name');
}
You can use the React useEffect hook to invoke a Server Action when the component mounts or a dependency changes. This is useful for mutations that depend on global events or need to be triggered automatically. For example, onKeyDown for app shortcuts, an intersection observer hook for infinite scrolling, or when the component mounts to update a view count:
'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);
});
}, []);
// You can use `isPending` to give users feedback
return <p>Total Views: {views}</p>;
}
'use client';
import { incrementViews } from './actions';
import { useState, useEffect, useTransition } from 'react';
export default function ViewCount({ initialViews }) {
const [views, setViews] = useState(initialViews);
const [isPending, startTransition] = useTransition();
useEffect(() => {
startTransition(async () => {
const updatedViews = await incrementViews();
setViews(updatedViews);
});
}, []);
// You can use `isPending` to give users feedback
return <p>Total Views: {views}</p>;
}
Creating Server Functions
두 가지 Server Function 정의 패턴을 비교하고, JS 활성/비활성 시 Progressive Enhancement 동작을 확인해보세요.
왜 필요한가: Server Function 정의 위치에 따라 Client Component에서의 사용 가능 여부와 재사용성이 달라집니다.
언제 사용하는가: Server Component에서 데이터를 변경(mutate)하는 함수를 정의할 때
코드 패턴
export default function Page() {
// Server Action
async function createPost(formData: FormData) {
'use server'
// 서버에서만 실행됨
}
return <form action={createPost}>...</form>
}특성
Server Component 내부 함수 본문에 'use server'를 선언합니다. 해당 함수만 Server Function이 됩니다.
Progressive Enhancement 시뮬레이션
JS 활성 시
1. 사용자가 폼 제출
2. React가 fetch로 Server Action 호출
3. 페이지 새로고침 없이 UI 업데이트
JS 비활성 시
1. 사용자가 폼 제출
2. 브라우저 기본 HTML form POST
3. 전체 페이지 새로고침으로 결과 표시
핵심: Server Component에서 인라인으로 정의한 Server Function은 progressive enhancement를 지원합니다. JS가 로드되기 전이나 비활성화되어도 <form action>이 정상 동작합니다.
Creating Server Functions
파일 탭을 전환하여 Server Function을 별도 파일에 정의하고 Client Component에서 import하는 패턴을 확인해보세요.
왜 필요한가: 서버/클라이언트 경계를 명확히 분리하여 보안과 번들 크기를 최적화합니다.
언제 사용하는가: Client Component에서 Server Action을 호출해야 할 때
'use server'
export async function createPost() {
// 이 파일의 모든 export는 Server Function
// Client Component에서 import하여 사용 가능
}데이터 플로우
actions.ts
'use server'
Button.tsx
'use client'
네트워크 요청
POST (자동)
주의: Client Component에서 form으로 Server Action을 호출할 때, JS가 아직 로드되지 않았으면 제출이 큐에 저장됩니다. Hydration이 완료되면 큐의 제출이 순차 처리되고, 이후 브라우저의 기본 form 새로고침은 발생하지 않습니다.
핵심: Client Component에서 Server Function을 사용하려면 반드시 'use server' 지시어가 있는 별도 파일에서 import해야 합니다. 인라인 선언은 Server Component에서만 가능합니다.
Creating Server Functions
컴포넌트 트리를 클릭하여 Server Action이 prop으로 전달되는 과정을 단계별로 추적해보세요.
왜 필요한가: 컴포넌트 분리(Server/Client)를 유지하면서도 Server Action을 유연하게 재사용할 수 있습니다.
언제 사용하는가: Server Component의 action을 여러 Client Component에서 재사용하거나, 동적으로 action을 바인딩할 때
컴포넌트 트리 (클릭하여 단계 추적)
ServerPage
Server Component
ClientComponent
'use client'
Server Component (page.tsx)
Server// Server Component
async function updateItem(formData: FormData) {
'use server'
// 서버에서 실행
}
export default function Page() {
return (
<ClientComponent
updateItemAction={updateItem}
/>
)
}Client Component
Client'use client'
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (fd: FormData) => void
}) {
return (
<form action={updateItemAction}>
{/* ... */}
</form>
)
}핵심: Server Action을 prop으로 전달하면 Client Component가 서버 로직에 직접 의존하지 않으면서도 서버 경계를 안전하게 넘나들 수 있습니다. React가 직렬화를 자동 처리합니다.
Invoking Server Functions
폼 필드에 값을 입력하고 제출해보세요. FormData 객체에 어떤 key-value가 자동으로 수집되는지 실시간으로 확인할 수 있습니다.
왜 필요한가: Server Action과 HTML form의 통합은 progressive enhancement의 핵심이며, FormData가 데이터 전달의 표준 인터페이스입니다.
언제 사용하는가: 서버에서 데이터를 생성/수정/삭제하는 폼을 만들 때
인터랙티브 폼
FormData 추출 결과
대기 중코드 패턴
<form action={createPost}>
<input type="text" name="title" />
<input type="text" name="content" />
<button type="submit">Create</button>
</form>핵심: <form action={serverFn}>을 사용하면 React가 자동으로 FormData를 수집하여 Server Function에 전달합니다. input의 name 속성이 FormData의 key가 됩니다.
Invoking Server Functions
Like 버튼을 클릭하여 onClick 이벤트 핸들러에서 Server Action이 호출되는 비동기 흐름을 추적해보세요.
왜 필요한가: form 제출이 아닌 버튼 클릭, 드래그 앤 드롭 등 다양한 이벤트에서 서버 데이터를 변경해야 할 때 필요합니다.
언제 사용하는가: form 이외의 이벤트 핸들러에서 서버 데이터를 변경(mutate)할 때
인터랙티브 Like 버튼
Total Likes: 42
비동기 흐름 로그
코드 패턴
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}>
Like
</button>
</>
)
}핵심: onClick 핸들러에서 await serverAction()으로 호출하고, 응답값으로 useState를 업데이트하여 UI에 반영합니다. form 제출이 아닌 모든 이벤트 기반 mutation에 사용하세요.
Examples
버튼을 클릭하여 useActionState의 pending 상태 전환을 시뮬레이션해보세요. [state, action, pending] 반환값이 실시간으로 변합니다.
왜 필요한가: 사용자에게 서버 작업이 진행 중임을 알려 UX를 개선합니다. pending 없이는 버튼이 멈춘 것처럼 보입니다.
언제 사용하는가: Server Action 호출 중 로딩 인디케이터나 비활성 버튼을 보여줘야 할 때
상태 파이프라인
Idle
Pending
Complete
useActionState 반환값 실시간
state
false (초기값)
action
fn()
pending
false코드 패턴
const [state, action, pending] = useActionState(
createPost,
false // 초기값
)
return (
<button onClick={() => startTransition(action)}>
{pending ? <LoadingSpinner /> : 'Create Post'}
</button>
)핵심: useActionState는 [state, action, pending]을 반환합니다. pending이 true인 동안 로딩 UI를 표시하고, state에 서버 응답 결과가 반영됩니다.
Examples
시뮬레이션을 실행하여 데이터 변경 후 refresh()가 클라이언트 라우터를 갱신하는 과정을 타임라인으로 확인해보세요.
왜 필요한가: Server Action에서 데이터를 변경한 후, 페이지에 최신 상태를 즉시 반영해야 할 때 사용합니다.
언제 사용하는가: mutation 후 현재 페이지의 UI만 갱신하면 충분할 때 (캐시 태그 무효화가 불필요할 때)
실행 타임라인
데이터 변경
DB 업데이트 실행
refresh() 호출
클라이언트 라우터 갱신
UI 업데이트
최신 데이터 표시
서버 데이터
title: "원래 제목"
화면 표시 (UI)
title: "원래 제목"
코드 패턴
'use server'
import { refresh } from 'next/cache'
export async function updatePost(formData: FormData) {
// Update data
// ...
refresh() // 클라이언트 라우터 갱신
}핵심: refresh()는 클라이언트 라우터를 갱신하여 UI에 최신 상태를 반영합니다. 하지만 tagged data는 재검증하지 않으므로, 태그 기반 캐시에는 revalidateTag를 사용하세요.
Examples
path와 tag 방식을 전환하고 무효화 버튼을 눌러보세요. 어떤 캐시 노드가 무효화되는지 하이라이트로 확인할 수 있습니다.
왜 필요한가: 데이터 변경 후 관련된 캐시만 선택적으로 무효화하여 성능과 데이터 정합성을 동시에 확보합니다.
언제 사용하는가: Server Action에서 데이터를 변경한 후 캐시를 갱신해야 할 때
무효화할 경로 선택
캐시 노드 다이어그램
/posts (목록)
/posts/1
/posts/2
/posts/1/comments
코드 패턴
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
'use server'
// Update data...
revalidatePath('/posts')
}핵심: revalidatePath는 특정 경로 하나를, revalidateTag는 같은 태그를 가진 모든 캐시를 무효화합니다. 태그 방식이 더 세밀한 제어를 제공합니다.
Examples
시뮬레이션을 실행하여 revalidatePath → redirect 순서로 실행되는 과정을 관찰하세요. redirect 이후 코드가 실행되지 않는 것을 확인할 수 있습니다.
왜 필요한가: redirect 순서를 잘못 배치하면 revalidation이 실행되지 않아 오래된 데이터가 표시될 수 있습니다.
언제 사용하는가: Server Action에서 데이터 변경 후 다른 페이지로 이동시킬 때
코드 실행 시뮬레이션
실행 순서
주의: redirect()는 내부적으로 exception을 throw하므로 이후의 코드는 실행되지 않습니다. 반드시 revalidatePath/revalidateTag를 redirect보다 먼저 호출하세요.
핵심: redirect는 제어 흐름 exception을 throw합니다. 반드시 revalidatePath나 revalidateTag를 먼저 호출한 후 redirect를 호출하세요.
Examples
마운트 버튼을 눌러 컴포넌트가 마운트되면서 useEffect 내에서 Server Action이 자동 호출되는 과정을 타임라인으로 확인하세요.
왜 필요한가: 컴포넌트 마운트나 의존성 변경 시 자동으로 서버 데이터를 변경해야 하는 경우가 있습니다.
언제 사용하는가: 조회수 카운트, 앱 단축키 처리, intersection observer 등 글로벌 이벤트/마운트 시점의 mutation
컴포넌트 마운트 시뮬레이션
실행 타임라인
Component Mount
컴포넌트가 DOM에 마운트됨
useEffect → startTransition
startTransition 내에서 Server Action 호출
isPending = true
서버 응답 대기 중... (isPending 피드백)
Resolved
setViews(updatedViews) → UI 업데이트
코드 패턴
'use client'
import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'
export default function ViewCount({ initialViews }) {
const [views, setViews] = useState(initialViews)
const [isPending, startTransition] = useTransition()
useEffect(() => {
startTransition(async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
})
}, [])
return <p>Total Views: {views}</p>
}핵심: useEffect + startTransition으로 마운트 시 Server Action을 자동 호출합니다. isPending으로 로딩 피드백을 제공할 수 있습니다.