정적 사이트의 문의 폼

정적 사이트의 문의 폼4분 분량

Netlify Forms 없이 Netlify에 문의 폼 추가하기

Netlify Forms 없이 Netlify 사이트에서 문의 폼을 운영하세요. 빌드 시점 감지 없이 React에서 JSON을 POST하고, 배포 프리뷰용 폼을 따로 둡니다.

Preet SutharIkiform 창업자

폼에서 data-netlify를 빼고 action을 Ikiform 엔드포인트로 지정하세요. 그러면 Netlify는 페이지만 제공하고 POST에는 관여하지 않습니다. React와 Vue 폼은 숨겨진 HTML 사본이 필요 없고 JSON을 POST할 수 있습니다. 프리뷰 호스트는 Allowed domains에서 사이트와 일치하지 않으므로, netlify.toml에서 배포 프리뷰에 다른 폼 ID를 설정하세요.

Netlify에서 다른 점

Netlify는 주요 정적 호스트 중 유일하게 폼 기능이 내장되어 있습니다. Netlify Forms는 빌드할 때마다 배포된 HTML을 읽어 data-netlify나 netlify가 표시된 폼을 찾고, 그 폼에 대해서는 여러분의 사이트로 보낸 POST를 받습니다. 그 밖의 정적 페이지 POST는 모두 실패하며, Netlify에서는 405가 아니라 404로 실패합니다.

터미널
curl -i -X POST -d "message=hello" https://your-site.netlify.app/contact/
응답
HTTP/2 404
server: Netlify
cache-status: "Netlify Edge"; fwd=method; fwd-status=404
content-length: 0

이 404 때문에 사람들은 사라지지도 않은 페이지를 찾아 헤맵니다. 실제 의미는 감지된 폼 중 POST와 일치하는 것이 없다는 것입니다. Netlify Forms를 쓸지는 별개의 문제이며, Ikiform과 Netlify Forms 비교에서 솔직하게 답합니다. 사이트가 언제까지나 Netlify에 있고 폼이 일반 HTML이라면 Netlify Forms가 더 수고가 적습니다. 이 페이지는 그 밖의 경우를 위한 것입니다.

여러분의 상황Netlify Forms가 걸림돌이 되는 이유
폼을 React, Vue, Svelte로 렌더링함빌드는 정적 HTML을 파싱하므로, 클라이언트에서 렌더링하는 폼은 Netlify가 찾을 수 있게 숨겨진 사본이 필요합니다
fetch로 JSON을 POST하고 싶음Netlify Forms는 urlencoded와 multipart 본문만 받습니다
사이트를 다른 호스트로 옮길 수도 있음폼이 여러분의 도메인으로 POST하므로 Netlify가 그 도메인을 제공하는 동안에만 작동합니다
메시지가 직접 검증하는 웹훅으로 가야 함Ikiform은 모든 전달에 Standard Webhooks 헤더로 서명하고 재시도합니다
같은 Netlify 사이트에서 나란히 쓰는 Netlify Forms와 IkiformNetlify Forms 사용 시Ikiform 사용 시빌드Netlify가 HTML 스캔data-netlify?내 사이트로 POSTNetlify가 저장POST에 404폼 무시됨빌드스캔 불필요ikiform.com으로POSTHTML 또는 JSONIkiform저장, 이메일, 웹훅찾음못 찾음같은 Netlify 사이트에서 나란히 쓰는 Netlify Forms와 IkiformNetlify Forms 사용 시Ikiform 사용 시빌드Netlify가 HTML 스캔data-netlify?내 사이트로 POSTNetlify가 저장POST에404폼 무시됨빌드스캔 불필요ikiform.com으로POSTHTML 또는 JSONIkiform저장, 이메일, 웹훅찾음못 찾음
  1. Netlify Forms를 쓰면 Netlify가 배포마다 HTML을 스캔해 data-netlify가 표시된 폼을 찾습니다.
  2. 찾은 폼은 여러분의 사이트로 POST를 받고, Netlify가 이를 저장합니다.
  3. 숨겨진 사본 없이 JavaScript로 렌더링한 폼처럼 찾지 못한 폼은 모든 POST에서 404를 받습니다.
  4. Ikiform을 쓰면 빌드 시점에 아무것도 찾을 필요가 없습니다.
  5. 폼이 HTML이나 JSON으로 ikiform.com에 바로 POST하고, Ikiform이 저장하고 이메일로 알려 줍니다.
