새 SaaS용 대기자 명단

새 SaaS용 대기자 명단5분 분량

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

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

Preet SutharIkiform 창업자

이메일을 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 대기자 명단 페이지에서 가입이 거칠 수 있는 세 가지 경로JavaScript 없음스크립트 로드됨스크립트가 끝내지 못함폼 POSTurlencodedIkiform303 리디렉션thanks.html내 페이지fetchAccept:application/jsonIkiform201 또는 오류인라인 메시지같은 페이지fetch네트워크 오류 또는 확인 필요form.submit()일반 POST확인 페이지 후thanks.html_redirectJSON폴백일반 HTML 대기자 명단 페이지에서 가입이 거칠 수 있는 세 가지 경로JavaScript 없음스크립트 로드됨스크립트가 끝내지 못함폼 POSTurlencodedIkiform303 리디렉션thanks.html내 페이지fetchAccept: application/jsonIkiform201 또는 오류인라인 메시지같은 페이지fetch네트워크 오류 또는 확인 필요form.submit()일반 POST확인 페이지 후 thanks.html_redirectJSON폴백
  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
<!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
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
<!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
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 폼 문서에 모든 예약 필드가 나와 있고, 대기자 명단 템플릿은 이름과 역할이 있는 버전을 만들어 줍니다. 목록 내보내기와 출시 이메일은 메인 가이드에 있습니다.

질문

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