새 SaaS용 대기자 명단

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

Framer 사이트에 대기자 명단 추가하는 방법

Framer에서 대기자 명단 가입을 받는 두 가지 방법: 웹훅으로 Ikiform에 보내는 Framer 폼, 또는 방문자별 한도와 스팸 검사를 유지하는 코드 컴포넌트입니다.

Preet SutharIkiform 창업자

Framer 폼은 외부 URL로 POST할 수 없지만 각 제출을 웹훅으로 보낼 수는 있습니다. 그래서 코드 없는 방법은 Ikiform 엔드포인트를 웹훅으로 지정한 Framer 폼입니다. 공개 출시라면 대신 작은 코드 컴포넌트를 쓰세요. 브라우저가 직접 POST하므로 Ikiform의 방문자별 레이트 리밋, 허용 도메인, 허니팟이 모두 계속 작동합니다.

페이지 자체는 오후 한나절이면 만들 수 있어서 많은 출시 전 페이지가 Framer로 만들어집니다. 덜 명확해지는 부분은 대기자 명단 폼입니다. Framer의 폼 블록은 action을 바꿀 수 없으므로, 실제 트래픽이 들어오면 다르게 동작하는 두 방법 중 하나를 골라야 합니다.

Framer에서 다른 점

  • 사용자 지정 action이 없습니다. Framer 폼은 Framer의 대상으로만 보냅니다. 자체 저장소, 이메일, 또는 Webhook입니다. 코드 없이 외부 백엔드에 닿는 방법은 웹훅뿐입니다.
  • 웹훅은 방문자가 아니라 Framer 서버에서 옵니다. Ikiform은 Framer의 IP 주소를 보고 Origin 헤더는 보지 못합니다. 그래서 허용 도메인, 방문자별 레이트 리밋, 사람 확인이라는 세 가지 보호가 꺼집니다.
  • 재시도. Framer는 2xx가 아닌 응답을 모두 실패로 보고 최대 다섯 번 재시도합니다. 리디렉션은 따르지 않습니다. Ikiform은 JSON POST에 201로 응답하므로 저장된 가입은 첫 번째에 성공합니다.
  • 코드 컴포넌트는 진짜 React입니다. 게시된 사이트에서 방문자의 브라우저로 실행되므로, 그 안의 폼은 다른 웹 페이지처럼 POST합니다.
Framer 대기자 명단 가입이 Ikiform에 도달하는 두 가지 경로웹훅을 쓰는 Framer 폼코드 컴포넌트Framer 폼내 페이지에서Framer 서버JSON, 최대 5회 재시도IkiformFramer의 IP만 보임, 출처 없음코드 컴포넌트내 페이지의 React 폼방문자의 브라우저멱등성 키와 함께 fetchIkiform방문자와 내 도메인이 보임제출웹훅제출전송Framer 대기자 명단 가입이 Ikiform에 도달하는 두 가지 경로웹훅을 쓰는 Framer 폼코드 컴포넌트Framer 폼내 페이지에서Framer 서버JSON, 최대 5회 재시도IkiformFramer의 IP만 보임, 출처 없음코드 컴포넌트내 페이지의 React 폼방문자의 브라우저멱등성 키와 함께 fetchIkiform방문자와 내 도메인이 보임제출웹훅제출전송
  1. 웹훅 방식에서는 방문자가 Framer 폼을 제출하면 Framer 서버가 필드를 JSON으로 Ikiform에 전달합니다.
  2. Ikiform은 Framer 서버를 발신자로 보므로 방문자를 구분하거나 폼이 어느 사이트에 있는지 확인할 수 없습니다.
  3. 코드 컴포넌트 방식에서는 방문자의 브라우저가 Ikiform으로 바로 POST합니다.
  4. Ikiform은 각 방문자의 IP와 내 도메인을 보므로 모든 보호가 적용됩니다.
두 방식 모두 같은 가입을 저장합니다. 차이는 Ikiform이 누가 보냈다고 여기는지입니다.

방식 고르기

Item웹훅을 쓰는 Framer 폼코드 컴포넌트
설정오른쪽 사이드바의 필드 두 개코드 파일 하나를 한 번 붙여 넣기
스타일Framer 폼의 스타일과 상태직접 수정하는 인라인 스타일 또는 자체 클래스
방문자별 레이트 리밋아니요. 모든 POST가 Framer의 IP를 공유합니다예, 각각 분당 5회
허용 도메인비워 둠Framer 도메인과 커스텀 도메인으로 설정
허니팟아니요예
사람 확인끔끔, fetch로 POST하므로
유입 경로 추적없음URL의 ?ref= 또는 인스턴스별 기본값

