Updating Data

김동현·2026년 3월 4일

Next.js에서는 React의 서버 함수(Server Functions)를 사용해서 데이터를 업데이트할 수 있어요. 이 페이지에서는 서버 함수를 생성하는 방법호출하는 방법에 대해 쭉 살펴볼 거예요.


서버 함수가 무엇인가요? (What are Server Functions?)

서버 함수(Server Function) 는 오직 '서버'에서만 실행되는 비동기(asynchronous) 함수를 말해요. 클라이언트(브라우저)에서 네트워크 요청을 통해 이 함수들을 호출할 수 있기 때문에, 반드시 비동기 방식으로 동작해야 한답니다.

액션(action)이나 데이터 변형(mutation)이 일어나는 문맥에서는 이를 서버 액션(Server Actions) 이라고도 불러요.

💡 강사님의 꿀팁 (알아두면 좋아요!): > 서버 액션(Server Action)은 폼(form) 제출이나 데이터 변형을 처리하기 위해 '특정한 방식'으로 사용되는 서버 함수를 의미해요. 즉, '서버 함수'가 조금 더 넓은 의미의 단어랍니다. 실무에서는 보통 두 용어를 혼용해서 쓰기도 하지만, 폼 제출과 엮일 때 주로 서버 액션이라고 부른다고 이해하시면 편해요!

관례적으로 서버 액션은 startTransition과 함께 사용되는 비동기 함수예요. 이 동작은 함수가 다음과 같이 사용될 때 자동으로 일어납니다.

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

Next.js에서 서버 액션은 프레임워크의 캐싱(caching) 아키텍처와 아주 긴밀하게 통합되어 있어요. 액션이 호출되면, Next.js는 단 한 번의 서버 왕복(roundtrip)만으로 업데이트된 UI와 새로운 데이터를 동시에 반환할 수 있답니다. 성능 최적화에 엄청난 이점이 있죠!

보이지 않는 곳에서는 액션들이 HTTP POST 메서드를 사용하고 있어요. 오직 이 메서드만이 액션을 호출할 수 있답니다.


서버 함수 생성하기 (Creating Server Functions)

서버 함수는 use server 디렉티브(지시어)를 사용해서 정의할 수 있어요. 비동기(asynchronous) 함수의 맨 윗부분에 이 지시어를 배치해서 해당 함수를 서버 함수로 표시하거나, 아예 별도의 파일 맨 윗부분에 배치해서 그 파일에서 내보내는(export) 모든 함수를 서버 함수로 표시할 수도 있습니다.

👨‍🏫 강사의 부연 설명:
'use server'라고 적는 순간, Next.js는 이 함수를 위한 숨겨진 API 엔드포인트를 백그라운드에서 알아서 만들어줍니다. 예전처럼 데이터를 보내기 위해 따로 pages/api 폴더에 API 라우트를 만들고, fetch로 주소를 적어 호출하는 번거로운 과정을 거칠 필요가 없어지는 거죠! 엄청난 발전입니다.

export async function createPost(formData: FormData) {
  'use server'
  const title = formData.get('title')
  const content = formData.get('content')

  // 데이터 업데이트
  // 캐시 재검증(Revalidate)
}

export async function deletePost(formData: FormData) {
  'use server'
  const id = formData.get('id')

  // 데이터 업데이트
  // 캐시 재검증(Revalidate)
}
export async function createPost(formData) {
  'use server'
  const title = formData.get('title')
  const content = formData.get('content')

  // 데이터 업데이트
  // 캐시 재검증(Revalidate)
}

export async function deletePost(formData) {
  'use server'
  const id = formData.get('id')

  // 데이터 업데이트
  // 캐시 재검증(Revalidate)
}

서버 컴포넌트 (Server Components)

서버 함수는 함수 본문 맨 위에 "use server" 지시어를 추가함으로써 서버 컴포넌트 내부에 직접 인라인(inline)으로 작성할 수도 있어요.

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

💡 강사님의 꿀팁 (알아두면 좋아요!): > 서버 컴포넌트는 기본적으로 점진적 향상(progressive enhancement) 을 지원해요. 이게 무슨 뜻이냐면, 사용자의 브라우저에서 JavaScript가 아직 다 로딩되지 않았거나 심지어 비활성화되어 있는 환경이라 할지라도, 서버 액션을 호출하는 폼(form) 제출이 정상적으로 동작한다는 의미입니다. 웹 접근성과 안정성 면에서 아주 훌륭한 기능이죠.

클라이언트 컴포넌트 (Client Components)

클라이언트 컴포넌트 내부에서는 서버 함수를 '직접' 정의하는 건 불가능해요. 하지만, 맨 윗부분에 "use server" 지시어가 있는 파일에서 함수를 import(불러오기)해서 클라이언트 컴포넌트 안에서 호출하는 것은 가능합니다!

