# JavaScript와 fetch

> 제출 엔드포인트 자세히 보기: 본문 형식, 헤더, CORS, 응답, 멱등성.

HTTP 요청을 보낼 수 있는 코드라면 무엇이든 Ikiform에 제출할 수 있습니다. JSON, `FormData`, urlencoded 데이터를 `https://ikiform.com/f/<formId>`로 POST하고 JSON 응답을 읽으세요. [SDK](https://ikiform.com/ko/docs/sdk.md)는 이 과정을 함수 하나로 감싸지만 꼭 필요하지는 않습니다. 특정 스택의 단계별 설정은 [플랫폼별 가이드](#guides)를 참고하세요.

## fetch 사용

**JSON** (`submit.js`)

```js
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({
    name: "Ada Lovelace",
    email: "ada@example.com",
    message: "Hello",
  }),
});

const result = await response.json();
if (!result.ok) console.error(result.error.code, result.error.fields);
```

**FormData** (`form.js`)

```js
const form = document.querySelector("form");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const response = await fetch(form.action, {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new FormData(form),
  });
  const result = await response.json();
  if (result.ok) form.replaceWith("Thanks, we got it.");
});
```

**SDK** (`submit.ts`)

```ts
import { submit } from "@ikiform/sdk";

const result = await submit("frm_xxxxxxxxxxxxxxxx", {
  name: "Ada Lovelace",
  email: "ada@example.com",
});

if (result.ok) console.log("Stored", result.id);
else console.error(result.error.code, result.error.fields);
```

## 직접 해 보기

인터랙티브 요청 빌더는 필드 행으로 제출을 구성하고, 그에 맞는 curl, fetch, SDK 코드를 보여 주며, 여러분의 폼 ID를 입력하면 실제 POST를 한 번 보낼 수 있습니다. 자동으로 전송되는 것은 없습니다.

## 엔드포인트

| 메서드 | 경로 | 하는 일 |
| --- | --- | --- |
| `POST` | `/f/<formId>` | 제출 생성 |
| `OPTIONS` | `/f/<formId>` | CORS 프리플라이트, 항상 `204` |
| `GET` | `/f/<formId>/schema` | 폼의 공개 필드 정의, [shadcn/ui](https://ikiform.com/ko/docs/shadcn.md#schema-endpoint) 참고 |
| `GET` | `/f/<formId>/thanks` | 호스팅 감사 페이지 |
| `GET` | `/f/<formId>/challenge` | SDK가 사용하는 Ikiform 사람 확인 프레임 |

브라우저 탭에서 `/f/<formId>` 자체를 열면 `GET` 요청이 되어 `404 not_found`로 응답합니다. 정상입니다. 이 엔드포인트는 POST만 받습니다.

## 본문 형식

| Content-Type | 형태 | 참고 |
| --- | --- | --- |
| `application/json` 및 `+json` 타입 | JSON 객체 하나 | 값은 문자열, 숫자, 불리언, 배열, 객체가 될 수 있으며, 최상위 객체를 포함해 최대 3단계까지 중첩할 수 있습니다 |
| `application/x-www-form-urlencoded` | 이름과 값의 쌍 | 반복되는 이름과 `name[]`은 배열이 됩니다 |
| `multipart/form-data` | 이름과 값의 쌍 | urlencoded와 같음. 실제 파일은 거부됩니다 |
| 그 외 |  | `415 unsupported_media_type` |

본문은 UTF-8이어야 합니다. `utf-8`이 아닌 `charset`은 `415 unsupported_charset`을 받고, 올바른 UTF-8이 아닌 바이트도 같은 코드를 받습니다. JSON 배열이나 형식이 잘못된 본문은 `400 invalid_body`를 받습니다.

## 헤더

### 요청 헤더

| 헤더 | 필수 | 효과 |
| --- | --- | --- |
| `Content-Type` | 예 | 본문 파서를 고릅니다. 위 내용 참고 |
| `Accept` | 아니요 | `application/json`이면 JSON으로 응답합니다. [JSON 또는 리디렉션](https://ikiform.com/ko/docs/how-it-works.md#response-modes) 참고 |
| `Idempotency-Key` | 아니요 | 재시도를 안전하게 만듭니다. 최대 255자, [멱등성](#idempotency) 참고 |
| `Authorization` | 아니요 | `Bearer ik_sk_...` 또는 `Bearer ik_pub_...`, [서버에서 보내기](https://ikiform.com/ko/docs/guides/server.md) 참고 |
| `Origin`, `Referer` | 브라우저가 보냄 | 폼의 허용 도메인과 비교합니다 |

### 응답 헤더

| 헤더 | 시점 | 값 |
| --- | --- | --- |
| `X-Request-Id` | 모든 응답 | `req_...`, 오류 본문에 `request_id`로도 포함 |
| `Retry-After` | `429` | 다음 POST가 허용될 때까지 남은 초 |
| `Location` | 브라우저 성공 시 `303` | 리디렉션 주소 또는 호스팅 감사 페이지 |
| `Access-Control-Allow-Origin` | 요청에 `Origin`이 있을 때 | 요청 자신의 출처 |
| `Access-Control-Expose-Headers` | CORS 사용 시 | `X-Request-Id, Retry-After`, 브라우저 코드가 둘 다 읽을 수 있도록 |
| `Cache-Control` | 성공 시 | `no-store` |

## CORS

제출 엔드포인트는 어떤 사이트에서 오는 교차 출처 POST든 받습니다. 프리플라이트는 항상 성공하며 요청의 출처를 그대로 돌려줍니다. 쿠키나 자격 증명을 쓰지 않으므로, 폼을 보호하는 것은 CORS가 아니라 **허용 도메인**입니다. 허용 도메인을 설정하면 다른 사이트에서 온 POST는 폼 설정에 따라 스팸으로 저장되거나 거부됩니다. [허용 도메인](https://ikiform.com/ko/docs/spam.md#allowed-domains)을 참고하세요.

`Preflight`:

```http
OPTIONS /f/frm_xxxxxxxxxxxxxxxx HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, Accept, Idempotency-Key, Authorization
Access-Control-Max-Age: 86400
Vary: Origin
```

> `/api/v1` 아래의 [REST API](https://ikiform.com/ko/docs/api.md)는 다릅니다. CORS 헤더를 보내지 않고 브라우저 프리플라이트에 `405`로 응답하므로, 시크릿 키는 웹 페이지에서 절대 사용할 수 없습니다.

## 응답

성공하면 새 제출 ID와 함께 `201`을 반환합니다. 오류는 어디서나 같은 형식을 사용하며, 검증에 실패하면 필드 ID를 키로 필드 오류를 담습니다. 모든 코드는 [오류](https://ikiform.com/ko/docs/errors.md)에 정리되어 있습니다.

**201** (`Created`)

```json
{ "ok": true, "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3" }
```

**422** (`Validation failed`)

```json
{
  "ok": false,
  "error": {
    "code": "validation_failed",
    "message": "Some fields are invalid.",
    "fields": { "email": ["Enter a valid email address"] }
  },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
```

> 스팸 점수를 받은 POST도 `201`을 받습니다. 스팸은 따로 저장되며 웹훅이나 받은편지함에 닿지 않습니다.

## 멱등성

재시도를 안전하게 하려면 `Idempotency-Key` 헤더나 `_idempotency` 필드를 보내세요. 키는 최대 255자이며 24시간 동안 폼 단위로 적용됩니다. 둘 다 보내면 헤더가 우선합니다.

| 재시도 조건 | 결과 |
| --- | --- |
| 같은 키, 같은 필드 | 첫 응답을 그대로 돌려줍니다. 두 번째 제출은 만들어지지 않습니다. |
| 같은 키, 다른 필드 | `422 idempotency_mismatch` |
| 첫 요청이 아직 처리 중일 때 같은 키 | `409 idempotency_in_progress` |

검증 오류처럼 제출이 저장되기 전에 실패한 POST는 키를 보관하지 않으므로, 데이터를 고친 뒤 같은 키로 다시 시도할 수 있습니다. 안전한 재시도 루프:

`retry.js`:

```js
const key = crypto.randomUUID();

async function send(attempt = 1) {
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: { "Content-Type": "application/json", "Idempotency-Key": key },
    body: JSON.stringify({ email: "ada@example.com" }),
  }).catch(() => null);
  if (response && response.status < 500 && response.status !== 429) return response.json();
  if (attempt === 3) throw new Error("Ikiform is unreachable");
  const wait = Number(response?.headers.get("Retry-After")) || attempt * 2;
  await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  return send(attempt + 1);
}
```

## 플랫폼별 가이드

### 프레임워크

- [Next.js](https://ikiform.com/ko/docs/guides/nextjs.md): 클라이언트 컴포넌트 또는 서버 액션.
- [React](https://ikiform.com/ko/docs/guides/react.md): 대기, 오류, 성공 상태.
- [Vue](https://ikiform.com/ko/docs/guides/vue.md): 싱글 파일 컴포넌트.
- [Nuxt](https://ikiform.com/ko/docs/guides/nuxt.md): 하이드레이션 전에는 일반 POST, 이후에는 `$fetch`.
- [Svelte](https://ikiform.com/ko/docs/guides/svelte.md): Svelte 5 컴포넌트와 서버 액션.
- [SvelteKit](https://ikiform.com/ko/docs/guides/sveltekit.md): 점진적 향상을 적용한 프리렌더링 페이지.
- [Astro](https://ikiform.com/ko/docs/guides/astro.md): JavaScript 없는 폼, 원하면 향상 적용.
- [Gatsby](https://ikiform.com/ko/docs/guides/gatsby.md): 정적 폼 또는 Gatsby Function.
- [순수 JavaScript](https://ikiform.com/ko/docs/guides/javascript.md): 인라인 오류를 표시하는 `fetch`.

### 정적 사이트 생성기

- [Hugo](https://ikiform.com/ko/docs/guides/hugo.md): 파셜과 콘텐츠 파일용 쇼트코드.
- [Jekyll](https://ikiform.com/ko/docs/guides/jekyll.md): 인클루드와 GitHub Pages 설정.
- [Eleventy](https://ikiform.com/ko/docs/guides/eleventy.md): Nunjucks, Liquid, Markdown용 쇼트코드.

### 사이트 빌더

- [Webflow](https://ikiform.com/ko/docs/guides/webflow.md): 커스텀 action을 설정한 Form Block.
- [Framer](https://ikiform.com/ko/docs/guides/framer.md): 폼 웹훅 또는 코드 컴포넌트.
- [WordPress](https://ikiform.com/ko/docs/guides/wordpress.md): Custom HTML 블록 또는 쇼트코드.
- [Wix](https://ikiform.com/ko/docs/guides/wix.md): `fetch`로 POST하는 HTML 임베드.
- [Squarespace](https://ikiform.com/ko/docs/guides/squarespace.md): JavaScript를 쓰거나 쓰지 않는 코드 블록.
- [Carrd](https://ikiform.com/ko/docs/guides/carrd.md): URL로 보내는 커스텀 폼.
