프런트엔드 연동 예제
위젯은 표준 커스텀 요소라, 설치할 래퍼 없이 모든 프레임워크에서 동작합니다. 모양은 늘 같습니다: 패키지를 한 번 로드하고(클라이언트 측 연동), 폼 안에 <caputchin-widget sitekey="cpt_pub_...">을 렌더링하고, 방문자가 챌린지를 풀 때 그것이 주입하는 숨겨진 caputchin-token 필드를 읽습니다. 게임을 원하면 <caputchin-game ... game="caputchin/games/leaf-memory">로 바꾸세요.
아래 예제들은 각 프레임워크의 문법과 그 하나의 커스텀 요소 특이점에서만 다릅니다.
순수 HTML
주입된 필드는 평범한 폼 POST에 실려 가니, 따로 배선할 것이 없습니다:
<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에서 필드를 읽으세요:
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가 두 요소를 받아들이도록 한 번 선언하세요:
// 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- 태그가 커스텀 요소임을 알려, 그것들을 컴포넌트로 해석하려 들지 않게 하세요:
// vite.config.js
export default {
plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("caputchin-") } } })],
};<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는 커스텀 요소를 있는 그대로 렌더링하니, 설정이 필요 없습니다:
<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에서):
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 스크립트를 로드하는 것) 외에 특별한 처리가 필요 없습니다.
함께 보기
- 로딩 옵션은 클라이언트 측 연동.
- 백엔드 절반은 서버 측 연동.