
Contact Form, 누구나 만들지만
정말 "잘 만들었다"는 얘기를 들어본 적 있으신가요?
이번 글에서는 React Hook Form과 Zod를 기반으로,
제 포트폴리오의 Contact Form을 중심으로 실용적인 구조 설계법을 정리해보겠습니다.
흔하지만, 제대로 만들기 은근히 어려운 UI
Form은 보통 “입력 받고 전송하는 UI”로 생각되기 쉽습니다.
하지만 타입 안정성과 신뢰도를 고려하면 이야기가 달라집니다.
특히 Next.js에선 CSR/SSR 분리까지 신경 써야 하죠.
React Hook Form은 폼 상태 관리를
Zod는 스키마 기반 타입 검증에 특화되어 있습니다.
// ContactSchema.ts
import { z } from "zod";
export const ContactSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
subject: z.string().min(1),
message: z.string().min(1),
});
이 스키마를 zodResolver와 함께 RHF에 연결하면,
useForm({
resolver: zodResolver(ContactSchema),
})
폼 필드의 유효성 검사는 자동으로 처리되고
각 필드별 에러 메시지를 분리해 관리할 수 있어 일관된 UX 구성이 가능합니다.
Next.js의 Route Handler와 같은 기능을 사용해 API를 구성한다면,
클라이언트와 같은 스키마로 서버에서도 검증할 수 있어 타입 일관성이 유지됩니다.
// app/api/contact/route.ts
export async function POST(req: NextRequest) {
const json = await req.json();
// ContactSchema로 json 데이터 검증
const result = ContactSchema.safeParse(json);
if (!result.success) {
return new Response(JSON.stringify(result.error), { status: 400 });
}
// 실제 메일 전송 or DB 저장 로직…
return Response.json({ ok: true });
}
Form Submit 후 사용자에게 즉각적인 피드백을 주는 것도 중요합니다.
const { toast } = useToast();
const onSubmit = async (data: ContactFormValues) => {
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
if (process.env.NODE_ENV === "development") {
console.error("Error submitting form", errors);
}
return;
}
toast({
title: "Message sent successfully!",
description: "I will get back to you soon.",
});
reset();
};
// 에러 콜백함수 정의
const onError = (errors: FieldErrors<ContactFormValues>) => {
if (process.env.NODE_ENV === "development") {
console.error("Error submitting form", errors);
}
toast({
title: "Error sending message",
description: "Please check your input and try again.",
variant: "destructive",
});
};
저는 shadcn/ui의 useToast 훅으로 간단하게 알림을 제공했습니다.
전송 후 useForm의 reset으로 폼 초기화, formState.isSubmitting으로 버튼 상태 관리도 가능합니다
정리하면 이 구조는 다음과 같은 강점을 가집니다:
Form을 이렇게 설계한다면 신뢰도 높은 인터페이스를 완성할 수 있습니다.
입력 값이 누락되었을 경우, 다음과 같이 에러 응답을 확인할 수 있습니다!

📝 다음 글 예고
Notion API로 만드는 서버리스 Contact 시스템
생각보다 훨씬 강력합니다!