수백 명에게 공유하는 페이지라면 웹훅 방식으로 충분합니다. 큰 곳에 페이지를 올릴 예정이라면 코드 컴포넌트를 쓰세요. Framer 서버를 거쳐 몰려드는 봇 가입은 스팸 점수로만 막을 수 있습니다.

방법 1: 웹훅을 쓰는 Framer 폼

  1. 페이지에 Email 입력 하나가 있는 폼을 추가하세요. 입력 설정에서 이름을 email로 지정하세요. Framer는 각 입력의 이름을 JSON 키로 보내고, Ikiform은 email을 회신 주소와 일회용 도메인 검사에 씁니다.
  2. 폼을 선택하세요. Send To 옆에서 Add… › Webhook을 고르고 엔드포인트 https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx를 붙여 넣으세요. 서명 시크릿은 선택 사항이며, Ikiform은 Framer의 서명을 검사하지 않습니다.
  3. Ikiform에서 Allowed domains는 비워 두고 스팸 검사는 끄세요. 둘 중 하나라도 켜면 Framer POST는 출처가 없고 브라우저 검사를 실행할 수 없으므로 모두 표시되거나 거부됩니다.
  4. 레이트 리밋을 Relaxed로, 출시일에는 Custom으로 설정하세요. Standard는 Framer의 모든 POST를 방문자 한 명으로 세고, 폼 전체는 분당 300회로 제한됩니다.
  5. Framer에서 폼의 성공 상태를 꾸미세요. Ikiform이 201로 응답하면 표시됩니다.
필드 정의가 있는데 이메일이 빠진 경우처럼 Ikiform이 거부하는 POST는 Framer의 다섯 번 재시도에서도 똑같이 실패합니다. 가입이 들어오지 않으면 Framer에서 전달 상태를 확인하세요.

방법 2: 코드 컴포넌트

Assets 패널에서 Code를 열고 WaitlistSignup이라는 코드 파일을 만든 뒤 이것을 붙여 넣으세요. 페이지로 끌어다 놓고 속성 패널에서 Form ID를 설정하세요.

WaitlistSignup.tsx
import { addPropertyControls, ControlType } from "framer";
import { type CSSProperties, type FormEvent, useRef, useState } from "react";

const box: CSSProperties = {
  font: "inherit",
  padding: "12px 14px",
  borderRadius: 10,
  border: "1px solid rgba(0, 0, 0, 0.15)",
};

interface Props {
  formId: string;
  buttonLabel: string;
  joinedText: string;
  source: string;
}

