# Nuxt

> 하이드레이션 전에는 일반 폼으로, 이후에는 $fetch로 POST하는 Nuxt 컴포넌트. 폼 ID는 런타임 설정에 둡니다. nuxt generate와 함께 작동합니다.

Nuxt는 서버에서 폼을 렌더링하므로, 브라우저에 도착한 HTML은 이미 작동하는 폼입니다. 앱이 하이드레이션되기 전에는 일반 HTML 폼처럼 POST하고, 그 이후에는 컴포넌트가 이어받아 결과를 그 자리에 보여 줍니다. 서버에서 실행되는 것이 없으므로 `nuxt generate`로 빌드한 정적 사이트에서도 작동합니다.

대신 시크릿 키를 사용해 Nuxt 서버 라우트에서 POST하려면 [Vue](https://ikiform.com/ko/docs/guides/vue.md#nuxt)를 참고하세요.

## 설정하기

### 1단계: 폼 ID를 런타임 설정에 넣기

`nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      ikiformFormId: "frm_xxxxxxxxxxxxxxxx",
    },
  },
});
```

환경마다 `NUXT_PUBLIC_IKIFORM_FORM_ID`로 덮어쓸 수 있습니다. `nuxt generate`를 쓰면 값이 빌드 시점에 고정되므로 빌드 환경에 변수를 설정하세요.

### 2단계: 컴포넌트 추가하기

`app/components/ContactForm.vue`:

```vue
<script setup lang="ts">
type Result =
  | { ok: true; id: string }
  | { ok: false; error: { message: string; fields?: Record<string, string[]> } };

const config = useRuntimeConfig();
const endpoint = `https://ikiform.com/f/${config.public.ikiformFormId}`;

const status = ref<"idle" | "sending" | "sent" | "error">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function onSubmit(event: Event) {
  status.value = "sending";
  const result = await $fetch<Result>(endpoint, {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new FormData(event.currentTarget as HTMLFormElement),
    ignoreResponseError: true,
  }).catch(() => null);
  if (result?.ok) {
    status.value = "sent";
    return;
  }
  errors.value = result?.error.fields ?? {};
  message.value = result?.error.message ?? "Couldn't reach the server. Try again.";
  status.value = "error";
}
</script>

<template>
  <p v-if="status === 'sent'">Thanks, we got it.</p>
  <form v-else :action="endpoint" method="post" @submit.prevent="onSubmit">
    <label>
      Email
      <input name="email" type="email" autocomplete="email" required />
    </label>
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label>
      Message
      <textarea name="message" rows="5" />
    </label>
    <input type="hidden" name="_redirect" value="https://example.com/thanks" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="status === 'error' && !errors.email" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Send" }}</button>
  </form>
</template>
```

- Nuxt 3에서는 `app/components/` 대신 `components/`에 넣으세요.
- `ignoreResponseError: true`를 쓰면 `$fetch`가 `4xx`에서 예외를 던지지 않으므로 오류 형식이 데이터로 돌아옵니다. 네트워크 오류만 `catch`에 도달합니다.
- `@submit.prevent`는 앱이 하이드레이션된 뒤에만 존재합니다. 그 전에는 브라우저가 `action`으로 POST하고 `_redirect`를 따릅니다.

### 3단계: 페이지에서 사용하기

`app/pages/contact.vue`:

```vue
<template>
  <ContactForm />
</template>
```

대체 동작에는 서버 렌더링이나 프리렌더링이 필요합니다. `ssr: false`이면 JavaScript가 실행될 때까지 페이지가 비어 있어서, JavaScript 없이 POST할 폼이 없습니다.

## 허용 도메인

프로덕션과 배포처의 미리보기 호스트를 등록하세요.

| 폼이 실행되는 곳 | Allowed domains에 추가할 값 |
| --- | --- |
| `nuxt dev` | `localhost`. `http://localhost:3000`과 일치합니다. Ikiform은 포트가 아니라 호스트 이름만 비교합니다. |
| 여러분의 도메인 | `example.com`, `www.example.com`도 포함 |
| Vercel | `your-project.vercel.app`. `your-project-git-branch-team.vercel.app` 같은 미리보기 URL은 일치하지 않으므로, 테스트하는 동안 정확한 미리보기 호스트를 추가하세요. |
| Netlify | `your-site.netlify.app`. 배포 미리보기(`deploy-preview-12--your-site.netlify.app`)와 브랜치 배포(`branch--your-site.netlify.app`)는 하위 도메인이 아니므로, 테스트하는 동안 정확한 미리보기 호스트를 추가하세요. |
| Cloudflare Pages | `your-project.pages.dev`. `abc123.your-project.pages.dev` 같은 미리보기 URL은 하위 도메인이므로 이미 일치합니다. |

- `vercel.app`, `netlify.app`, `pages.dev`만 단독으로 추가하지 마세요. 다른 사람의 사이트를 포함해 그 호스트의 모든 사이트가 일치하게 됩니다.

## 테스트 체크리스트

1. 허용 도메인에 `localhost`를 넣고 `nuxt dev`를 실행해 제출하세요. 감사 메시지가 폼을 대체해야 합니다.
2. 브라우저 개발자 도구에서 JavaScript를 끄고 새로 고친 뒤 제출하세요. 로컬에서는 Ikiform의 감사 페이지로, `https:` 도메인에서는 `/thanks`로 이동해야 합니다.
3. 필드를 정의한 상태에서 잘못된 이메일을 제출하고 필드 아래에 메시지가 나타나는지 확인하세요.
4. `nuxt generate`를 실행하고 결과물을 배포한 뒤 미리보기 호스트에서 제출하세요.

## 문제 해결

| 증상 | 원인 | 해결 |
| --- | --- | --- |
| `FetchError: [POST] "https://ikiform.com/f/...": 422` | `ignoreResponseError`가 없어서 `$fetch`가 오류에서 예외를 던짐 | `ignoreResponseError: true`를 추가하세요. |
| `404 not_found`, action이 `/f/undefined`로 끝남 | 사이트를 빌드할 때 폼 ID가 설정되지 않음 | 빌드 환경에 `NUXT_PUBLIC_IKIFORM_FORM_ID`를 설정하거나 `nuxt.config.ts`에 기본값을 넣으세요. |
| JavaScript 없이는 아무 일도 일어나지 않음 | 페이지가 `ssr: false`로 클라이언트에서 렌더링됨 | 서버 렌더링을 켜거나 페이지를 프리렌더링하세요. |
| 제출이 `origin_mismatch`와 함께 스팸 필터에 들어감 | POST한 페이지가 폼의 허용 도메인에 없음 | 위 표의 호스트를 추가하세요. **Reject**를 골랐다면 같은 POST가 대신 `403 origin_not_allowed`를 받습니다. |

## 관련 문서

- [Vue](https://ikiform.com/ko/docs/guides/vue.md): 싱글 파일 컴포넌트.
- [순수 JavaScript](https://ikiform.com/ko/docs/guides/javascript.md): 인라인 오류를 표시하는 `fetch`.
- [허용 도메인](https://ikiform.com/ko/docs/spam.md#allowed-domains): 출처를 비교하는 방식과 불일치 시 동작.
- [오류](https://ikiform.com/ko/docs/errors.md): 모든 오류 코드와 원인, 해결 방법.
