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

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

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

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에 도달하는 두 가지 경로

1. 웹훅 방식에서는 방문자가 Framer 폼을 제출하면 Framer 서버가 필드를 JSON으로 Ikiform에 전달합니다.
2. Ikiform은 Framer 서버를 발신자로 보므로 방문자를 구분하거나 폼이 어느 사이트에 있는지 확인할 수 없습니다.
3. 코드 컴포넌트 방식에서는 방문자의 브라우저가 Ikiform으로 바로 POST합니다.
4. Ikiform은 각 방문자의 IP와 내 도메인을 보므로 모든 보호가 적용됩니다.

두 방식 모두 같은 가입을 저장합니다. 차이는 Ikiform이 누가 보냈다고 여기는지입니다.

## 방식 고르기

|   | 웹훅을 쓰는 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. [레이트 리밋](https://ikiform.com/ko/docs/rate-limits.md)을 Relaxed로, 출시일에는 Custom으로 설정하세요. Standard는 Framer의 모든 POST를 방문자 한 명으로 세고, 폼 전체는 분당 300회로 제한됩니다.
5. Framer에서 폼의 성공 상태를 꾸미세요. Ikiform이 `201`로 응답하면 표시됩니다.

> 필드 정의가 있는데 이메일이 빠진 경우처럼 Ikiform이 거부하는 POST는 Framer의 다섯 번 재시도에서도 똑같이 실패합니다. 가입이 들어오지 않으면 Framer에서 전달 상태를 확인하세요.

## 방법 2: 코드 컴포넌트

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

`WaitlistSignup.tsx`:

```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** 탭에 다음을 추가하세요.

`필드`:

```json
{
  "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 문서 가이드](https://ikiform.com/ko/docs/guides/framer.md)에 있습니다. 목록 내보내기와 출시 이메일 발송은 [메인 가이드](https://ikiform.com/ko/guides/waitlist.md#launch-day)에 있습니다.

## 질문

### Framer 폼이 외부 URL로 POST할 수 있나요?

폼 action으로는 안 됩니다. Framer 폼은 Framer 자체 대상으로 보내며, 그중 하나가 어떤 https URL로든 보내는 웹훅입니다. 이것을 Ikiform에 쓰거나, 코드 컴포넌트로 직접 폼을 렌더링하세요.

### Framer 대기자 명단 가입이 왜 스팸으로 표시되나요?

웹훅 방식에서는 Framer가 Origin 헤더 없이 서버에서 POST하므로, Allowed domains가 설정된 폼은 모든 POST를 origin_mismatch로 표시합니다. 웹훅 폼에서는 Allowed domains를 비워 두세요.

### Framer 웹훅 가입이 Framer의 폼 한도에 포함되나요?

Framer는 폼 제출이 무제한이라고 밝히고 있으므로 웹훅 가입이 Framer 한도에 걸리지 않습니다. 코드 컴포넌트 방식은 Framer 폼을 전혀 쓰지 않습니다.

### Ikiform은 무료인가요?

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

## 가이드 더 보기

- [새 SaaS용 대기자 명단](https://ikiform.com/ko/guides/waitlist.md): 내 랜딩 페이지에 출시 전 대기자 명단을 만듭니다. 폼, 스팸 방지, 중복 처리, 확인 이메일, 출시일의 깔끔한 내보내기까지 다룹니다.
- [Webflow 대기자 명단](https://ikiform.com/ko/guides/waitlist/webflow.md): Webflow Form Block을 Ikiform으로 연결하고, 숨겨진 필드로 가입 경로를 태그하고, 짧은 스크립트로 모든 폼에서 Webflow의 성공 상태를 유지합니다.
- [Carrd 대기자 명단](https://ikiform.com/ko/guides/waitlist/carrd.md): Carrd 페이지에서 대기자 명단 가입 받기: Pro Plus에서 UTM 추적이 되는 Send to URL 폼, 또는 Pro Standard에서 허니팟이 있는 임베드 HTML 폼.
- [일반 HTML 대기자 명단](https://ikiform.com/ko/guides/waitlist/html.md): 빌드 없이 일반 HTML로 만든 완전한 대기자 명단 페이지로, JavaScript가 꺼져도 작동하고, 스크립트가 로드되면 인라인 감사 문구를 보여 주며, 필요할 때는 기본 방식으로 돌아갑니다.
- [Product Hunt 출시 리스트](https://ikiform.com/ko/guides/waitlist/product-hunt.md): Product Hunt 몇 주 전부터 출시 리스트를 모으고, 가입마다 채널 태그를 달고, 태평양 시간 리셋에 맞춰 출시일 이메일을 두 번에 나눠 보냅니다.
- [선별 기능이 있는 베타 신청](https://ikiform.com/ko/guides/waitlist/beta-signup.md): 서버가 강제하는 선별 질문이 있는 베타 신청 폼과, 지원자에게 점수를 매겨 목록에서 기수별 참가자를 고르는 스크립트입니다.
