iBetter Books
수정

Ch 04. 서버에서 실행되는 함수 — "use server"

주방으로 주문을 넣다

홀(클라이언트)에 앉아 있는 손님이 주방(서버)에 직접 전화를 걸 수 있다면 어떨까요. "김치찌개 하나 더 주세요"라고 말하면, 주방에서 바로 조리를 시작합니다. 손님은 앉은 자리에서 주방 함수를 호출한 셈입니다.

Server Actions가 이런 방식입니다. Client Component(브라우저)에서 서버 함수를 직접 호출할 수 있습니다. 별도의 API 엔드포인트를 만들 필요가 없습니다.

"use server" 선언의 의미

"use server"는 이 함수는 서버에서 실행된다는 선언입니다. 클라이언트에서 이 함수를 호출하면, 실제로는 서버에 HTTP 요청이 전송되고 서버에서 함수가 실행됩니다.

"use server"를 함수 안에 직접 쓰거나, 파일 최상단에 쓸 수 있습니다.

// 파일: app/actions/post.ts// 파일 최상단에 "use server"를 쓰면 이 파일의 모든 export 함수가 Server Action이 됩니다'use server';import { db } from '@/lib/db';import { revalidatePath } from 'next/cache';export async function createPost(title: string, content: string) {  await db.post.create({    data: {      title,      content,      publishedAt: new Date(),    },  });  revalidatePath('/posts'); // 게시글 목록 캐시 갱신}```text### form의 `action` prop에 Server Action 연결하기HTML `<form>`의 `action` prop에 Server Action을 넣을 수 있습니다. 폼이 제출되면 Server Action이 서버에서 실행됩니다.```tsx// 파일: app/posts/new/page.tsximport { createPost } from '@/actions/post';export default function NewPostPage() {  return (    <div>      <h1>새 글 작성</h1>      <form action={createPost}>        <div>          <label htmlFor="title">제목</label>          <input id="title" name="title" type="text" required />        </div>        <div>          <label htmlFor="content">내용</label>          <textarea id="content" name="content" required />        </div>        <button type="submit">저장</button>      </form>    </div>  );}```textServer Action은 `FormData`를 인자로 받습니다. `name` 속성으로 폼 데이터에 접근합니다.```ts// 파일: app/actions/post.ts'use server';import { db } from '@/lib/db';import { revalidatePath } from 'next/cache';import { redirect } from 'next/navigation';export async function createPost(formData: FormData) {  const title = formData.get('title') as string;  const content = formData.get('content') as string;  if (!title || !content) {    throw new Error('제목과 내용을 입력해주세요.');  }  const post = await db.post.create({    data: { title, content, publishedAt: new Date() },  });  revalidatePath('/posts');  redirect(`/posts/${post.slug}`);}```text### Server Action을 Client Component에서 호출하기버튼 클릭처럼 폼 제출이 아닌 경우에도 Server Action을 사용할 수 있습니다.```tsx// 파일: app/posts/[slug]/components/DeleteButton.tsx'use client';import { deletePost } from '@/actions/post';import { useTransition } from 'react';interface Props {  postId: number;}export default function DeleteButton({ postId }: Props) {  const [isPending, startTransition] = useTransition();  function handleDelete() {    startTransition(async () => {      await deletePost(postId);    });  }  return (    <button onClick={handleDelete} disabled={isPending}>      {isPending ? '삭제 중...' : '게시글 삭제'}    </button>  );}```text```ts// 파일: app/actions/post.ts (이전 코드에 추가)'use server';// ... 기존 코드 ...export async function deletePost(postId: number) {  await db.post.delete({ where: { id: postId } });  revalidatePath('/posts');  redirect('/posts');}

다음 챕터에서는

다음 챕터에서는 어떤 컴포넌트를 서버에 두고, 어떤 컴포넌트를 클라이언트에 두어야 하는지 판단하는 체크리스트를 정리합니다.