Next.js를 활용한 효율적인 폼 처리: 검증에서 제출까지

Next.js를 활용한 효율적인 폼 처리: 검증에서 제출까지

최근 React 기반 애플리케이션에서 폼 처리는 중요한 주제로 자리잡고 있습니다. 특히 Next.js의 최신 버전에서는 App Router 구조를 통해 더욱 강력하고 간단한 폼 처리가 가능해졌습니다. 이 문서에서는 실시간 검증, 비동기 데이터 저장 및 오류 처리에 초점을 맞춘 방법을 안내합니다.

Next.js를 사용하는 이유

Next.js 13+ 버전부터 도입된 App Router는 전통적인 React 방식보다 더 나은 폼 처리 솔루션을 제공합니다:

  • Server Actions: 클라이언트 측에서 직접 서버 함수를 호출할 수 있어 별도의 API 엔드포인트 없이 작업이 가능합니다.
  • React Server Components: 클라이언트 사이드 JavaScript 양을 줄여 성능을 향상시킵니다.
  • 자동 상태 관리: 복잡한 상태 관리 로직을 단순화합니다.

다음 예제에서는 이러한 특징들을 바탕으로 폼 처리의 모든 과정을 다룹니다.

단계 1: 기본 폼 구성 요소 작성

먼저 app/ 디렉토리 아래에 기본 폼 구성 요소를 생성합니다. 여기서는 클라이언트 컴포넌트로 설정하며, Server Actions와 연결됩니다.

// app/form-component.tsx
"use client";

import { useFormState } from "react-dom";
import { saveData } from "./server-actions";

export function FormComponent() {
  const [state, formAction] = useFormState(saveData, { feedback: "" });

  return (
    <form action={formAction} className="flex gap-2">
      <input 
        type="text" 
        name="data" 
        placeholder="데이터 입력"
        className="flex-1 p-2 border rounded"
      />
      <button 
        type="submit"
        className="p-2 bg-green-500 text-white rounded"
      >
        저장
      </button>
      {state.feedback && <p className="text-red-600">{state.feedback}</p>}
    </form>
  );
}

이 컴포넌트는 다음과 같은 기능을 포함합니다:

  • 클라이언트 컴포넌트로 표시됨.
  • Server Actions과 연결된 useFormState 활용.
  • 필수 입력 필드와 오류 메시지 표시.

단계 2: 서버 측 데이터 검증 및 처리

서버 동작(Server Actions)은 폼 데이터를 안전하게 처리하는 핵심입니다. Zod 라이브러리를 사용하여 데이터 유효성을 체크합니다.

// app/server-actions.ts
"use server";

import { revalidatePath } from "next/cache";
import sql from "postgres";
import { z } from "zod";

const db = sql(process.env.DATABASE_URL!, { ssl: "allow" });

export async function saveData(
  prevState: { feedback: string },
  formData: FormData
) {
  const schema = z.object({
    data: z.string().min(1, "데이터를 입력하세요").max(100, "데이터는 100자 이하로 입력하세요"),
  });

  const parsed = schema.safeParse({ data: formData.get("data") });
  
  if (!parsed.success) {
    const errorMessage = parsed.error.issues.map(issue => issue.message).join(", ");
    return { feedback: errorMessage };
  }

  try {
    await db`INSERT INTO items (content) VALUES (${parsed.data.data})`;
    revalidatePath("/");
    return { feedback: "저장되었습니다!" };
  } catch (error) {
    return { feedback: "데이터베이스 오류: 다시 시도하세요." };
  }
}

위 코드는 다음 기능을 제공합니다:

  • Zod를 통한 데이터 검증.
  • PostgreSQL과의 상호 작용.
  • 경로 재검증을 통해 데이터 새로고침.

단계 3: 상태 관리 및 사용자 피드백

useFormState와 useFormStatus를 활용하여 사용자 경험을 개선할 수 있습니다.

// app/delete-form.tsx
"use client";

import { useFormState, useFormStatus } from "react-dom";
import { removeItem } from "./server-actions";

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button 
      type="submit" 
      disabled={pending} 
      className="ml-2 text-red-600 hover:text-red-800"
    >
      {pending ? "삭제 중..." : "삭제"}
    </button>
  );
}

export function DeleteForm({ id, content }: { id: number; content: string }) {
  const [state, formAction] = useFormState(removeItem, { feedback: "" });

  return (
    <form action={formAction} className="inline">
      <input type="hidden" name="id" value={id} />
      <SubmitButton />
      {state.feedback && <span className="text-red-600 ml-2">{state.feedback}</span>}
    </form>
  );
}

이 삭제 폼은 다음과 같이 작동합니다:

  • 현재 진행 상태를 표시하는 로딩 메시지 제공.
  • 중복 제출 방지를 위한 버튼 비활성화.
  • 결과 메시지 표시.

전체 페이지 통합

마지막으로 모든 폼을 하나의 페이지에 통합합니다.

// app/page.tsx
import { FormComponent } from "./form-component";
import { DeleteForm } from "./delete-form";
import sql from "postgres";

const db = sql(process.env.DATABASE_URL!, { ssl: "allow" });

export default async function HomePage() {
  const items = await db`SELECT * FROM items ORDER BY id DESC`;

  return (
    <main className="max-w-lg mx-auto p-4">
      <h1 className="text-xl font-bold mb-4">데이터 관리 시스템</h1>
      
      <FormComponent />
      
      {items.length === 0 ? (
        <p className="mt-4 text-gray-500">아직 데이터가 없습니다.</p>
      ) : (
         {items.map(item =&gt; ( - {item.content} <deleteform content="{item.content}" id="{item.id}"></deleteform>
 ))} 
      )}
    </main>
  );
}

최적화 및 고급 팁

1. 성능 최적화

  • 대량 데이터 처리: 여러 필드를 한 번에 처리하기 위해 formData.getAll() 사용 고려.
  • 실시간 입력 최적화: 디바운싱(debouncing) 적용으로 불필요한 요청 감소.

2. 보안 최적화

  • 항상 서버 측 검증: 클라이언트 측 검증은 부차적 역할로 활용.
  • SQL 인젝션 방어: 매개변수화된 쿼리 사용.

3. 사용자 경험 개선

  • 명확한 오류 메시지: 사용자가 문제를 쉽게 해결할 수 있도록 세부 정보 제공.
  • 자동 포커스: 폼 제출 후 입력란 자동 선택.

태그: next.js ServerActions Zod

8월 16일 19:15에 게시됨