# Framer 폼: 폼 백엔드를 추가할 때와 방법

> Framer 폼은 이미 이메일, Sheets, 웹훅으로 보냅니다. 저장되고 걸러진 제출, 팀 알림, 더 많은 목적지를 원한다면 Ikiform을 웹훅으로 추가하세요.

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

Framer의 기본 폼은 스팸 보호가 내장된 채로 각 제출을 이메일, Google Sheets 또는 웹훅으로 보내므로, 받은편지함 하나나 시트 하나면 다른 것이 필요 없을 수 있습니다. 모든 제출을 스팸 상태와 함께 저장하고, 필드에 맞춰 검증하고, 팀에 이메일로 보내고, 여러 서명된 웹훅으로 전달하고 싶을 때 Ikiform을 웹훅 대상으로 추가하세요. Framer는 자체 서버에서 POST하므로 폼의 origin 확인을 느슨하게 하거나, 중간에 Framer의 서명을 검증하는 작은 릴레이를 두세요.

## Framer에서 다른 점

Framer는 이 가이드에서 벗어날 한도가 없는 유일한 빌더입니다. 폼은 이메일, Google Sheets, 어떤 웹훅으로든 보낼 수 있고, 의심스러운 제출을 표시하거나 차단하는 스팸 보호가 내장되어 있습니다. 웹훅은 서명됩니다. 각 요청에는 본문과 제출 id의 HMAC-SHA256인 `Framer-Signature` 헤더와 `Framer-Webhook-Submission-Id`가 담깁니다. Framer는 엔드포인트가 `2xx`로 응답하지 않으면 최대 다섯 번 재시도하며, 리디렉션은 따르지 않습니다.

그러니 폼 백엔드를 추가하는 이유를 솔직하게 따져 보세요. 양쪽이 다루는 것은 다음과 같습니다.

| 원하는 것 | Framer만 | Ikiform을 웹훅으로 |
| --- | --- | --- |
| 받은편지함 하나나 시트 하나에 모인 제출 | 예 | 예, 대시보드도 포함 |
| 스팸이 표시된 검색 가능한 기록과 CSV로 내보내기 | 받은편지함이나 시트가 기록 | 예 |
| 타입, 길이, 선택지에 대한 서버 측 확인 | 아니요 | 예, 필드 정의가 있으면 |
| 여러 사람에게 보내는 알림, 답장은 제출자에게 | 이메일 목적지 | 최대 10명의 수신자, reply-to 포함 |
| 한 폼에서 여러 도구로 전달, 각각 재시도 | 추가한 목적지마다 웹훅 하나 | 최대 3개의 서명된 엔드포인트, 전달 로그와 함께 재시도 |
| Framer, Webflow, 코드 사이트에 걸친 같은 백엔드 | 아니요 | 예 |

첫 번째 행만 필요하다면 Framer의 목적지를 그대로 쓰세요. 이 페이지의 나머지는 그렇지 않을 때를 위한 것입니다.

## 함정: Framer는 자체 서버에서 POST합니다

웹훅 대상이란 방문자의 브라우저가 아니라 Framer의 서버가 POST를 보낸다는 뜻입니다. Ikiform 쪽에서는 세 가지가 바뀝니다.

- **origin 없음.** 서버 POST에는 `Origin` 헤더가 없으므로 **Allowed domains**가 설정되어 있으면 모든 POST가 잘못된 사이트에서 온 것으로 취급됩니다.
- **공유 IP 주소.** Ikiform은 IP 주소별로 레이트 리밋을 세는데, 모든 POST가 방문자 주소가 아니라 몇 개의 Framer 주소에서 옵니다.
- **허니팟 없음.** Framer는 만든 입력만 보내며, Framer 폼은 Ikiform의 사람 확인을 실행할 수 없습니다.

이를 받아들이거나 릴레이로 해결할 수 있습니다. 두 경로 모두 아래에 있습니다.

다이어그램: Ikiform으로 직접, 그리고 서명된 릴레이를 거쳐 POST하는 Framer 폼

1. 직접: 방문자가 Framer 폼을 제출하면 Framer 서버가 Origin 헤더 없이 JSON으로 보내고, Ikiform은 Allowed domains가 비어 있고 Relaxed 레이트 리밋인 상태에서 이를 받습니다.
2. 릴레이: Framer 서버가 모든 POST에 시크릿으로 서명해 직접 배포한 Worker로 보냅니다.
3. Worker가 Framer의 서명을 다시 계산합니다. 유효한 서명이 없는 POST는 401을 받고 더 진행되지 않습니다.
4. 유효한 POST는 시크릿 키와 함께, Framer의 제출 id를 멱등성 키로 삼아 Ikiform으로 전달되므로 Framer가 재시도해도 사본이 두 번 저장되지 않습니다.

릴레이의 실제 로직은 열두 줄 정도입니다. 그 대가로 Worker만 시크릿 키를 가지므로 폼은 엄격한 허용 도메인을 유지할 수 있습니다.

