포트폴리오에 딱 맞는 Contact Form, React Hook Form + Zod로 타입 안정성 확보하기

Taehee_Hwang·2025년 4월 15일
0

Next.js

목록 보기
2/2
post-thumbnail

Contact Form, 누구나 만들지만
정말 "잘 만들었다"는 얘기를 들어본 적 있으신가요?

이번 글에서는 React Hook Form과 Zod를 기반으로,

  • 클라이언트와 서버 모두에서 타입 안전성을 유지하고
  • 검증 → 전송 → 피드백까지 자연스럽게 흐르는 UX를 설계하며
  • Next.js App Router 환경에서도 API를 안정적으로 구성하는 방법까지

제 포트폴리오의 Contact Form을 중심으로 실용적인 구조 설계법을 정리해보겠습니다.


왜 Contact Form인가?

흔하지만, 제대로 만들기 은근히 어려운 UI

Form은 보통 “입력 받고 전송하는 UI”로 생각되기 쉽습니다.
하지만 타입 안정성과 신뢰도를 고려하면 이야기가 달라집니다.
특히 Next.js에선 CSR/SSR 분리까지 신경 써야 하죠.


React Hook Form + Zod 조합

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 App Router에서 API 구성

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/uiuseToast 훅으로 간단하게 알림을 제공했습니다.
전송 후 useFormreset으로 폼 초기화, formState.isSubmitting으로 버튼 상태 관리도 가능합니다


마무리: 실무에서도 바로 쓸 수 있는 구조란?

정리하면 이 구조는 다음과 같은 강점을 가집니다:

  • 검증 → 전송 → 피드백까지 자연스러운 흐름
  • 클라이언트/서버 스키마 공유로 타입 안정성 확보
  • 기능 추가나 재사용이 쉬운 보일러플레이트 구조

Form을 이렇게 설계한다면 신뢰도 높은 인터페이스를 완성할 수 있습니다.

입력 값이 누락되었을 경우, 다음과 같이 에러 응답을 확인할 수 있습니다!


📝 다음 글 예고

Notion API로 만드는 서버리스 Contact 시스템

생각보다 훨씬 강력합니다!

0개의 댓글