# Netlify Forms 한도: 제출 100건에서 일어나는 일과 해결 방법

> 레거시 Netlify 플랜은 사이트별 폼 제출을 계량해 자동으로 업그레이드하고, 크레딧 플랜은 사이트를 일시 중지할 수 있으니 폼을 백엔드로 옮기세요.

2026-10-25 게시 · https://ikiform.com/ko/guides/website-builder-forms/netlify-forms-limit

Netlify의 레거시 플랜에서는 사이트마다 월 100건의 폼 제출이 포함되며, 이를 넘으면 사이트가 자동으로 유료 Forms 등급으로 올라갑니다. 2025년 9월 이후 생성된 계정의 크레딧 기반 플랜에서는 폼이 무료지만, 크레딧이 떨어진 무료 팀은 폼을 포함한 모든 사이트가 일시 중지됩니다. 폼을 Ikiform으로 옮기려면 일반 HTML이든 어떤 프레임워크든 Netlify 속성을 제거하고 `action`을 설정하면 됩니다.

## Netlify에서 다른 점

Netlify는 사이트 빌더가 아니라 폼이 내장된 호스트입니다. 이 점이 한도와 해결 방법 모두를 결정합니다. 어떤 한도가 적용되는지는 계정 생성 시점에 따라 다릅니다.

| 계정 | Netlify 문서의 설명 | 제출이 많은 폼에 주는 의미 |
| --- | --- | --- |
| 레거시 플랜, 2025년 9월 4일 이전 생성 | Forms Level 0은 사이트당 월 100건의 제출과 10 MB의 업로드를 포함합니다. 이를 넘으면 사이트가 1,000건의 Forms Level 1로 올라가고 결제 주기 말에 청구됩니다. 그 이상은 추가 사용량 패키지가 500건씩 더합니다 | 스팸이 몰려오면 아무도 결정하지 않았는데 사이트가 유료 등급으로 올라갈 수 있습니다. 무료 레거시 등급에서는 한도에 도달하면 계정의 모든 사이트가 일시 중지됩니다 |
| 크레딧 기반 플랜, 그 이후 생성 | 폼 제출은 등급 없이 무료이고 무제한입니다 | 폼 자체에는 비용이 들지 않습니다. 하지만 무료 팀이 월 크레딧을 다 쓰면 Netlify가 모든 프로젝트를 일시 중지하고, 일시 중지된 프로젝트는 폼 제출을 받지 않습니다 |

Netlify 자체 문서는 레거시 업그레이드가 자동이며 사용량이 하위 등급보다 높은 동안에는 내려갈 수 없다고 분명히 밝힙니다. 결정하기 전에 Netlify 대시보드에서 팀의 플랜 유형과 사이트의 폼 사용량을 확인하세요.

사람들이 옮기는 두 번째 이유는 숫자와 무관합니다. Netlify Forms는 배포된 HTML을 읽는 방식으로 동작합니다.

- 폼은 배포 시점에 감지됩니다. React, Vue, Svelte가 렌더링하는 폼은 그 HTML에 존재하지 않으므로, 빌드가 찾을 수 있도록 숨겨진 정적 사본을 두고 `form-name` 필드와 함께 그곳으로 POST합니다.
- Next.js에서는 OpenNext 어댑터가 그 사본을 `public/`의 정적 파일로 요구하며, POST는 Next.js 페이지가 아니라 그 정적 파일로 가야 합니다. 사본 없이 React 코드에 `data-netlify`가 있으면 빌드가 실패합니다.
- 제출은 urlencoded나 multipart여야 합니다. JSON은 받지 않습니다.
- 폼은 사이트가 Netlify에 배포되어 있는 동안에만 동작합니다. 호스트를 옮기면 폼도 옮겨야 합니다.

다이어그램: 레거시 Netlify 사이트의 폼 사용량이 한 달 동안 늘어나는 방식과 Ikiform으로 POST하면 바뀌는 것

1. 레거시 플랜에서 사이트는 100건이 포함된 Forms Level 0으로 달을 시작합니다.
2. 101번째 제출이 도착합니다. Netlify 사용량 문서는 사이트의 사용량을 기준으로 설명하므로, 스팸이 몰려와 한도를 넘길 수도 있습니다.
3. Netlify가 사이트를 1,000건의 Forms Level 1로 올리고 주기 말에 청구합니다.
4. Level 1을 넘으면 추가 사용량 패키지가 500건씩 더합니다.
5. 폼이 Ikiform으로 POST하면 페이지는 여전히 Netlify에 있지만 POST는 Ikiform 엔드포인트로 갑니다.
6. Netlify Forms는 POST를 받지 않으므로 집계하거나 업그레이드할 것이 없습니다.