## 방법 1: Ikiform을 웹훅으로

1. Framer에서 각 입력에 `name`, `work_email`, `company`, `team_size` 같은 소문자 이름을 주세요. Framer는 이름을 JSON 키로 씁니다.
2. 폼을 선택하고 **Send To** 옆에서 **Add…** > **Webhook**을 고르세요. 엔드포인트를 붙여 넣으세요. 시크릿은 비워 두세요. Ikiform은 `Framer-Signature`를 확인하지 않습니다.
3. Ikiform 폼 설정에서 **Allowed domains**는 비우고, **Spam check**는 끈 채로 두고, **Rate limit**을 **Relaxed**로 설정하세요.
4. 게시하세요. Framer는 게시된 사이트에서만 웹훅을 보냅니다.

`웹훅 URL`:

```text
https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx
```

Ikiform은 제출을 저장하면 `201`로 응답하며, Framer는 이를 전달 완료로 집계합니다. `422 validation_failed`는 다섯 번의 재시도에서 모두 똑같이 실패하므로 기다리지 말고 입력 이름을 고치세요. 링크, 스팸 문구, 일회용 주소, 반복 같은 스팸 점수 매기기는 여전히 모든 POST에 실행됩니다.

## 방법 2: 서명된 릴레이

Framer의 서명을 확인하고 유효한 POST를 시크릿 키와 함께 전달하는 Cloudflare Worker를 배포하세요. 그러면 Ikiform 폼이 **Reject**와 함께 **Allowed domains**에 Framer 도메인을 등록할 수 있어, 내 사이트의 브라우저나 Worker가 아닌 것은 모두 거부됩니다.

`src/index.ts`:

```ts
interface Env {
  FRAMER_SECRET: string;
  IKIFORM_SECRET_KEY: string;
}

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const encoder = new TextEncoder();

async function signatureFor(secret: string, body: ArrayBuffer, submissionId: string) {
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const id = encoder.encode(submissionId);
  const message = new Uint8Array(body.byteLength + id.byteLength);
  message.set(new Uint8Array(body), 0);
  message.set(id, body.byteLength);
  const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, message));
  return `sha256=${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}

