deep-dive2025-06-14·10 min·25/348

Next.js 서버 컴포넌트에서 form 처리 방법

React Server Components에서 form을 안전하게 처리하고 Server Actions를 활용하는 방법을 알아봅니다.

Next.js 서버 컴포넌트에서 form 처리 방법

Introduction

React Server Components(RSC)는 서버에서 렌더링되므로 기존 이벤트 핸들러를 사용할 수 없습니다. 이 글에서는 Server Actions를 활용하여 서버 컴포넌트에서 form을 안전하게 처리하는 방법을 다룹니다.

Environment

# 프로젝트 구조
my-app/
├── app/
│   ├── page.tsx
│   ├── actions.ts
│   └── layout.tsx
├── components/
│   └── Form.tsx
└── package.json

# 기술 스택
Next.js: 14.2.5
React: 18.3.1

Problem

서버 컴포넌트에서 이벤트 핸들러를 사용하면 에러가 발생합니다:

// ❌ 서버 컴포넌트에서 이벤트 핸들러 사용 불가
export default function ServerForm() {
  return (
    
{ e.preventDefault(); // 이 코드는 서버에서 실행되므로 작동하지 않음 console.log('Form submitted'); }}>
); }

Analysis

Server Actions는 서버 컴포넌트와 호환되는 form 처리 메커니즘입니다:

  1. use server 디렉티브로 서버 전용 함수 정의
  2. form의 action 속성에 Server Action 바인딩
  3. 클라이언트 자바스크립트 없이도 form 처리 가능

Solution

1. 기본 Server Action 구현

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createUser(formData: FormData) {
  const username = formData.get('username') as string;
  const email = formData.get('email') as string;
  
  // 데이터베이스 저장 로직
  console.log('Creating user:', { username, email });
  
  // 캐시 무효화
  revalidatePath('/');
  
  return { success: true, message: '사용자가 생성되었습니다.' };
}

2. 서버 컴포넌트에서 form 사용

// app/page.tsx
import { createUser } from './actions';

export default function HomePage() {
  return (
    

사용자 등록

); }

3. 폼 유효성 검사 통합

// lib/validations.ts
import { z } from 'zod';

export const userSchema = z.object({
  username: z.string()
    .min(3, '사용자명은 3자 이상이어야 합니다')
    .max(20, '사용자명은 20자 이하여야 합니다'),
  email: z.string()
    .email('올바른 이메일 형식이 아닙니다'),
  age: z.number()
    .min(18, '만 19세 이상만 가능합니다')
    .max(100, '나이를 확인해주세요'),
});

export type UserInput = z.infer;
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { userSchema } from '@/lib/validations';

export async function createUser(formData: FormData) {
  const rawData = {
    username: formData.get('username') as string,
    email: formData.get('email') as string,
    age: Number(formData.get('age')),
  };
  
  // Zod 유효성 검사
  const result = userSchema.safeParse(rawData);
  
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    };
  }
  
  // 데이터베이스 저장
  console.log('Creating user:', result.data);
  
  revalidatePath('/');
  
  return { success: true, message: '사용자가 생성되었습니다.' };
}

4. 클라이언트 컴포넌트에서 form 사용

// components/ClientForm.tsx
'use client';

import { useState } from 'react';
import { createUser } from '@/app/actions';

export function ClientForm() {
  const [state, setState] = useState<{
    success: boolean;
    message?: string;
    errors?: Record;
  } | null>(null);
  
  const [isPending, setIsPending] = useState(false);
  
  async function handleSubmit(formData: FormData) {
    setIsPending(true);
    
    try {
      const result = await createUser(formData);
      setState(result);
    } catch (error) {
      setState({
        success: false,
        message: '오류가 발생했습니다.',
      });
    } finally {
      setIsPending(false);
    }
  }
  
  return (
    
{state?.errors?.username && (

{state.errors.username[0]}

)}
{state?.errors?.email && (

{state.errors.email[0]}

)}
{state?.message && (

{state.message}

)}
); }

5. Progressively Enhanced Form

// components/ProgressiveForm.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useTransition } from 'react';

export function ProgressiveForm() {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  
  async function handleSubmit(formData: FormData) {
    startTransition(async () => {
      // JavaScript가 비활성화된 경우 form action이 직접 호출됨
      const response = await fetch('/api/users', {
        method: 'POST',
        body: formData,
      });
      
      if (response.ok) {
        router.refresh();
      }
    });
  }
  
  return (
    
); }

Lessons Learned

  1. Progressive Enhancement: JavaScript 없이도 form이 작동하도록 설계
  2. 서버 검증 필수: 클라이언트 검증만으로는 보안 취약점 존재
  3. 에러 처리: Server Action의 에러를 사용자에게 친절하게 전달
  4. 캐시 관리: revalidatePath로 데이터 변경 시 캐시 업데이트

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.