호스팅은 Netlify에 남습니다. 폼의 목적지와 함께 계량만 옮겨 갑니다.

## 스택별로 폼 옮기기

코드를 바꾸기 전에 Netlify 폼 이름마다 Ikiform 폼을 하나씩 만들고 **Honeypot field**를 `bot-field`로 설정하세요. 그러면 기존 숨겨진 입력이 이름 변경 없이 허니팟으로 계속 동작합니다. 스택을 고르세요.

**HTML**

Netlify의 일반 HTML은 변경이 가장 적습니다. Netlify 속성과 `form-name` 입력을 삭제하고, `action`이 엔드포인트를 가리키게 하고, 감사 페이지 경로를 전체 URL로 `_redirect`에 옮기세요. `bot-field` 입력은 허니팟으로 남습니다.

**수정 전** (`contact.html`)

```html
<form name="contact" method="POST" action="/thanks" data-netlify="true" netlify-honeypot="bot-field">
  <input type="hidden" name="form-name" value="contact" />
  <p hidden><input name="bot-field" /></p>
  <label>Email <input type="email" name="email" required /></label>
  <label>Topic
    <select name="topic">
      <option value="question">A question</option>
      <option value="bug">Something is broken</option>
      <option value="work">Working together</option>
    </select>
  </label>
  <label>Message <textarea name="message" required></textarea></label>
  <button type="submit">Send</button>
</form>
```

**수정 후** (`contact.html`)

```html
<form method="post" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx">
  <input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
  <label>Email <input type="email" name="email" required /></label>
  <label>Topic
    <select name="topic">
      <option value="question">A question</option>
      <option value="bug">Something is broken</option>
      <option value="work">Working together</option>
    </select>
  </label>
  <label>Message <textarea name="message" required></textarea></label>
  <input type="hidden" name="_redirect" value="https://your-site.netlify.app/thanks" />
  <button type="submit">Send</button>
</form>
```

**Next.js**

Netlify에서 Next.js 폼은 컴포넌트가 POST하는 `public/__forms.html` 같은 `public/`의 정적 사본을 통해서만 Netlify Forms와 동작했습니다. 그 파일을 삭제하고 `NETLIFY_NEXT_VERIFY_FORMS` 설정을 제거한 뒤 컴포넌트에서 Ikiform으로 POST하세요. `action` 덕분에 폼은 JavaScript 없이도 POST합니다.

`app/contact/contact-form.tsx`:

```tsx
"use client";

import { type FormEvent, useState } from "react";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export function ContactForm() {
  const [state, setState] = useState<"idle" | "sending" | "sent">("idle");
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setState("sending");
    setError("");
    try {
      const response = await fetch(ENDPOINT, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(event.currentTarget),
      });
      const result = await response.json();
      if (result.ok) return setState("sent");
      setError(result.error.fields?.message?.[0] ?? result.error.fields?.email?.[0] ?? result.error.message);
    } catch {
      setError("Couldn't send. Check your connection and try again.");
    }
    setState("idle");
  }

  if (state === "sent") return <p role="status">Thanks. We read every message and reply within a day.</p>;

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="topic">Topic</label>
      <select id="topic" name="topic" defaultValue="question">
        <option value="question">A question</option>
        <option value="bug">Something is broken</option>
        <option value="work">Working together</option>
      </select>
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={state === "sending"}>{state === "sending" ? "Sending" : "Send"}</button>
    </form>
  );
}
```

**Astro**

정적 Astro 페이지에는 JavaScript가 전혀 필요 없습니다. 폼에서 `data-netlify`를 제거하고 엔드포인트를 `action`으로 설정한 뒤, `astro.config.mjs`의 `site`로 리디렉션을 만드세요. 페이지는 사전 렌더링 상태로 둘 수 있습니다.

`src/pages/contact.astro`:

```astro
---
const endpoint = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const thanksUrl = new URL("/thanks", Astro.site).href;
---

<form method="post" action={endpoint}>
  <input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  <label for="topic">Topic</label>
  <select id="topic" name="topic">
    <option value="question">A question</option>
    <option value="bug">Something is broken</option>
    <option value="work">Working together</option>
  </select>
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>
  <input type="hidden" name="_redirect" value={thanksUrl} />
  <button>Send</button>
</form>
```

**TanStack Start**

Netlify의 TanStack Start 앱은 React로 폼을 렌더링하므로 Netlify의 배포 시 감지가 이를 볼 수 없었습니다. 대신 서버 함수로 POST하고 숨겨진 정적 사본을 없애세요. Netlify에서 사이트의 환경 변수에 `IKIFORM_SECRET_KEY`를 설정하세요. 함수가 브라우저의 `Origin` 헤더를 전달하므로, 워크스페이스에 플랜이 생기기 전에는 `localhost`의 POST가 무료 테스트로 집계됩니다.

`src/routes/contact.tsx`:

```tsx
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

const sendMessage = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      email: String(data.get("email") ?? "").trim(),
      topic: String(data.get("topic") ?? "question"),
      message: String(data.get("message") ?? "").trim(),
      "bot-field": String(data.get("bot-field") ?? ""),
    };
  })
  .handler(async ({ data }) => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    return result.ok ? { sent: true, error: "" } : { sent: false, error: String(result.error.message) };
  });

export const Route = createFileRoute("/contact")({ component: Contact });

function Contact() {
  const send = useServerFn(sendMessage);
  const [state, setState] = useState({ sent: false, error: "" });
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    try {
      setState(await send({ data: new FormData(event.currentTarget) }));
    } catch {
      setState({ sent: false, error: "Couldn't send. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (state.sent) return <p role="status">Thanks. We read every message and reply within a day.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="topic">Topic</label>
      <select id="topic" name="topic" defaultValue="question">
        <option value="question">A question</option>
        <option value="bug">Something is broken</option>
        <option value="work">Working together</option>
      </select>
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send"}</button>
    </form>
  );
}
```

**React Router**

프레임워크 모드의 React Router에서는 라우트의 `action`이 POST를 전달합니다. `FormData`가 그대로 전달되므로 `bot-field`가 Ikiform의 허니팟 확인에 도달하고, 브라우저의 `Origin` 헤더도 함께 가므로 `localhost`의 POST가 테스트로 집계됩니다. Netlify용으로 둔 정적 사본을 제거하세요.

`app/routes/contact.tsx`:

```tsx
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/contact";

export async function action({ request }: Route.ActionArgs) {
  const form = await request.formData();
  const origin = request.headers.get("origin");
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: form,
  });
  const result = await response.json();
  if (result.ok) return { sent: true, error: "" };
  return data({ sent: false, error: String(result.error.message) }, { status: response.status });
}

export default function Contact({ actionData }: Route.ComponentProps) {
  const sending = useNavigation().state === "submitting";
  if (actionData?.sent) return <p role="status">Thanks. We read every message and reply within a day.</p>;

  return (
    <Form method="post">
      <input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="topic">Topic</label>
      <select id="topic" name="topic" defaultValue="question">
        <option value="question">A question</option>
        <option value="bug">Something is broken</option>
        <option value="work">Working together</option>
      </select>
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required />
      {actionData?.error ? <p role="alert">{actionData.error}</p> : null}
      <button disabled={sending}>{sending ? "Sending" : "Send"}</button>
    </Form>
  );
}
```

**SvelteKit**

Netlify의 SvelteKit 페이지는 자체 폼 액션 없이 브라우저에서 바로 POST할 수 있습니다. `action` 속성 덕분에 하이드레이션 전에도 폼이 동작합니다. Netlify용으로 추가한 폼의 사전 렌더링 사본은 삭제하세요.

`src/routes/contact/+page.svelte`:

```svelte
<script lang="ts">
  const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

  let sent = $state(false);
  let sending = $state(false);
  let error = $state("");

  async function send(event: SubmitEvent) {
    event.preventDefault();
    sending = true;
    error = "";
    try {
      const response = await fetch(ENDPOINT, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(event.currentTarget as HTMLFormElement),
      });
      const result = await response.json();
      if (result.ok) sent = true;
      else error = result.error.message;
    } catch {
      error = "Couldn't send. Check your connection and try again.";
    }
    sending = false;
  }
</script>

{#if sent}
  <p role="status">Thanks. We read every message and reply within a day.</p>
{:else}
  <form method="post" action={ENDPOINT} onsubmit={send}>
    <input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />
    <label for="topic">Topic</label>
    <select id="topic" name="topic">
      <option value="question">A question</option>
      <option value="bug">Something is broken</option>
      <option value="work">Working together</option>
    </select>
    <label for="message">Message</label>
    <textarea id="message" name="message" required></textarea>
    {#if error}<p role="alert">{error}</p>{/if}
    <button disabled={sending}>{sending ? "Sending" : "Send"}</button>
  </form>
{/if}
```

**Nuxt**

Nuxt에서는 컴포넌트가 브라우저에서 `$fetch`로 POST하므로 배포할 서버 라우트가 없습니다. `$fetch`는 `2xx`가 아닌 응답에 예외를 던지며, Ikiform의 오류 봉투는 오류의 `data`에 있습니다. `public/`의 숨겨진 정적 폼과 `form-name` 필드를 제거하세요.

`app/components/ContactForm.vue`:

```vue
<script setup lang="ts">
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const sent = ref(false);
const sending = ref(false);
const error = ref("");

async function send(event: Event) {
  sending.value = true;
  error.value = "";
  try {
    await $fetch(ENDPOINT, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(event.target as HTMLFormElement),
    });
    sent.value = true;
  } catch (failure) {
    const data = (failure as { data?: { error?: { message?: string } } }).data;
    error.value = data?.error?.message ?? "Couldn't send. Check your connection and try again.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="sent" role="status">Thanks. We read every message and reply within a day.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />
    <label for="topic">Topic</label>
    <select id="topic" name="topic">
      <option value="question">A question</option>
      <option value="bug">Something is broken</option>
      <option value="work">Working together</option>
    </select>
    <label for="message">Message</label>
    <textarea id="message" name="message" required></textarea>
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Send" }}</button>
  </form>
</template>
```

**React**

Netlify의 React 싱글 페이지 앱은 `form-name` 필드와 함께 urlencoded 데이터를 `/`로 POST했고, 감지를 위해 `index.html`에 정적 사본이 필요했습니다. 이제는 `FormData`를 엔드포인트로 POST합니다. 요청만 바뀝니다.

**수정 전** (`ContactForm.tsx`)

```tsx
const body = new URLSearchParams(new FormData(event.currentTarget) as unknown as Record<string, string>);
body.set("form-name", "contact");
await fetch("/", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: body.toString(),
});
```

**수정 후** (`ContactForm.tsx`)

```tsx
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(event.currentTarget),
});
const result = await response.json();
if (!result.ok) setError(result.error.message);
```

Netlify로 POST하는 것이 없어지면 `index.html`에서 숨겨진 `<form netlify>`를 삭제하세요.

**Vue**

Vue 싱글 파일 컴포넌트에서 같은 브라우저 측 POST입니다. Netlify가 감지하도록 `index.html`에 둔 숨겨진 폼을 제거하세요.

`src/components/ContactForm.vue`:

```vue
<script setup lang="ts">
import { ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const state = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");

async function send(event: Event) {
  state.value = "sending";
  error.value = "";
  try {
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(event.target as HTMLFormElement),
    });
    const result = await response.json();
    if (result.ok) {
      state.value = "sent";
      return;
    }
    error.value = result.error.message;
  } catch {
    error.value = "Couldn't send. Check your connection and try again.";
  }
  state.value = "idle";
}
</script>

<template>
  <p v-if="state === 'sent'" role="status">Thanks. We read every message and reply within a day.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />
    <label for="topic">Topic</label>
    <select id="topic" name="topic">
      <option value="question">A question</option>
      <option value="bug">Something is broken</option>
      <option value="work">Working together</option>
    </select>
    <label for="message">Message</label>
    <textarea id="message" name="message" required></textarea>
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="state === 'sending'">{{ state === "sending" ? "Sending" : "Send" }}</button>
  </form>
</template>
```