둘은 한 사이트에 함께 있을 수 있습니다. data-netlify가 있는 폼은 Netlify로, action이 Ikiform 엔드포인트인 폼은 Ikiform으로 갑니다.

Netlify Forms에서 폼 옮기기

  • 폼에서 data-netlify 또는 netlify, netlify-honeypot, data-netlify-recaptcha를 지우고, 숨겨진 form-name 입력도 지우세요. 남겨 두면 form-name이 일반 필드로 저장됩니다.
  • 아래와 같은 JavaScript 폼의 숨겨진 정적 사본을 지우세요. Netlify가 폼을 감지하는 데만 필요했던 것입니다.
  • action 또는 fetch URL을 Ikiform 엔드포인트로 설정하세요. 기존의 action="/thanks"는 폼의 Redirect after submit 설정으로 옮깁니다.
  • 먼저 Netlify의 Forms 페이지에서 지난 제출을 내려받으세요. 자동으로 옮겨지지 않습니다.
index.html, 이 부분 삭제
<form name="contact" netlify hidden>
  <input name="name" />
  <input name="email" />
  <textarea name="message"></textarea>
</form>

사이트의 다른 폼이 아직 Netlify Forms를 쓴다면 form detection을 켜 두세요. Netlify 문서에 따르면 감지를 끄는 것은 Netlify Forms를 쓰지 않는 사이트만을 위한 것입니다. 더 이상 쓰는 폼이 없다면 끄면 배포할 때마다 스캔을 하나 줄일 수 있습니다.

Netlify의 스팸 설정을 대신하는 것

Netlify는 제출에 Akismet을 실행하고 netlify-honeypot과 reCAPTCHA를 제공합니다. Ikiform에서는 폼의 Honeypot field를 bot-field처럼 이미 쓰는 이름으로 설정하면 허니팟이 계속 작동합니다. 스팸 점수는 설정 없이 모든 POST에 적용되고, Spam check를 Ikiform으로 설정하면 키 없이 reCAPTCHA 대신 Cloudflare Turnstile을 씁니다. 전환할 때 reCAPTCHA div를 지우세요.

JSON을 POST하는 React 폼

Netlify Forms가 끼어들지 않으면 클라이언트에서 렌더링하는 폼에 사본이 필요 없고, JSON을 보낼 수 있습니다. JSON은 타입을 유지하고 값을 먼저 다듬을 수 있게 해 줍니다. 이 예제는 어떤 Netlify 컨텍스트가 페이지를 빌드했는지도 기록합니다.

src/ContactForm.tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = `https://ikiform.com/f/${import.meta.env.VITE_IKIFORM_FORM_ID}`;

interface Message {
  name: string;
  email: string;
  message: string;
  context: string;
}

export function ContactForm() {
  const [sent, setSent] = useState(false);
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const body: Message & Record<string, string> = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      context: import.meta.env.VITE_CONTEXT ?? "local",
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
    setError("");
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify(body),
    }).catch(() => null);
    const result = await response?.json().catch(() => null);
    if (result?.ok) return setSent(true);
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    setError(Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.");
  }

  if (sent) return <p role="status">Got it. I'll answer from my own inbox, usually the same day.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} minLength={10} required />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button>Send</button>
    </form>
  );
}
  • Content-Type: application/json은 Ikiform에 JSON을 파싱하라고 알리고, Accept: application/json은 JSON 응답을 요청합니다.
  • VITE_CONTEXT는 예시 이름입니다. Netlify는 빌드 중에 CONTEXT를 설정합니다. 빌드 명령에서 Vite에 노출하거나, 이 필드를 빼세요.
  • 허니팟은 JSON 키로 보냅니다. 입력이 숨겨져 있어도 본문에 남겨 두어야 이를 채우는 봇이 스스로 드러납니다.

