# 어떤 랜딩 페이지에든 일반 HTML로 대기자 명단 폼 추가하는 방법

> 빌드 없이 일반 HTML로 만든 완전한 대기자 명단 페이지로, JavaScript가 꺼져도 작동하고, 스크립트가 로드되면 인라인 감사 문구를 보여 주며, 필요할 때는 기본 방식으로 돌아갑니다.

2026-10-25 게시 · https://ikiform.com/ko/guides/waitlist/html

이메일을 Ikiform 엔드포인트로 POST하고 감사 페이지로 리디렉션하는 일반 HTML 폼을 작성하세요. 그러면 JavaScript 없이 어떤 호스트에서든 작동합니다. 그런 다음 같은 폼을 `fetch`로 POST해 감사 문구를 인라인으로 보여 주고, 사람 확인이 켜져 있을 때처럼 스크립트가 끝까지 처리할 수 없으면 일반 POST로 되돌리는 작은 스크립트를 추가하세요.

대기자 명단 페이지에는 프레임워크도, 빌드 단계도, 서버도 필요 없습니다. 정적 파일 네 개와 폼 백엔드면 충분하고, 빌더가 내보내는 어떤 것보다 빨리 로드됩니다. 이 페이지는 층을 하나씩 쌓아 전체를 만들며, 각 층은 그 자체로 작동합니다.

## 빌드 단계가 없을 때 중요한 것

- **폼 자체가 연동입니다.** `action`이 있는 일반 `<form>`이 Ikiform과의 연결 전부입니다. 패키지도, API 키도, 컴파일할 것도 없습니다.
- **JavaScript는 필수가 아니라 개선입니다.** 스크립트가 차단되거나, 느리거나, 깨져도 페이지는 가입을 받아야 합니다. 아래 각 층은 이전 층에 기능을 더하고, 실패하면 이전 층으로 돌아갑니다.
- **스크립트가 사람을 가둬서는 안 됩니다.** `fetch`가 실패하거나 Ikiform이 `fetch`로는 실행할 수 없는 사람 확인을 요구하면, 스크립트는 폼을 브라우저에 돌려줘 일반 방식으로 POST하게 합니다.
- **호스팅은 어디든 됩니다.** GitHub Pages, Netlify, Cloudflare Pages, S3 버킷, 공유 호스팅 모두 이 파일을 그대로 제공합니다.

| 층 | 더하는 것 | 없을 때 |
| --- | --- | --- |
| HTML 폼 | 이메일을 POST하고 `thanks.html`로 리디렉션 | 아무것도 작동하지 않으므로 가장 먼저 |
| CSS | 라이트 모드와 다크 모드에서 읽기 좋은 페이지 | 수수하지만 작동하는 폼 |
| 스크립트 | 인라인 감사 문구, 필드 오류, 페이지 이동 없음 | 일반 POST와 리디렉션 |

다이어그램: 일반 HTML 대기자 명단 페이지에서 가입이 거칠 수 있는 세 가지 경로

1. JavaScript가 꺼져 있으면 브라우저가 폼을 POST하고, Ikiform이 303으로 _redirect URL로 리디렉션하며, 방문자는 thanks.html을 봅니다.
2. 스크립트가 로드되면 fetch로 POST하고 JSON을 요청합니다. Ikiform은 201이나 오류로 응답하고, 스크립트가 같은 페이지에 결과를 보여 줍니다.
3. fetch가 실패하거나 Ikiform이 challenge_required로 응답하면 스크립트가 form.submit()을 호출합니다.
4. 그 일반 POST는 필요하면 Ikiform의 사람 확인 페이지를 거쳐 thanks.html에서 끝납니다.

모든 경로는 가입이 저장되는 것으로 끝납니다. 스크립트는 방문자에게 보이는 방식만 바꿉니다.

## 파일

### 페이지

`index.html`:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Acme is almost here</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <h1>Acme is almost here</h1>
    <p>Invoices that chase themselves. Leave your email and we'll invite you first.</p>

    <form id="waitlist" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
      <label for="email">Email</label>
      <div class="row">
        <input id="email" name="email" type="email" autocomplete="email" required placeholder="you@company.com">
        <button>Join the waitlist</button>
      </div>
      <input type="hidden" name="_redirect" value="https://example.com/thanks.html">
      <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
      <p class="message" role="status" aria-live="polite"></p>
    </form>
  </main>
  <script src="waitlist.js" defer></script>
