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 => ( - {item.content} <deleteform content="{item.content}" id="{item.id}"></deleteform>
))}
)}
</main>
);
}
최적화 및 고급 팁
1. 성능 최적화
- 대량 데이터 처리: 여러 필드를 한 번에 처리하기 위해
formData.getAll()사용 고려. - 실시간 입력 최적화: 디바운싱(debouncing) 적용으로 불필요한 요청 감소.
2. 보안 최적화
- 항상 서버 측 검증: 클라이언트 측 검증은 부차적 역할로 활용.
- SQL 인젝션 방어: 매개변수화된 쿼리 사용.
3. 사용자 경험 개선
- 명확한 오류 메시지: 사용자가 문제를 쉽게 해결할 수 있도록 세부 정보 제공.
- 자동 포커스: 폼 제출 후 입력란 자동 선택.