배포 프리뷰에는 별도의 폼

배포 프리뷰는 deploy-preview-12--your-site.netlify.app 같은 호스트에 있습니다. 이는 your-site.netlify.app의 하위 도메인이 아니므로 Allowed domains에서 일치하지 않고, 프리뷰 POST는 스팸으로 분류됩니다. netlify.toml에서 프리뷰와 브랜치 배포에 별도의 폼을 주고, 같은 파일에서 Content-Security-Policy 헤더를 설정하세요.

netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[context.production.environment]
  VITE_IKIFORM_FORM_ID = "frm_xxxxxxxxxxxxxxxx"

[context.deploy-preview.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[context.branch-deploy.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"

프리뷰 폼의 Allowed domains는 비워 두거나, 테스트하는 동안 정확한 프리뷰 호스트를 추가하세요. 프로덕션 폼에는 여러분의 도메인과 your-site.netlify.app을 넣습니다. netlify.toml의 헤더는 모든 배포 컨텍스트에 적용됩니다. _headers 파일을 선호한다면 게시 디렉터리에 들어가야 합니다.

Allowed domains에 netlify.app만 단독으로 추가하지 마세요. 다른 사람의 사이트에 있는 폼 복사본을 포함해 모든 Netlify 사이트가 일치하게 됩니다.

작동 확인하기

  1. 배포 프리뷰를 열어 제출하고, 메시지가 프리뷰 폼에 도착하는지 확인하세요.
  2. 병합한 뒤 프로덕션에서 제출하세요. 프로덕션 폼에 도착하고, Netlify의 Forms 페이지에는 새로운 것이 없는지 확인하세요.
  3. 알림 수신자마다 받은편지함을 확인하세요. 각자 필드가 담긴 이메일을 받고, 폼에 email 필드가 있으면 Reply가 제출한 사람에게 갑니다.
  4. 허니팟을 채워 _gotcha=bot으로 POST하세요. 여전히 성공 응답을 받지만, 제출은 스팸으로 저장되고 이메일이나 웹훅을 보내지 않습니다.
  5. 아무것도 오지 않았나요? 대시보드의 상태 필터를 Spam으로 바꿔 보세요. 스팸은 실제 제출과 같은 성공 응답을 받습니다.

주의할 점

문제해결 방법
제출 시 404POST가 여러분의 사이트로 갔습니다. 캐시된 빌드에 이전 action이 남아 있거나, fetch 호출이 아직 /로 POST합니다. 게시 디렉터리에서 form-name을 검색하세요.
제출이 여전히 Netlify에 나타남페이지의 어떤 폼이 아직 data-netlify나 netlify와 함께 여러분의 사이트로 POST하므로 Netlify가 계속 감지하고 저장합니다. 속성을 지우고, action을 Ikiform으로 설정하고, 다시 배포하세요.
프리뷰에서만 Ikiform이 404 not_found배포 프리뷰 컨텍스트에 VITE_IKIFORM_FORM_ID가 없어서 URL이 undefined로 끝납니다. [context.deploy-preview.environment] 아래에 추가하세요.
403 origin_not_allowed, 또는 모든 POST가 스팸으로 분류됨페이지의 도메인이 폼의 Allowed domains에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 localhost도 추가하세요. 등록한 도메인의 하위 도메인은 허용됩니다.
422 files_not_supported폼에 파일이 선택된 <input type="file">이 있습니다. 파일 업로드는 아직 지원하지 않습니다. 입력을 지우거나 대신 링크를 요청하세요.

질문

막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.