export default function WaitlistSignup({ formId, buttonLabel, joinedText, source }: Props) {
  const [status, setStatus] = useState<"idle" | "sending" | "joined">("idle");
  const [error, setError] = useState("");
  const key = useRef("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    const ref = new URLSearchParams(window.location.search).get("ref");
    body.set("ref", (ref || source).slice(0, 40));
    key.current ||= crypto.randomUUID();
    setStatus("sending");
    setError("");
    try {
      const response = await fetch(`https://ikiform.com/f/${formId}`, {
        method: "POST",
        headers: { Accept: "application/json", "Idempotency-Key": key.current },
        body,
      });
      const result = await response.json();
      if (result.ok) return setStatus("joined");
      setError(result.error.fields?.email?.[0] ?? result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    }
    setStatus("idle");
  }

  if (status === "joined") return <p role="status" style={{ margin: 0 }}>{joinedText}</p>;

  return (
    <form onSubmit={onSubmit} style={{ display: "flex", flexWrap: "wrap", gap: 8, width: "100%" }}>
      <input
        name="email"
        type="email"
        placeholder="you@company.com"
        aria-label="Email"
        autoComplete="email"
        required
        style={{ ...box, flex: "1 1 220px" }}
      />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      <button type="submit" disabled={status === "sending"} style={{ ...box, cursor: "pointer" }}>
        {status === "sending" ? "Joining" : buttonLabel}
      </button>
      {error ? <p role="alert" style={{ flexBasis: "100%", margin: 0 }}>{error}</p> : null}
    </form>
  );
}

addPropertyControls(WaitlistSignup, {
  formId: { type: ControlType.String, title: "Form ID", defaultValue: "frm_xxxxxxxxxxxxxxxx" },
  buttonLabel: { type: ControlType.String, title: "Button", defaultValue: "Join the waitlist" },
  joinedText: { type: ControlType.String, title: "Joined text", defaultValue: "You're on the list." },
  source: { type: ControlType.String, title: "Source", defaultValue: "framer" },
});
  • 속성 패널에서 버튼 라벨, 가입 완료 문구, 기본 Source를 설정할 수 있습니다. 히어로에는 hero로, 푸터에는 footer로 인스턴스를 하나씩 두면 어느 쪽이 전환되는지 알 수 있습니다.
  • 페이지 URL의 ?ref=가 기본값보다 우선하므로 캠페인 링크에는 자동으로 태그가 붙습니다.
  • 멱등성 키는 첫 제출 때 만들어지고 같은 방문 중의 재시도에 재사용되므로, 휴대폰에서 두 번 탭해도 가입은 하나만 저장됩니다.
  • window는 제출 핸들러 안에서만 읽습니다. Framer는 컴포넌트를 먼저 서버에서 렌더링하므로 렌더링 중에 window를 건드리면 깨집니다.

컴포넌트용 Ikiform 설정

이메일이 필수이고 ref가 허용되도록 폼의 Fields 탭에 다음을 추가하세요.

필드
{
  "version": 1,
  "title": "Waitlist",
  "fields": [
    { "id": "email", "type": "email", "label": "Email", "required": true },
    { "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
  ]
}
  • Allowed domains에 your-project.framer.app과 커스텀 도메인을 추가하세요. framer.app만 단독으로 추가하지 마세요. 모든 Framer 사이트가 일치하게 됩니다.
  • 스팸 검사는 꺼 두세요. 컴포넌트는 fetch로 POST하고 토큰을 받지 않으므로, 검사가 켜진 폼은 403 challenge_required로 응답합니다.
  • Standard 레이트 리밋을 유지하세요. 이제 방문자마다 따로 집계됩니다.

작동 확인하기

  1. 사이트를 게시하세요. 웹훅과 코드 컴포넌트는 편집기 미리보기가 아니라 게시된 사이트에서만 POST합니다.
  2. framer.app 도메인에서 폼을 제출하고, 커스텀 도메인에서도 다시 제출하세요.
  3. 대시보드에서 Submissions를 여세요. 보낸 모든 필드가 담긴 새 제출이 있습니다.
  4. 컴포넌트의 경우 ?ref=test로 페이지를 열고 가입에 ref가 test로 표시되는지 확인하세요.
  5. 알림 수신자마다 받은편지함을 확인하세요. 각자 필드가 담긴 이메일을 받으며, 폼에 email 필드가 있으면 Reply(답장)는 제출자에게 갑니다.
  6. 아무것도 안 왔나요? 대시보드의 상태 필터를 Spam으로 바꿔 보세요. 스팸도 실제 제출과 같은 성공 응답을 받습니다.

주의할 점

문제해결 방법
모든 웹훅 가입이 origin_mismatch로 스팸에 들어감Allowed domains가 설정되어 있는데 Framer 서버의 POST에는 출처가 없습니다. 이 폼에서는 비우거나 코드 컴포넌트로 바꾸세요.
Framer가 웹훅 전달 실패를 보고함Ikiform이 POST를 거부했습니다. 스팸 검사가 켜져 있어 403 challenge_required이거나, 입력 이름이 email이 아니라서 422입니다. 원인을 고치세요. 재시도도 똑같이 실패합니다.
429 rate_limited한 방문자가 폼의 레이트 리밋보다 많이 보냈습니다. Standard는 방문자당 분당 5회를 허용합니다. Retry-After만큼 기다리거나, 공유 IP 하나에서 POST하는 폼이라면 Relaxed나 Custom을 고르세요.
컴포넌트에 Unknown field "ref"가 표시됨필드 정의에 ref가 없습니다. 위의 정의처럼 추가하세요.
403 origin_not_allowed이거나 모든 POST가 스팸으로 분류됨페이지의 도메인이 폼의 Allowed domains에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 localhost도 추가하세요. 등록된 도메인의 하위 도메인은 허용됩니다.

문의 폼 버전을 포함한 전체 Framer 설정은 Framer 문서 가이드에 있습니다. 목록 내보내기와 출시 이메일 발송은 메인 가이드에 있습니다.

질문

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