</body>
</html>
```

- `_redirect`는 일반 POST 후 브라우저를 `thanks.html`로 보냅니다. 폼의 **Allowed domains** 중 하나에 있는 `https:` URL일 때만 작동하므로 그곳에 도메인을 추가하세요. 폼 설정의 **Redirect after submit**을 쓰면 숨겨진 입력 없이 같은 일을 합니다.
- `_gotcha` 입력은 허니팟입니다. 사람에게는 보이지 않고, 모든 입력을 채우는 봇은 스팸으로 저장됩니다.
- `aria-live="polite"`는 포커스를 옮기지 않고 스크린 리더가 스크립트의 메시지를 읽어 주게 합니다.
- script 태그에 `defer`를 쓰므로 폼이 생긴 뒤에 실행되고 페이지 그리기를 막지 않습니다.

### 기능 개선

`waitlist.js`:

```js
const form = document.getElementById("waitlist");
const message = form.querySelector(".message");
const button = form.querySelector("button");
const key = crypto.randomUUID();

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  button.disabled = true;
  message.textContent = "Joining…";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json", "Idempotency-Key": key },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.replaceChildren(Object.assign(document.createElement("p"), {
        textContent: "You're on the list. We'll email you on launch day.",
        role: "status",
      }));
      return;
    }
    if (result.error.code === "challenge_required") {
      form.submit();
      return;
    }
    const wait = response.headers.get("Retry-After");
    message.textContent = wait
      ? `Too many tries. Wait ${wait} seconds and try again.`
      : result.error.fields?.email?.[0] ?? result.error.message;
  } catch {
    form.submit();
    return;
  }
  button.disabled = false;
});
```

- `Accept: application/json`은 Ikiform이 리디렉션 대신 JSON으로 응답하게 해서, 스크립트가 결과를 제자리에 보여 줄 수 있게 합니다.
- 멱등성 키는 페이지를 불러올 때마다 한 번 만들어집니다. 더블 클릭하거나 응답이 느려 재시도해도 가입은 하나만 저장됩니다.
- `challenge_required`는 폼의 스팸 검사가 **Ikiform**으로 설정되어 있는데 `fetch`가 토큰을 보내지 않았다는 뜻입니다. `form.submit()`은 일반 방식으로 POST해 Ikiform의 확인 페이지를 보여 준 뒤 감사 페이지로 넘어갑니다. `submit` 이벤트를 다시 발생시키지 않으므로 무한 반복은 없습니다.
- 네트워크 오류도 같은 폴백을 탑니다. Ikiform에 연결할 수 없었다면 일반 POST가 브라우저 자체 오류를 보여 주는데, 멈춘 버튼보다는 여전히 솔직합니다.
- `429`가 나면 `Retry-After`를 읽어 얼마나 기다려야 하는지 알려 줍니다.

### 감사 페이지

`thanks.html`:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>You're on the list</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <h1>You're on the list</h1>
    <p>We'll email you the day Acme opens. Nothing before that.</p>
    <p><a href="/">Back to the page</a></p>
  </main>
</body>
</html>
```

### 스타일

`style.css`:

```text
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; color: #111; background: #fff; }
main { max-width: 34rem; margin: 0 auto; padding: 18vh 1rem 4rem; }
h1 { font-size: 2.25rem; line-height: 1.1; margin: 0 0 0.75rem; }
label { display: block; font-weight: 600; margin: 1.5rem 0 0.375rem; }
.row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
input[type="email"] { flex: 1 1 14rem; font: inherit; padding: 0.625rem 0.75rem; border: 1px solid #ccc; border-radius: 0.5rem; }
button { font: inherit; padding: 0.625rem 1rem; border: 0; border-radius: 0.5rem; background: #111; color: #fff; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: progress; }
.message { min-height: 1.5em; margin: 0.5rem 0 0; }
@media (prefers-color-scheme: dark) {
  body { color: #eee; background: #111; }
  input[type="email"] { background: #1b1b1b; color: inherit; border-color: #444; }
  button { background: #eee; color: #111; }
}
```

> 페이지가 `example.com`이 아닌 다른 도메인에 있다면 `_redirect`와 돌아가기 링크를 바꾸세요. Ikiform은 따를 수 없는 `_redirect`를 무시하고 자체 호스팅 감사 페이지를 보여 주므로, 값이 맞지 않아도 가입을 잃지는 않습니다.

## 배포하고 테스트하기

파일 네 개를 한 폴더에 넣고 아무 정적 호스트에 올리세요. 그런 다음 **Allowed domains**에 호스트의 도메인을 추가하세요. 예를 들어 `username.github.io`, `your-site.netlify.app` 또는 내 도메인입니다. 로컬에서 작업하는 동안에는 `localhost`도 추가하세요. `file://` URL로 연 페이지는 출처를 보내지 않으므로, 아무 정적 서버로 폴더를 제공하세요.

## 작동 확인하기

1. 폼의 **Allowed domains**에 사이트 도메인을 추가하고, 개발 중에는 `localhost`도 추가하세요. 비워 두면 어디서 온 POST든 받습니다.
2. 브라우저에서 JavaScript를 끄고 가입하세요. `thanks.html`로 이동합니다.
3. 다시 켜고 가입하세요. 페이지를 떠나지 않고 폼이 감사 문구로 바뀝니다.
4. **Spam check**를 **Ikiform**으로 설정하고 한 번 더 가입하세요. 페이지가 확인 단계로 넘어갔다가 `thanks.html`로 이동합니다.
5. 대시보드에서 **Submissions**를 여세요. 보낸 모든 필드가 담긴 새 제출이 있습니다.
6. 허니팟을 채워서 `_gotcha=bot`으로 POST해 보세요. 여전히 성공 응답을 받지만, 제출은 스팸으로 저장되고 이메일이나 웹훅을 보내지 않습니다.