👨‍🏫 강사의 부연 설명:
왜 클라이언트 컴포넌트 안에서는 직접 못 만들까요? 클라이언트 컴포넌트는 사용자의 브라우저로 코드가 번들링 되어 전송됩니다. 거기에 서버에서만 돌아가야 할 보안 관련 코드(예: DB 비밀번호, 쿼리문)가 포함되면 아주 큰일 나겠죠? 그래서 서버 함수는 무조건 서버 컴포넌트 안이나 분리된 파일('use server' 명시)에 두고 가져와서(import) 써야 하는 거랍니다.

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

💡 강사님의 꿀팁 (알아두면 좋아요!): > 클라이언트 컴포넌트에서 폼이 서버 액션을 호출할 때, 만약 JavaScript가 아직 로드되지 않은 상태라면 폼 제출을 큐(queue, 대기열)에 넣어두고 나중에 하이드레이션(hydration)이 될 때 최우선으로 처리하게 됩니다. 그리고 하이드레이션이 끝난 후에는 폼을 제출해도 브라우저가 새로고침되지 않아요. 사용자 경험(UX)이 매끄럽게 유지되는 거죠!

Props로 액션 전달하기 (Passing actions as props)

서버 액션을 클라이언트 컴포넌트의 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>
}

서버 함수 호출하기 (Invoking Server Functions)

서버 함수를 호출하는 두 가지 주요 방법이 있어요.

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

💡 강사님의 꿀팁 (알아두면 좋아요!): > 서버 함수는 기본적으로 서버 측에서 데이터를 변경(mutation)하기 위해 설계되었어요. 현재 클라이언트는 서버 함수들을 한 번에 하나씩만 전송(dispatch)하고 기다립니다. 이건 내부 구현 방식이라 나중에 변경될 수도 있어요.
만약 데이터를 병렬로 가져와야(parallel data fetching) 한다면, 서버 컴포넌트의 데이터 페칭(data fetching) 기능을 사용하거나, 하나의 서버 함수 혹은 라우트 핸들러(Route Handler) 내부에서 병렬 작업을 수행하는 것이 좋습니다.

폼 (Forms)

React는 HTML <form> 요소를 확장해서, HTML action prop으로 서버 함수를 호출할 수 있도록 만들었어요.

폼 안에서 호출될 때, 함수는 자동으로 FormData 객체를 전달받게 됩니다. 네이티브 FormData 메서드들을 사용해서 아주 쉽게 데이터를 뽑아낼 수 있어요.

👨‍🏫 강사의 부연 설명:
과거 React에서는 input 값들을 모두 useState로 연결하고 onChange로 상태를 업데이트해줘야 했죠? 이젠 그럴 필요가 없어요! HTML 네이티브 FormData를 그대로 넘겨받기 때문에 코드가 엄청나게 간결해집니다. name 속성만 잘 맞춰주시면 돼요.

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

  // 데이터 업데이트
  // 캐시 재검증(Revalidate)
}
'use server'

export async function createPost(formData) {
  const title = formData.get('title')
  const content = formData.get('content')

  // 데이터 업데이트
  // 캐시 재검증(Revalidate)
}

이벤트 핸들러 (Event Handlers)

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

예제 (Examples)

대기 상태 보여주기 (Showing a pending state)

서버 함수가 실행되는 동안, 사용자에게 '처리 중'이라는 걸 알려주는 건 UX의 기본이죠. React의 useActionState 훅을 사용하면 로딩 인디케이터(스피너 같은 것들)를 쉽게 보여줄 수 있어요. 이 훅은 pending이라는 boolean(참/거짓) 값을 반환해 줍니다.

👨‍🏫 강사의 부연 설명:
이전 버전 React나 문서를 보셨다면 useFormStatus를 보신 적이 있을 텐데요. 최근에는 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>
  )
}
'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>
  )
}

새로고침하기 (Refreshing)

데이터를 변형(업데이트)한 후에는, 최신 데이터를 보여주기 위해 현재 페이지를 새로고침하고 싶을 수 있어요. 이럴 땐 서버 액션 내부에서 next/cacherefresh 함수를 호출하면 됩니다.

'use server'

import { refresh } from 'next/cache'

export async function updatePost(formData: FormData) {
  // 데이터 업데이트
  // ...

  refresh()
}
'use server'

import { refresh } from 'next/cache'

export async function updatePost(formData) {
  // 데이터 업데이트
  // ...

  refresh()
}

이 함수는 클라이언트 라우터를 새로고침해서 UI가 최신 상태를 반영하도록 보장해줘요. 단, refresh() 함수는 태그된(tagged) 데이터를 재검증(revalidate)하지는 않는다는 점을 기억하세요. 태그된 데이터를 재검증하려면 대신 updateTagrevalidateTag를 사용하셔야 합니다.

재검증하기 (Revalidating)

