Next.js에서 문의 양식 만들기 (백엔드 없이)

발행: (2026년 3월 26일 AM 10:30 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

Next.js에서 백엔드 없이 연락처 양식 만들기 커버 이미지

Long N.

옵션, 빠르게

“Next.js contact form”을 검색하면 보통 세 가지 접근 방식을 보게 됩니다:

  1. Route Handler + email API (Resend, SendGrid 등) – 완전한 제어가 가능하지만 모든 것을 직접 관리해야 합니다.
  2. Server Actions – 더 깔끔한 개발자 경험(DX)을 제공하지만 이메일 + 스팸 처리를 직접 해야 합니다.
  3. Form‑backend 서비스 – 데이터를 호스팅된 엔드포인트로 전송하면 저장 + 이메일을 서비스가 처리합니다.

대부분의 프로젝트—랜딩 페이지, SaaS 사이트, 정적 앱—에서는 옵션 3이 가장 빠른 경로입니다. 이 가이드는 해당 접근 방식을 단계별로 안내합니다.

가장 간단한 버전: HTML만 사용

로드 상태나 인터랙티브 기능이 필요하지 않다면, JavaScript 없이 순수 HTML 폼만 사용할 수 있습니다:

<form action="https://formtorch.com/f/YOUR_FORM_ID" method="POST">
  <input type="text" name="name" placeholder="Name" required />
  <input type="email" name="email" placeholder="Email" required />
  <textarea name="message" placeholder="Message" required></textarea>
  <button type="submit">Send message</button>
</form>

이미 동작하는 폼입니다. 브라우저가 제출을 처리하고, 이후에 리디렉션됩니다.

자체 페이지로 리디렉션

<form action="/thank-you" method="POST">
  <!-- fields -->
</form>

많은 경우에 이것만으로 충분합니다. 하지만 React 앱은 보통 좀 더 인터랙티브한 기능을 원합니다.

적절한 React 연락 양식

다음은 로딩, 성공, 오류 상태를 포함한 프로덕션 수준의 컴포넌트입니다:

"use client";

import { useState } from "react";

type FormState = "idle" | "loading" | "success" | "error";

export function ContactForm() {
  const [state, setState] = useState<FormState>("idle");

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setState("loading");

    try {
      const data = new FormData(e.currentTarget);

      const res = await fetch("https://formtorch.com/f/YOUR_FORM_ID", {
        method: "POST",
        headers: { "X-Requested-With": "XMLHttpRequest" },
        body: data,
      });

      if (!res.ok) throw new Error();
      setState("success");
    } catch {
      setState("error");
    }
  }

  if (state === "success") {
    return (
      <div>
        <h3>Message sent.</h3>
        <p>Thanks for reaching out. I’ll get back to you soon.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label>
        Name
        <input type="text" name="name" required />
      </label>

      <label>
        Email
        <input type="email" name="email" required />
      </label>

      <label>
        Message
        <textarea name="message" required></textarea>
      </label>

      {state === "error" && <p>Something went wrong. Try again.</p>}

      <button type="submit" disabled={state === "loading"}>
        {state === "loading" ? "Sending…" : "Send message"}
      </button>
    </form>
  );
}

왜 이렇게 구현하면 좋은가

  • FormData 대신 JSON 사용: 모든 필드(파일 포함)를 자동으로 캡처합니다.
  • X-Requested-With 헤더: 응답이 HTML 리다이렉트가 아닌 JSON이 되도록 보장합니다.
  • 로딩 상태 + 비활성화된 버튼: 중복 제출을 방지합니다.

유효성 검사 추가

구조를 변경하지 않고 클라이언트 측 유효성 검사를 추가할 수 있습니다:

function validate(data: FormData) {
  const errors: Record<string, string> = {};

  if (!data.get("name")) errors.name = "Name is required";

  const email = String(data.get("email") ?? "");
  if (!email) errors.email = "Email is required";
  else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
    errors.email = "Invalid email";

  if (!data.get("message")) errors.message = "Message is required";

  return errors;
}

브라우저의 기본 UI를 비활성화하고 오류를 직접 표시하려면 <form> 요소에 noValidate를 추가하세요.

react-hook-form 사용하기

보다 복잡한 폼에서는 react-hook-form이 상태 관리를 간소화합니다:

import { useForm } from "react-hook-form";

export function ContactForm() {
  const { register, handleSubmit, formState } = useForm();

  async function onSubmit(data: any) {
    const formData = new FormData();
    Object.entries(data).forEach(([k, v]) => formData.append(k, String(v)));

    await fetch("https://formtorch.com/f/YOUR_FORM_ID", {
      method: "POST",
      headers: { "X-Requested-With": "XMLHttpRequest" },
      body: formData,
    });
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <input {...register("name")} placeholder="Name" required />
      <input {...register("email")} type="email" placeholder="Email" required />
      <textarea {...register("message")} placeholder="Message" required></textarea>

      <button type="submit" disabled={formState.isSubmitting}>
        {formState.isSubmitting ? "Sending…" : "Send"}
      </button>
    </form>
  );
}

Formtorch와 같은 호스팅된 폼 백엔드를 사용하면, 맞춤 API나 서버‑사이드 이메일 처리, 추가 인프라 관리 없이도 몇 분 안에 완전한 기능의 연락 폼을 구축하고 실행할 수 있습니다. 즐거운 코딩 되세요!

스팸은 어떻게 할까?

모든 공개 양식은 스팸을 받습니다.

일반적인 접근법

  • CAPTCHA (사용자 경험 저하)
  • 허니팟
  • 속도 제한
  • 스팸 감지

간단한 허니팟 필드:

<input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off" />

Bots fill it. Humans don’t.

핵심 요약

백엔드를 구축하지 않아도 연락처 양식 이메일을 보낼 수 있습니다.

실용적인 설정

  • 간단한 페이지 → HTML 폼
  • React 앱 → fetch + FormData
  • 복잡한 폼 → react-hook-form

그 외의 모든 것은 단순히 배관일 뿐입니다.

선택 사항: 백엔드 완전 생략

이메일 API, 스팸 필터링, 제출물 저장 등을 다루고 싶지 않다면, 호스팅된 폼 백엔드를 사용할 수 있습니다.

예를 들어, Formtorch는 다음을 제공합니다:

  • 바로 사용할 수 있는 엔드포인트
  • 이메일 알림
  • 내장 스팸 필터링

따라서 서버 코드를 전혀 사용하지 않고도 폼이 작동합니다.

사용해 보고 싶다면:

👉 Formtorch

어쨌든 흐름을 이해하면, 연락 폼이 “설정 작업”이 아니라 해결된 문제로 바뀝니다.

0 조회
Back to Blog

관련 글

더 보기 »

HTML 파트 3

Label A는 사용자에게 입력 상자에 무엇을 입력해야 하는지 알려줍니다. 라벨과 입력을 for와 id를 사용해 연결하는 것이 가장 좋습니다. 사용자가 라벨을 클릭하면, 연결된…