## 주의할 점

| 문제 | 해결 방법 |
| --- | --- |
| 브라우저가 내 감사 페이지 대신 Ikiform의 감사 페이지로 이동함 | `_redirect`는 도메인이 **Allowed domains**에 있고 `https:` URL일 때만 적용됩니다. 도메인을 추가하거나, 대신 폼 설정에서 **Redirect after submit**을 지정하세요. |
| `403 origin_not_allowed`이거나 모든 POST가 스팸으로 분류됨 | 페이지의 도메인이 폼의 **Allowed domains**에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 `localhost`도 추가하세요. 등록된 도메인의 하위 도메인은 허용됩니다. |
| 파일로 연 페이지의 POST가 거부되거나 스팸으로 표시됨 | `file://` 페이지는 출처를 보내지 않습니다. 로컬 정적 서버로 폴더를 제공하고 **Allowed domains**에 `localhost`를 추가하세요. |
| 감사 문구가 나타나지 않지만 가입은 들어옴 | 스크립트가 로드되지 않았습니다. `src` 속성의 경로나 호스트가 `.js` 파일을 제공하는지 확인하세요. 일반 POST는 여전히 작동하므로 가입이 들어오는 것입니다. |
| `402 test_limit_reached` | 워크스페이스에 플랜이 없고 이번 달 무료 `localhost` 테스트 50건을 다 썼습니다. 플랜을 고르거나 대시보드의 **Send test**를 쓰세요. |

[HTML 폼 문서](https://ikiform.com/ko/docs/html.md)에 모든 예약 필드가 나와 있고, [대기자 명단 템플릿](https://ikiform.com/forms/waitlist)은 이름과 역할이 있는 버전을 만들어 줍니다. 목록 내보내기와 출시 이메일은 [메인 가이드](https://ikiform.com/ko/guides/waitlist.md#launch-day)에 있습니다.

## 질문

### 대기자 명단 폼이 JavaScript 없이 작동할 수 있나요?

네. 일반 HTML 폼이 이메일을 Ikiform 엔드포인트로 POST하면 Ikiform이 브라우저를 감사 페이지로 리디렉션합니다. JavaScript는 인라인 메시지만 더할 뿐입니다.

### 일반 HTML 대기자 명단 페이지는 어디에 호스팅할 수 있나요?

정적 파일을 제공하는 곳이라면 어디든 됩니다. GitHub Pages, Netlify, Cloudflare Pages, S3, 공유 호스팅 등입니다. 폼은 Ikiform으로 POST하므로 호스트는 어떤 코드도 실행하지 않습니다.

### 사람 확인이 켜져 있는데 스크립트가 fetch로 POST하면 어떻게 되나요?

Ikiform이 403 challenge_required로 응답합니다. 이 가이드의 스크립트는 이를 잡아 폼을 일반 방식으로 제출하고, 확인 페이지를 거쳐 감사 페이지를 보여 줍니다.

### Ikiform이 폼 페이지를 호스팅해 주나요?

아니요. Ikiform에는 호스팅 폼 페이지가 없습니다. 폼은 어떤 빌더나 프레임워크든 내 사이트에 두고 Ikiform 엔드포인트를 가리키게 하세요.

### Ikiform은 무료인가요?

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

## 가이드 더 보기

- [새 SaaS용 대기자 명단](https://ikiform.com/ko/guides/waitlist.md): 내 랜딩 페이지에 출시 전 대기자 명단을 만듭니다. 폼, 스팸 방지, 중복 처리, 확인 이메일, 출시일의 깔끔한 내보내기까지 다룹니다.
- [Next.js 대기자 명단](https://ikiform.com/ko/guides/waitlist/nextjs.md): Next.js App Router 대기자 명단: 시크릿 키로 가입을 전달하는 서버 액션, useActionState, 낙관적 감사 문구, 방문자별 한도를 다룹니다.
- [Carrd 대기자 명단](https://ikiform.com/ko/guides/waitlist/carrd.md): Carrd 페이지에서 대기자 명단 가입 받기: Pro Plus에서 UTM 추적이 되는 Send to URL 폼, 또는 Pro Standard에서 허니팟이 있는 임베드 HTML 폼.
- [Product Hunt 출시 리스트](https://ikiform.com/ko/guides/waitlist/product-hunt.md): Product Hunt 몇 주 전부터 출시 리스트를 모으고, 가입마다 채널 태그를 달고, 태평양 시간 리셋에 맞춰 출시일 이메일을 두 번에 나눠 보냅니다.
- [선별 기능이 있는 베타 신청](https://ikiform.com/ko/guides/waitlist/beta-signup.md): 서버가 강제하는 선별 질문이 있는 베타 신청 폼과, 지원자에게 점수를 매겨 목록에서 기수별 참가자를 고르는 스크립트입니다.
- [모바일 앱 대기자 명단](https://ikiform.com/ko/guides/waitlist/mobile-app.md): iOS나 Android 앱의 출시 전 대기자 명단으로, 사용하는 휴대폰을 묻고 iPhone 테스터는 TestFlight로 보내고 Play 비공개 테스트를 채웁니다.