function sameText(a: string, b: string) {
  const left = encoder.encode(a);
  const right = encoder.encode(b);
  return left.byteLength === right.byteLength && crypto.subtle.timingSafeEqual(left, right);
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") return new Response(null, { status: 405 });
    const signature = request.headers.get("framer-signature") ?? "";
    const submissionId = request.headers.get("framer-webhook-submission-id") ?? "";
    const body = await request.arrayBuffer();
    if (!submissionId || !sameText(signature, await signatureFor(env.FRAMER_SECRET, body, submissionId))) {
      return new Response("Bad signature", { status: 401 });
    }

    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${env.IKIFORM_SECRET_KEY}`,
        "Idempotency-Key": submissionId,
      },
      body,
    });
    if (response.ok) return new Response(null, { status: 204 });
    return new Response(await response.text(), { status: response.status });
  },
};
```

### 배포하기

`터미널`:

```bash
npx wrangler secret put FRAMER_SECRET
npx wrangler secret put IKIFORM_SECRET_KEY
npx wrangler deploy
```

- `FRAMER_SECRET`은 Framer의 웹훅 대상에 설정한 시크릿입니다. Framer는 최소 32자를 요구하고 다시 보여 주지 않으므로, 생성해서 보관하고 양쪽에 붙여 넣으세요.
- `IKIFORM_SECRET_KEY`는 Ikiform 대시보드의 시크릿 API 키입니다. 절대 Framer에 넣지 마세요.
- 서명은 `sha256=`에 원본 본문과 그 뒤에 붙인 제출 id의 hex HMAC을 더한 것입니다. 본문을 바이트로 한 번 읽고, 확인과 전달 모두에 그 같은 바이트를 쓰세요.
- `Idempotency-Key`는 Framer가 재시도할 때 중복을 저장하는 대신 Ikiform의 첫 응답을 재생하게 합니다. 키는 24시간 동안 보관됩니다.
- Worker는 Ikiform의 상태를 그대로 전달합니다. Framer는 `2xx`가 아닌 응답을 최대 다섯 번 재시도하므로 `429`나 `5xx`는 다시 기회를 얻지만, `422`는 입력 이름을 고칠 때까지 매번 똑같이 실패합니다.

그런 다음 Framer의 웹훅이 엔드포인트 대신 Worker의 URL을 가리키게 하고 시크릿을 채우세요.

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

## 방법 3: Framer 폼 건너뛰기

코드 컴포넌트는 실제 폼을 렌더링해 방문자의 브라우저에서 POST하므로, origin 확인, 허니팟, 방문자별 레이트 리밋이 다른 사이트처럼 동작합니다. 그 폼에서는 Framer의 시각적 폼 편집을 잃습니다. [Framer 문서 가이드](https://ikiform.com/ko/docs/guides/framer.md#code-component)에 컴포넌트가 있습니다.

## Framer에서 동작 확인하기

1. Framer 사이트를 게시하고 편집기 미리보기가 아니라 게시된 사이트에서 폼을 제출하세요.
2. 대시보드에서 **Submissions**를 여세요. 보낸 모든 필드가 담긴 새 제출이 있습니다.
3. 필드 이름이 입력 이름과 정확히 일치하고 Framer가 추가한 키가 없는지 확인하세요.
4. 릴레이의 경우: 지어낸 `Framer-Signature`로 Worker에 요청을 보내 `401`로 응답하는지 확인하세요.
5. 릴레이의 경우: 제출 세부 정보에서 origin이 비어 있고 상태가 스팸이 아니라 received인지 확인하세요.
6. 알림 수신자 각각의 받은편지함을 확인하세요. 각자 필드가 담긴 이메일을 받고, 폼에 `email` 필드가 있으면 **Reply**는 제출자에게 갑니다.
7. 웹훅을 추가했다면 **Webhooks**에서 엔드포인트를 여세요. 전달이 서버의 응답과 함께 `succeeded`로 표시됩니다.

## 주의할 점

| 문제 | 해결 방법 |
| --- | --- |
| 모든 웹훅 POST가 `origin_mismatch`로 스팸에 들어감 | Framer가 직접 POST하는데 **Allowed domains**가 설정되어 있습니다. 비우거나 중간에 릴레이를 두세요. |
| Worker가 실제 POST에 `401`로 응답함 | Framer와 Worker의 시크릿이 다르거나, 무언가가 본문을 텍스트로 읽어 바꿨습니다. 시크릿을 다시 붙여 넣고 원본 바이트를 해시하세요. |
| 출시 중 `429 rate_limited` | 모든 POST가 Framer나 Cloudflare의 주소를 공유합니다. **Relaxed**나 Custom 한도를 고르세요. |
| `422 validation_failed` | POST가 폼의 필드 정의와 맞지 않습니다. `error.fields`에 각 필드와 이유가 나옵니다. 입력의 `name`을 필드 ID와 맞추세요. |
| `403 challenge_required` | 폼의 스팸 검사가 **Ikiform**으로 설정되어 있는데 `fetch` POST에 토큰이 없었습니다. 제출할 때마다 `@ikiform/sdk`의 `challenge(formId)`로 토큰을 받거나 검사를 끄세요. |

## 질문

### Framer 폼에 폼 백엔드가 필요한가요?

항상 그렇지는 않습니다. Framer는 자체적으로 제출을 이메일, Google Sheets, 웹훅으로 보냅니다. 저장되고 걸러진 기록, 서버 측 검증, reply-to가 있는 팀 알림, 또는 한 폼에서 재시도되는 여러 웹훅이 필요할 때 Ikiform을 추가하세요.

### Ikiform이 Framer의 웹훅 서명을 검증하나요?

아니요. Ikiform은 Framer의 헤더가 아니라 origin이나 시크릿 API 키로 POST를 인증합니다. Framer-Signature를 확인하려면 Framer와 Ikiform 사이에 이를 검증하고 시크릿 키로 전달하는 작은 릴레이를 두세요.

### Framer 제출이 스팸으로 표시되는 이유는 무엇인가요?

대부분 Allowed domains가 설정되어 있기 때문입니다. Framer의 웹훅 POST는 Origin 헤더 없이 서버에서 오므로 어떤 도메인과도 일치하지 않습니다. 목록을 비우거나 시크릿 키로 POST하는 릴레이를 쓰세요.

### Framer의 재시도가 중복 제출을 만드나요?

직접 POST할 때 Ikiform이 저장한 뒤 전달이 재시도되면 그럴 수 있습니다. 릴레이는 Framer의 제출 id를 멱등성 키로 보내므로 재시도 시 첫 응답을 재생합니다.

### 폼으로 파일 업로드를 받을 수 있나요?

아직은 안 됩니다. 파일이 포함된 POST는 422 files_not_supported로 거부됩니다. 대신 공유 드라이브 URL 같은 파일 링크를 요청하세요.

## 가이드 더 보기

- [빌더 폼 한계 넘어서기](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을 가리키게 하세요.
- [Carrd 폼](https://ikiform.com/ko/guides/website-builder-forms/carrd-forms.md): Carrd의 문의 폼은 이메일만 보내고 커스텀 폼에는 Pro Plus가 필요하므로, 모든 제출을 저장하려면 Plus에서는 Send to URL을, Standard에서는 HTML 임베드를 쓰세요.
- [Netlify Forms 한도](https://ikiform.com/ko/guides/website-builder-forms/netlify-forms-limit.md): 레거시 Netlify 플랜은 사이트별 폼 제출을 계량해 자동으로 업그레이드하고, 크레딧 플랜은 사이트를 일시 중지할 수 있으니 폼을 백엔드로 옮기세요.
