---
type: how-to
title: 프런트엔드 연동 예제
summary: 순수 HTML, React, Vue, Svelte, Angular, Next.js에서 위젯을 마운트하세요. 요소는 어디서나 같습니다.
---

위젯은 표준 커스텀 요소라, 설치할 래퍼 없이 모든 프레임워크에서 동작합니다. 모양은 늘 같습니다: 패키지를 한 번 로드하고([클라이언트 측 연동](/docs/integration-guides/cdn)), 폼 안에 `<caputchin-widget sitekey="cpt_pub_...">`을 렌더링하고, 방문자가 챌린지를 풀 때 그것이 주입하는 숨겨진 `caputchin-token` 필드를 읽습니다. 게임을 원하면 `<caputchin-game ... game="caputchin/games/leaf-memory">`로 바꾸세요.

<Callout type="warn">
사이트 키가 게임 게이트를 켰다면(그 **검증에 게임 요구** 설정), 체크박스 `<caputchin-widget>`은 그 키에서 통과하지 못합니다: 서버가 게임을 요구하므로, 위젯은 `gate-unavailable` 오류를 일으킵니다. 게이트된 키에서는 `<caputchin-game>`을 쓰세요. 이것은 의도된 것으로, 게이트가 Caputchin 쪽에서 강제되고 체크박스는 그것을 충족할 수 없기 때문입니다. [게임 게이트](/docs/site-keys/games)를 보세요.
</Callout>

아래 예제들은 각 프레임워크의 문법과 그 하나의 커스텀 요소 특이점에서만 다릅니다.

## 순수 HTML

주입된 필드는 평범한 폼 POST에 실려 가니, 따로 배선할 것이 없습니다:

```html
<script src="https://cdn.jsdelivr.net/npm/@caputchin/widget@3/dist/widget.js"></script>

<form action="/contact" method="POST">
  <input name="email" type="email" required />
  <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
  <button type="submit">Send</button>
</form>
```

## React

시작할 때 한 번 임포트하고, 제출 시 `FormData`에서 필드를 읽으세요:

```jsx
import "@caputchin/widget";

export function ContactForm() {
  async function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    await fetch("/api/contact", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: data.get("email"), token: data.get("caputchin-token") }),
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <caputchin-widget sitekey="cpt_pub_..." />
      <button type="submit">Send</button>
    </form>
  );
}
```

React와 함께 TypeScript를 쓴다면, JSX가 두 요소를 받아들이도록 한 번 선언하세요:

```ts
// caputchin.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from "react";
declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "caputchin-widget": DetailedHTMLProps<HTMLAttributes<HTMLElement> & { sitekey: string }, HTMLElement>;
      "caputchin-game": DetailedHTMLProps<HTMLAttributes<HTMLElement> & { sitekey: string; game?: string; games?: string }, HTMLElement>;
    }
  }
}
```

이 `declare module "react"` 형태는 React의 JSX에 한정됩니다. 순수 TypeScript에서 요소는 그냥 `HTMLElement`이며 선언이 필요 없고, 아래 다른 프레임워크들은 커스텀 요소를 저마다의 방식으로 타입 지정하니, 이 블록은 React 프로젝트에서만 필요합니다.

## Vue

Vue 컴파일러에게 `caputchin-` 태그가 커스텀 요소임을 알려, 그것들을 컴포넌트로 해석하려 들지 않게 하세요:

```js
// vite.config.js
export default {
  plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("caputchin-") } } })],
};
```

```vue
<script setup>
import "@caputchin/widget";
function onSubmit(e) {
  const data = new FormData(e.target);
  // send data.get("caputchin-token") to your backend
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <input name="email" type="email" required />
    <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
    <button type="submit">Send</button>
  </form>
</template>
```

## Svelte

Svelte는 커스텀 요소를 있는 그대로 렌더링하니, 설정이 필요 없습니다:

```svelte
<script>
  import "@caputchin/widget";
  function onSubmit(e) {
    const data = new FormData(e.currentTarget);
    // send data.get("caputchin-token") to your backend
  }
</script>

<form on:submit|preventDefault={onSubmit}>
  <input name="email" type="email" required />
  <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
  <button type="submit">Send</button>
</form>
```

## Angular

모듈 또는 standalone 컴포넌트에 `CUSTOM_ELEMENTS_SCHEMA`를 더해 Angular가 알 수 없는 태그를 허용하게 하고, 패키지를 한 번 임포트하세요(`main.ts`에서):

```ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@caputchin/widget";

@Component({
  selector: "contact-form",
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <form (submit)="onSubmit($event)">
      <input name="email" type="email" required />
      <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
      <button type="submit">Send</button>
    </form>
  `,
})
export class ContactFormComponent {
  onSubmit(e: Event) {
    const data = new FormData(e.target as HTMLFormElement);
    // send data.get("caputchin-token") to your backend
  }
}
```

## Next.js

같은 React 코드가 동작합니다. 커스텀 요소는 서버 렌더링 동안 빈 태그로 렌더링되고 패키지가 로드되면 클라이언트에서 업그레이드되니, 클라이언트 컴포넌트에서 `@caputchin/widget`을 임포트하는 것(또는 `next/script`로 CDN 스크립트를 로드하는 것) 외에 특별한 처리가 필요 없습니다.

## 함께 보기

- 로딩 옵션은 [클라이언트 측 연동](/docs/integration-guides/cdn).
- 백엔드 절반은 [서버 측 연동](/docs/integration-guides/verify-on-your-backend).
