🌐 웹개발

Server Actions

Server Actions

Next.js의 서버 함수 호출 기능. 폼 제출, 데이터 변경 간소화.

📖 상세 설명

Server Actions는 Next.js 13.4부터 도입된 기능으로, 클라이언트 컴포넌트에서 직접 서버 함수를 호출할 수 있게 해줍니다. 'use server' 지시어를 사용하여 함수가 서버에서만 실행되도록 표시하며, API 라우트를 만들지 않고도 데이터 변경이 가능합니다.

Server Actions의 가장 큰 장점은 폼 처리의 단순화입니다. form의 action 속성에 서버 함수를 직접 전달하면, JavaScript가 비활성화된 환경에서도 동작하는 Progressive Enhancement를 자동으로 지원합니다.

Server Actions는 RPC(Remote Procedure Call) 패턴을 React에 자연스럽게 통합합니다. 클라이언트 번들에 서버 코드가 포함되지 않아 보안이 강화되며, 데이터베이스 쿼리나 외부 API 호출을 컴포넌트와 같은 파일에서 정의할 수 있습니다.

revalidatePath나 revalidateTag와 함께 사용하면 mutation 후 캐시를 자동으로 갱신할 수 있습니다. useFormStatus, useFormState 훅과 결합하여 로딩 상태 표시나 낙관적 업데이트도 구현할 수 있습니다.

💻 코드 예제

// app/actions.ts - Server Actions 정의
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { db } from '@/lib/db';

// 스키마 검증
const PostSchema = z.object({
  title: z.string().min(1, '제목을 입력하세요'),
  content: z.string().min(10, '내용은 10자 이상이어야 합니다'),
});

// 폼 상태 타입
type FormState = {
  errors?: { title?: string[]; content?: string[] };
  message?: string;
};

// 게시글 생성 Action
export async function createPost(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  // 유효성 검사
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: '입력값을 확인해주세요.',
    };
  }

  try {
    await db.post.create({
      data: validatedFields.data,
    });
  } catch (error) {
    return { message: '게시글 작성에 실패했습니다.' };
  }

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

// 삭제 Action
export async function deletePost(id: string) {
  await db.post.delete({ where: { id } });
  revalidatePath('/posts');
}

// app/posts/new/page.tsx - 폼 컴포넌트
'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from '../actions';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? '저장 중...' : '게시하기'}
    </button>
  );
}

export default function NewPostForm() {
  const [state, formAction] = useFormState(createPost, {});

  return (
    <form action={formAction}>
      <input name="title" placeholder="제목" />
      {state.errors?.title && <p>{state.errors.title[0]}</p>}

      <textarea name="content" placeholder="내용" />
      {state.errors?.content && <p>{state.errors.content[0]}</p>}

      <SubmitButton />
      {state.message && <p>{state.message}</p>}
    </form>
  );
}

🗣️ 실무 대화 예시

기술 미팅에서:

"폼 제출용 API 라우트 따로 안 만들어도 돼요. Server Actions로 컴포넌트 파일에서 바로 서버 함수 정의하면 됩니다. 타입도 자동으로 공유되고요."

기술 면접에서:

"Server Actions의 보안상 장점은요?" - "'use server' 함수는 클라이언트 번들에 포함되지 않습니다. 서버에서만 실행되어 DB 연결 정보나 API 키가 노출되지 않아요."

코드 리뷰에서:

"Server Action에서 에러 throw하면 클라이언트에 에러 메시지 그대로 노출돼요. 에러 객체 반환하는 패턴으로 바꾸고, 민감한 정보는 로깅만 하세요."

⚠️ 주의사항

🔗 관련 용어

React Server Components Next.js Form Validation REST API Progressive Enhancement

📚 더 배우기

📄 Next.js 공식 문서 - Server Actions and Mutations 📄 React 공식 문서 - 'use server' 📄 Next.js - Forms and Mutations