데이터를 업데이트한 뒤에는 서버 함수 내부에서 revalidatePathrevalidateTag를 호출해서 Next.js 캐시를 무효화하고 업데이트된 새로운 데이터를 보여줄 수 있어요.

👨‍🏫 강사의 부연 설명:
실무에서 게시글을 작성하고 나면 게시판 목록에 내 글이 바로 보여야겠죠? Next.js는 성능을 위해 페이지를 캐싱(저장)해두기 때문에, 새 글을 써도 예전 화면이 보일 수 있어요. 이때 revalidatePath('/posts')를 호출해주면 해당 경로의 캐시를 날려버리고 즉시 새로운 데이터를 가져와 화면을 그리게 됩니다. 아주 자주 쓰이는 핵심 패턴이에요!

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  'use server'
  // 데이터 업데이트
  // ...

  revalidatePath('/posts')
}
import { revalidatePath } from 'next/cache'

export async function createPost(formData) {
  'use server'
  // 데이터 업데이트
  // ...
  revalidatePath('/posts')
}

리다이렉트하기 (Redirecting)

업데이트를 수행한 뒤에 사용자를 다른 페이지로 이동(리다이렉트)시키고 싶을 때가 많죠. 이땐 서버 함수 안에서 redirect를 호출하면 됩니다.

'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  // 데이터 업데이트
  // ...

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

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData) {
  // 데이터 업데이트
  // ...

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

redirect를 호출하면 프레임워크가 처리하는 제어 흐름 예외(control-flow exception)가 발생(throws)합니다. 즉, redirect 밑에 작성된 코드는 실행되지 않아요. 만약 새로운 데이터가 반영된 채로 이동하고 싶다면, redirect를 호출하기 전에 반드시 revalidatePathrevalidateTag를 먼저 호출해주세요!

쿠키 (Cookies)

서버 액션 안에서 cookies API를 사용해 쿠키를 가져오고(get), 설정하고(set), 삭제(delete)할 수도 있어요. 로그인이나 세션 관리할 때 정말 유용합니다.

서버 액션 안에서 쿠키를 설정하거나 삭제(set or delete)하면, Next.js는 현재 페이지와 그 레이아웃들을 서버에서 다시 렌더링해서 UI가 새로운 쿠키 값을 즉각 반영하도록 만들어줍니다.

💡 강사님의 꿀팁 (알아두면 좋아요!): > 이때 서버 업데이트는 현재의 React 트리에 적용되며, 필요에 따라 컴포넌트들을 재렌더링하거나 마운트/언마운트 시킵니다. 이때 재렌더링되는 컴포넌트들의 클라이언트 상태(Client state)는 그대로 보존되며, 의존성(dependencies) 배열의 값이 변경된 useEffect 같은 이펙트들만 다시 실행된답니다. 사용자가 작성 중이던 다른 폼의 내용이 날아가지 않게 보호해 주는 똑똑한 기능이죠!

'use server'

import { cookies } from 'next/headers'

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

  // 쿠키 가져오기
  cookieStore.get('name')?.value

  // 쿠키 설정하기
  cookieStore.set('name', 'Delba')

  // 쿠키 삭제하기
  cookieStore.delete('name')
}
'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // 쿠키 가져오기
  const cookieStore = await cookies()

  // 쿠키 가져오기
  cookieStore.get('name')?.value

  // 쿠키 설정하기
  cookieStore.set('name', 'Delba')

  // 쿠키 삭제하기
  cookieStore.delete('name')
}

useEffect

컴포넌트가 화면에 마운트(mount)되거나 특정 의존성 값이 바뀔 때 서버 액션을 호출하려면, React의 useEffect 훅을 사용하면 됩니다.

글로벌 이벤트에 따라 데이터 변형이 필요하거나 자동으로 트리거되어야 하는 작업에 유용해요. 예를 들어 앱의 단축키를 위한 onKeyDown 이벤트, 무한 스크롤(infinite scrolling)을 구현하는 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>
}
'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)
    })
  }, [])

  // `isPending`을 사용해서 사용자에게 로딩 피드백을 줄 수 있어요
  return <p>Total Views: {views}</p>
}

API 레퍼런스 (API Reference)

이 페이지에서 언급된 기능들에 대해 더 자세히 알고 싶으시다면 아래 API 레퍼런스를 읽어보세요!

  • revalidatePath
    • revalidatePath 함수에 대한 API 레퍼런스입니다.
  • revalidateTag
    • revalidateTag 함수에 대한 API 레퍼런스입니다.
  • redirect
    • redirect 함수에 대한 API 레퍼런스입니다.

전체 문서의 의미론적인 개요(semantic overview)를 확인하시려면 https://nextjs.org/docs/sitemap.md를 참고해 주세요.

사용 가능한 모든 문서의 인덱스를 확인하시려면 https://nextjs.org/docs/llms.txt를 참고해 주세요.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글