> TanStack Start와 React Router 버전은 서버에서 POST합니다. 서버 측 버전은 시크릿 키로 POST하므로 Ikiform이 **Allowed domains**와 사람 확인을 건너뜁니다. 그러면 모든 POST가 서버의 IP에서 오고, 방문자당 분당 5건인 Standard 한도가 모든 방문자를 합쳐서 셉니다. 서버를 거쳐 POST하는 폼에는 Relaxed 또는 Custom [레이트 리밋](https://ikiform.com/ko/docs/rate-limits.md)을 고르고, `IKIFORM_SECRET_KEY`는 서버 전용 환경 변수에 두세요.

모든 버전이 Netlify에서 쓰던 필드 이름을 유지하므로 알림 이메일과 CSV 열이 익숙하게 보입니다. [Netlify Forms 비교](https://ikiform.com/alternatives/netlify-forms)에 `data-netlify-recaptcha`와 파일 입력을 포함한 전체 속성 대응표가 있습니다.

## 배포 미리보기와 허용 도메인

Netlify는 풀 리퀘스트마다 `deploy-preview-42--your-site.netlify.app` 같은 별도 주소를 줍니다. 이는 `your-site.netlify.app`의 서브도메인이 아니므로, 사이트 주소를 **Allowed domains**에 등록해도 미리보기는 포함되지 않고 미리보기의 POST는 `origin_mismatch`로 스팸에 들어갑니다.

- 프로덕션용으로 `your-site.netlify.app`과 커스텀 도메인을 추가하세요.
- 테스트하는 동안 특정 미리보기 호스트를 추가했다가 나중에 제거하세요.
- `netlify.app`만 단독으로 추가하지 마세요. Netlify의 모든 사이트가 내 폼으로 POST할 수 있게 됩니다.
- 또는 환경 변수로 미리보기가 허용 도메인이 없는 두 번째 Ikiform 폼을 가리키게 하고, 프로덕션은 엄격하게 유지하세요.

## 제출을 잃지 않고 전환하기

1. 사이트의 **Forms** 섹션에서 각 폼의 과거 제출을 CSV로 다운로드하세요. Ikiform에는 가져오기 기능이 없습니다.
2. 새 폼을 미리보기에 배포하고 테스트를 보내세요. Ikiform 대시보드에서 필드 이름을 확인하세요.
3. 프로덕션에 병합하고 실제 도메인에서 테스트를 한 번 더 보내세요.
4. Netlify 발신 웹훅을 Ikiform [웹훅](https://ikiform.com/ko/docs/webhooks.md)으로 바꾸세요. Netlify는 JWS 토큰으로 서명하고 Ikiform은 Standard Webhooks 헤더를 쓰므로 검증 코드도 바뀝니다.
5. 며칠 동안 Netlify에 도달한 POST가 없으면 사이트의 Forms 설정에서 폼 감지를 끄세요.

> Netlify는 파일 업로드를 받고 모든 제출에 Akismet을 실행합니다. 파일 업로드는 아직 지원하지 않습니다. 파일이 있는 POST는 `422 files_not_supported`를 받습니다. 폼에 파일이 필요하다면 그 폼 하나만 Netlify Forms에 두고 나머지를 옮기세요.

## Netlify에서 동작 확인하기

1. 폼의 **Allowed domains**에 사이트 도메인을 추가하고, 개발하는 동안에는 `localhost`도 추가하세요. 비워 두면 어디서든 POST를 받습니다.
2. 미리보기를 배포하고 그 호스트를 추가한 뒤 거기서 폼을 제출하세요.
3. 대시보드에서 **Submissions**를 여세요. 보낸 모든 필드가 담긴 새 제출이 있습니다.
4. `form-name`과 `bot-field` 없이 필드가 `email`, `topic`, `message`인지 확인하세요.
5. Netlify에서 사이트의 **Forms** 탭을 열어 테스트가 없는지 확인하세요.
6. `bot-field`를 채워서 한 번 POST하세요. 스팸으로 저장되고 아무것도 보내지 않습니다.
7. 알림 수신자 각각의 받은편지함을 확인하세요. 각자 필드가 담긴 이메일을 받고, 폼에 `email` 필드가 있으면 **Reply**는 제출자에게 갑니다.

## 주의할 점

| 문제 | 해결 방법 |
| --- | --- |
| Ikiform에 `form-name` 필드가 나타남 | 숨겨진 `form-name` 입력이 아직 폼에 있거나 코드가 여전히 설정합니다. 제거하세요. Ikiform은 URL로 폼을 식별합니다. |
| 폼을 언급하며 Next.js 빌드가 실패함 | React 코드에 `netlify`나 `data-netlify` 속성이 아직 있습니다. `public/__forms.html`과 함께 제거하세요. |
| 미리보기 POST가 스팸으로 들어감 | 미리보기 호스트가 **Allowed domains**에 없습니다. 테스트하는 동안 추가하거나 미리보기용 두 번째 폼을 쓰세요. |
| 실제 POST까지 모두 스팸으로 표시됨 | 입력은 `bot-field`인데 **Honeypot field**가 아직 `_gotcha`이거나, 그 반대입니다. 둘을 맞추세요. |
| `422 files_not_supported` | 폼에 파일이 선택된 `<input type="file">`이 있습니다. 파일 업로드는 아직 지원하지 않습니다. 입력을 지우거나 대신 링크를 요청하세요. |
| `429 rate_limited` | 한 방문자가 폼의 레이트 리밋보다 많이 보냈습니다. Standard는 방문자당 분당 5회를 허용합니다. `Retry-After`만큼 기다리거나, 공유 IP 하나에서 POST하는 폼이라면 Relaxed나 Custom을 고르세요. |

## 질문

### Netlify 사이트가 폼 제출 100건을 넘으면 어떻게 되나요?

레거시 플랜에서는 Netlify가 사이트를 월 1,000건이 포함된 Forms Level 1로 올리고 결제 주기 말에 청구합니다. 무료 레거시 등급에서는 한도에 도달하면 업그레이드할 때까지 계정의 모든 사이트가 일시 중지됩니다.

### 새 크레딧 기반 플랜에서 Netlify Forms는 무료인가요?

Netlify 문서에 따르면 크레딧 기반 플랜에서 폼 제출은 무료이고 무제한입니다. 문제는 사이트 자체입니다. 무료 팀이 월 크레딧을 다 쓰면 프로젝트가 일시 중지되고 폼 제출을 받지 않습니다.

### Netlify에서 계속 호스팅하면서 폼에는 Ikiform을 쓸 수 있나요?

네. Ikiform은 페이지가 어디에 호스팅되든 상관하지 않습니다. 폼에서 Netlify 속성을 제거하고 action을 Ikiform 엔드포인트로 설정하면 Netlify Forms는 더 이상 이를 보지 않습니다.

### React나 Next.js에서 여전히 숨겨진 정적 폼이 필요한가요?

아니요. 그 사본은 Netlify 배포 단계가 폼을 찾기 위해서만 존재합니다. Ikiform은 URL로 POST를 받으므로 사본을 삭제하고 컴포넌트에서 POST하세요.

### Ikiform은 무료인가요?

아니요, 무료 플랜은 없습니다. 테스트는 무료입니다. 결제 전에 localhost에서 테스트 제출을 매월 50건 보낼 수 있습니다. 실제로 운영하려면 Pro가 제출 500건에 월 $6부터이며, 한 번만 결제해 제출 10,000건을 $49에, 50,000건을 $149에 살 수도 있습니다. 크레딧은 만료되지 않습니다.

## 가이드 더 보기

- [빌더 폼 한계 넘어서기](https://ikiform.com/ko/guides/website-builder-forms.md): Webflow, Netlify, Carrd, Framer 폼이 한도나 빠진 기능에 부딪히면, 디자인한 폼은 그대로 두고 대신 폼 백엔드를 가리키게 하세요.
- [Webflow 제출 한도](https://ikiform.com/ko/guides/website-builder-forms/webflow-submission-limit.md): Webflow의 무료 Starter 플랜은 사이트당 폼 제출 50건 이후로 수집을 멈추고 내보낸 사이트는 아예 수집하지 않으니, 대신 Form Block이 Ikiform을 가리키게 하세요.
- [Framer 폼](https://ikiform.com/ko/guides/website-builder-forms/framer-forms.md): Framer 폼은 이미 이메일, Sheets, 웹훅으로 보냅니다. 저장되고 걸러진 제출, 팀 알림, 더 많은 목적지를 원한다면 Ikiform을 웹훅으로 추가하세요.
- [Carrd 폼](https://ikiform.com/ko/guides/website-builder-forms/carrd-forms.md): Carrd의 문의 폼은 이메일만 보내고 커스텀 폼에는 Pro Plus가 필요하므로, 모든 제출을 저장하려면 Plus에서는 Send to URL을, Standard에서는 HTML 임베드를 쓰세요.
