정적 사이트의 문의 폼4분 분량
Cloudflare Pages 사이트에 문의 폼 추가하기
Function을 작성하지 않고 Cloudflare Pages에 문의 폼을 추가하세요. 폼 백엔드로 POST하고, _headers에서 헤더를 설정하고, 직접 가진 Turnstile 키를 씁니다.
Cloudflare Pages의 정적 에셋은 POST에 405로 응답하므로, Pages Function을 작성하거나 폼을 폼 백엔드로 POST해야 합니다. Ikiform을 쓰면 Function을 작성하지 않습니다. 폼의 action을 엔드포인트로 설정하고, 모든 프리뷰 URL까지 포함하는 your-project.pages.dev를 Allowed domains에 추가하고, _headers의 Content-Security-Policy에 Ikiform을 넣으세요. 이미 Cloudflare를 쓰고 있다면 직접 가진 Turnstile 키를 폼에 연결할 수 있습니다.
Cloudflare Pages에서 다른 점
Pages는 빌드 결과물을 Cloudflare 네트워크에서 제공합니다. 정적 에셋은 GET에 응답하고, POST에는 본문 없는 405로 응답합니다.
HTTP/2 405
server: cloudflare
content-length: 0Pages는 functions/ 폴더의 Pages Functions로 코드를 실행할 수 있습니다. functions/contact.ts로 POST를 받을 수는 있지만, 그러면 파싱, 검증, 이메일 제공업체, 스팸 필터링, 저장까지 갖춘 백엔드를 작성하게 됩니다. 또한 Cloudflare 문서는 새 프로젝트를 Pages가 아닌 Workers로 시작하라고 권합니다. 폼 백엔드로 POST하면 두 문제를 모두 피할 수 있고, 나중에 Workers 정적 에셋으로 옮겨도 같은 폼이 그대로 작동합니다.
| Pages의 특성 | 폼에 미치는 영향 |
|---|---|
프리뷰 URL은 abc123.your-project.pages.dev 같은 하위 도메인입니다 | 허용 도메인 your-project.pages.dev 하나로 프로덕션과 모든 프리뷰를 포함합니다. Vercel과 Netlify 프리뷰는 따로 추가해야 합니다 |
헤더는 빌드 결과물의 _headers 파일에서 옵니다 | 규칙은 최대 100개, 한 줄에 2,000자까지입니다. Pages Functions의 응답에는 적용되지 않습니다 |
| Turnstile은 Cloudflare 제품입니다 | 키 없이 Ikiform의 관리형 검사를 쓰거나, Cloudflare 대시보드에서 만든 자체 위젯을 쓸 수 있습니다 |
- Pages가 _headers 파일의 Content-Security-Policy와 함께 문의 페이지를 제공합니다.
- Turnstile 위젯이 여러분의 사이트 키로 실행되어 폼에 토큰을 넣습니다.
- 폼이 필드와 토큰을 Ikiform으로 POST합니다.
- Ikiform이 여러분의 시크릿 키로 토큰을 검증하고, 토큰의 호스트 이름을 허용 도메인과 비교합니다.
- 유효한 POST는 저장되고 이메일로 전달됩니다. 토큰이 없거나, 만료되었거나, 재사용되었다면 403 captcha_failed를 받고, 페이지가 위젯을 초기화합니다.
폼
이 버전은 자체 Turnstile 위젯을 씁니다. Cloudflare 계정에서 이미 Turnstile을 관리하고 있다면 합리적인 선택입니다. 폼 설정에서 Spam check를 Your own Turnstile keys로 설정하고 사이트 키와 시크릿을 붙여 넣으세요. 위젯은 폼에 cf-turnstile-response 필드를 추가하고, team 선택 항목은 누가 답해야 하는지 알려 줍니다.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="team">Who should read this?</label>
<select id="team" name="team">
<option value="support">Support</option>
<option value="sales">Sales</option>
<option value="press">Press</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" minlength="10" required></textarea>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAAxxxxxxxxxxxxxx"></div>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button type="submit">Send</button>
<p id="contact-status" role="status"></p>
</form>
<script src="/contact.js" defer></script>스크립트는 방문자를 페이지에 머물게 합니다. Turnstile 토큰은 한 번만 쓸 수 있으므로, 전송이 실패하면 다음 시도를 위해 위젯을 초기화합니다.
const form = document.querySelector("#contact");
const status = document.querySelector("#contact-status");
form.addEventListener("submit", async (event) => {
event.preventDefault();
status.textContent = "Sending";
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
}).catch(() => null);
const result = await response?.json().catch(() => null);
if (result?.ok) {
form.replaceWith(Object.assign(document.createElement("p"), { textContent: "Sent. We'll reply by email." }));
return;
}
window.turnstile?.reset();
if (result?.error?.code === "captcha_failed") {
status.textContent = "The human check expired. Complete it again, then send.";
return;
}
const fields = result?.error?.fields ?? {};
status.textContent = Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.";
});- Cloudflare의 Turnstile 위젯 설정에 폼이 실행되는 호스트 이름을 나열하세요. Ikiform은 토큰의 호스트 이름도 폼의 Allowed domains와 비교합니다.
- 유효한 토큰이 없는 POST는
403 captcha_failed로 거부됩니다. 위젯이 끝나기 전에 보낸 일반 HTML POST도 마찬가지입니다. - 키를 아예 쓰고 싶지 않나요? Spam check를 Ikiform으로 설정하고 위젯과 스크립트를 지우면, HTML POST가 ikiform.com에서 검사를 실행합니다. 그러면
fetch버전에는 SDK의challenge()가 필요합니다. 사람 확인을 참고하세요.
내 키 또는 Ikiform의 검사
| Item | Ikiform의 검사 | 직접 가진 Turnstile 키 |
|---|---|---|
| 관리할 키 | 없음 | Cloudflare 대시보드의 사이트 키와 시크릿 |
| HTML 폼 변경 | 없음. 검사는 ikiform.com 페이지에서 실행됩니다 | 위젯과 스크립트 추가 |
fetch 버전 | SDK의 challenge() 호출 | 위젯의 cf-turnstile-response 전송 |
| Content-Security-Policy | Ikiform용 form-action과 connect-src만 | script-src와 frame-src에 challenges.cloudflare.com도 추가 |
| Turnstile 분석 | 여러분의 계정에는 없음 | Cloudflare 대시보드에서 다른 위젯 옆에 표시 |
Ikiform의 검사가 설정할 것이 적습니다. 이미 Turnstile 분석을 보고 있거나 위젯의 모양과 위치를 직접 제어하고 싶다면 자체 키가 합리적입니다.
로컬에서 위젯 테스트하기
Cloudflare는 항상 통과하거나 항상 실패하는 테스트용 사이트 키와 시크릿을 공개합니다. 예를 들어 항상 통과하는 쌍은 1x00000000000000000000AA와 1x0000000000000000000000000000000AA입니다. 프로덕션 시크릿은 테스트 키로 만든 토큰을 거부하므로 한 폼에서 섞어 쓰지 마세요. 로컬 작업용 Ikiform 폼을 하나 더 만들어 테스트 키 쌍을 주고, data-sitekey와 폼 ID를 함께 바꾸세요. 그 폼의 Allowed domains는 비워 두세요. Ikiform은 목록이 설정된 경우에만 토큰의 호스트 이름을 검사하며, Cloudflare는 테스트 토큰이 어떤 호스트 이름을 보고하는지 문서화하지 않았습니다.
_headers 파일
_headers를 public/ 같은 정적 에셋 디렉터리에 두어 빌드 결과물에 포함되게 하세요. Turnstile에는 https://challenges.cloudflare.com에 대한 script-src와 frame-src가, Ikiform에는 form-action과 connect-src가 필요합니다.
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com나중에 같은 프로젝트에 Pages Function을 추가하면, 그 응답에는 이 헤더가 붙지 않습니다. Function 코드에서 설정하세요.
Workers로 옮긴다면
사이트를 Workers 정적 에셋으로 옮겨도 바뀌는 것은 배포 방식이지 폼의 작동 방식이 아닙니다. 폼은 여전히 ikiform.com으로 POST하므로 Cloudflare 쪽에서는 아무것도 처리하지 않습니다. 옮긴 뒤 두 가지를 확인하세요. 새 workers.dev 호스트나 라우트가 Allowed domains에 있는지, 그리고 Workers는 헤더를 설정하는 방식이 다르므로 헤더가 여전히 페이지에 적용되는지입니다.
your-project.pages.dev와 커스텀 도메인을 추가하세요. pages.dev만 단독으로 추가하지 마세요. 모든 Pages 프로젝트가 일치하게 됩니다.작동 확인하기
- 브랜치를 푸시하고 프리뷰 URL을 여세요. 제출한 뒤 메시지가 도착하고 스팸으로 표시되지 않았는지 확인하세요. 프리뷰는 프로젝트 호스트의 하위 도메인입니다.
- 프로덕션에서 다시 제출한 뒤, 뒤로 가기를 눌러 위젯을 완료하지 않고 같은 폼을 한 번 더 보내세요. 제출이 저장되는 대신 초기화 메시지가 나와야 합니다.
- 대시보드에서 Submissions를 여세요. 보낸 모든 필드가 담긴 새 제출이 있습니다.
- 알림 수신자마다 받은편지함을 확인하세요. 각자 필드가 담긴 이메일을 받고, 폼에
email필드가 있으면 Reply가 제출한 사람에게 갑니다. - 아무것도 오지 않았나요? 대시보드의 상태 필터를 Spam으로 바꿔 보세요. 스팸은 실제 제출과 같은 성공 응답을 받습니다.
주의할 점
| 문제 | 해결 방법 |
|---|---|
빈 본문의 405 | 폼이 Pages URL로 POST했습니다. 배포된 action을 확인하고, Function이나 리디렉션 규칙이 /contact를 다시 쓰지 않는지 확인하세요. |
| Turnstile 위젯이 나타나지 않음 | Content-Security-Policy가 challenges.cloudflare.com을 차단합니다. script-src와 frame-src에 추가하고, _headers가 빌드 결과물에 있는지 확인하세요. |
모든 POST에서 403 captcha_failed | 사이트 키와 시크릿이 서로 다른 위젯의 것이거나, 호스트 이름이 위젯에 등록되지 않았거나, 테스트 시크릿이 실제 사이트 키와 짝지어져 있습니다. |
403 origin_not_allowed, 또는 모든 POST가 스팸으로 분류됨 | 페이지의 도메인이 폼의 Allowed domains에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 localhost도 추가하세요. 등록한 도메인의 하위 도메인은 허용됩니다. |
질문
아니요. Function은 POST를 받는 한 가지 방법이지만, 그러면 검증, 이메일 전송, 스팸 필터링을 직접 작성해야 합니다. 폼 백엔드로 POST하면 프로젝트가 완전히 정적으로 유지됩니다.
아니요. Pages 프리뷰 URL은 your-project.pages.dev의 하위 도메인이므로, 그 호스트를 허용하면 모두 포함됩니다.
네. 폼의 스팸 검사를 자체 Turnstile 키로 설정하고 사이트 키와 시크릿을 붙여 넣으세요. Ikiform이 각 토큰을 검증하고 호스트 이름을 허용 도메인과 비교합니다.
폼이 여러분의 Pages URL로 POST했는데, 그곳은 GET 요청에만 응답하는 정적 에셋을 제공합니다. 폼의 action을 폼 백엔드로 지정하거나, 그 경로에 Pages Function을 추가하세요.
아니요, 무료 플랜은 없습니다. 테스트는 무료입니다. 결제하기 전에 localhost에서 한 달에 50건의 테스트 제출을 보낼 수 있습니다. 실제로 운영하려면 Pro가 제출 500건에 월 $6부터이며, 한 번만 결제해 제출 10,000건을 $49에, 50,000건을 $149에 살 수도 있습니다. 크레딧은 만료되지 않습니다.
막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.