HTML 폼 제출

HTML 폼 제출5분 분량

브라우저와 서버에서의 HTML 폼 검증

required, type, pattern, min으로 폼을 검증하고, :user-invalid로 오류에 스타일을 입히고, 필드 정의로 서버에서 같은 규칙을 강제하세요.

Preet SutharIkiform 창업자

required, type="email", min, max, pattern 같은 HTML 내장 제약 조건을 써서 무엇이든 전송되기 전에 브라우저가 잘못된 입력을 막게 하세요. 그런 다음 봇과 스크립트는 브라우저를 완전히 건너뛰므로 서버에서도 같은 규칙을 강제하세요. Ikiform에서는 폼의 필드를 정의하면 모든 POST가 확인됩니다. 잘못된 POST는 필드별 메시지와 함께 422 validation_failed를 받으며, 이를 setCustomValidity로 보여 줄 수 있습니다.

예시는 워크숍 예약 폼입니다. 검증이 제값을 하는 종류의 필드가 있습니다. 한도가 있는 팀 규모, 과거일 수 없는 날짜, 한 가지 형식의 전화번호, 형태가 정해진 예약 코드입니다.

이 경우가 다른 점

검증기는 두 개이고, 둘이 보는 요청은 같지 않습니다. 브라우저는 스크립트 유무와 관계없이 내 페이지를 쓰는 사람만 확인합니다. 서버는 엔드포인트로 바로 POST하는 봇과, 브라우저 확인을 건너뛰는 form.submit()을 호출하는 내 코드까지 모두 확인합니다. 할 일은 둘이 같은 말을 하게 만드는 것입니다.

브라우저 검증을 거친 뒤 Ikiform 필드 검증을 거치는 POST내 페이지의 사람봇 또는 스크립트제출브라우저 제약 조건required, type,min, pattern 확인Ikiform 필드같은 규칙, 강제됨저장타입이 지정된 값필드에 말풍선전송 안 됨직접 POST페이지도 확인도 없음Ikiform 필드422validation_failed저장되지 않음유효하지 않음브라우저 검증을 거친 뒤 Ikiform 필드 검증을 거치는 POST내 페이지의 사람봇 또는 스크립트제출브라우저 제약 조건required, type, min,pattern 확인Ikiform 필드같은 규칙, 강제됨저장타입이 지정된 값필드에 말풍선전송 안 됨직접 POST페이지도 확인도 없음Ikiform 필드422validation_failed저장되지 않음유효하지 않음
  1. 사람이 폼을 제출하면 브라우저가 HTML의 제약 조건을 확인합니다. 유효하지 않은 필드에 메시지가 표시되고 아무것도 전송되지 않습니다.
  2. 유효한 POST가 Ikiform에 도달하면 폼의 필드 정의와 대조하고 타입이 지정된 값을 저장합니다.
  3. 봇이나 스크립트는 엔드포인트로 바로 POST하므로 브라우저 확인이 실행되지 않습니다.
  4. Ikiform이 같은 정의와 대조해 무엇이든 저장하기 전에 422로 거부합니다.
브라우저 계층은 사람을 위한 것입니다. 실제로 지켜지는 것은 서버 계층입니다.

브라우저 계층

미리보기

Ikiform의 검증기를 브라우저에서 실행합니다. 아무것도 전송되지 않습니다.

Ikiform이 강제하는 정의입니다. 팀 규모 25, 11월 2일 이전 날짜, 또는 ws-12 같은 코드를 시험해 보세요.
workshop.html
<form id="workshop" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Your name</label>
  <input id="name" name="name" autocomplete="name" maxlength="120" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="team_size">People attending</label>
  <input id="team_size" name="team_size" type="number" min="2" max="20" step="1" required />

  <label for="date">Preferred date</label>
  <input id="date" name="date" type="date" min="2026-11-02" required />

  <label for="phone">Phone</label>
  <input id="phone" name="phone" type="tel" autocomplete="tel" placeholder="+14155550123"
    pattern="\+[1-9][0-9]{6,14}" title="International format, like +14155550123" />

  <label for="code">Booking code</label>
  <input id="code" name="code" pattern="WS-[0-9]{4}" title="Codes look like WS-1234" />

  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Request the workshop</button>
</form>

각 속성은 하나의 유효성 상태에 대응하며, 콘솔에서 input.validity로 읽을 수 있습니다.

속성유효성 상태참고
requiredvalueMissing라디오 그룹에서는 required가 있는 라디오 하나가 그룹 전체를 덮습니다
type="email", type="url"typeMismatch느슨함: 브라우저에서 a@b는 이메일로 통과합니다
patternpatternMismatch값 전체와 일치해야 합니다. 비어 있는 선택 입력 필드에서는 무시됩니다
minlength, maxlengthtooShort, tooLong사람이 값을 편집한 뒤에만 확인됩니다
min, maxrangeUnderflow, rangeOverflow숫자, 날짜, 시간
stepstepMismatchstep="1"은 소수를 거부합니다
파싱할 수 없는 입력badInput일부 브라우저에서 숫자 필드에 입력한 문자
setCustomValidity()customError설정한 모든 메시지, 다시 빈 값으로 설정할 때까지

두 엔진의 패턴

브라우저는 pattern을 v 플래그로 컴파일하며, 이는 대부분의 사람이 쓰는 정규 표현식보다 엄격합니다. 문자 클래스 안의 리터럴 -는 [a-z\-]처럼 이스케이프해야 합니다. Ikiform은 필드의 pattern을 u 플래그로 컴파일하며 역시 값 전체와 일치시킵니다. 위 예약 코드처럼 양쪽에서 유효한 패턴을 쓰고, 게시하기 전에 폼 미리보기에서 각각 테스트하세요.

오류에 스타일 입히기

:invalid는 페이지가 로드되는 순간 비어 있는 필수 필드와 일치하므로, 누군가 입력하기 전에 모든 필드가 빨갛게 보입니다. :user-invalid는 사람이 필드와 상호작용했거나 제출을 시도한 뒤에만 일치하며, 거의 항상 원하는 동작입니다.

workshop.html
<style>
  #workshop :user-invalid {
    border-color: #b42318;
  }
</style>

서버 계층

위 정의를 폼의 Fields 탭에 붙여 넣으세요. 그때부터 Ikiform은 출처와 관계없이 모든 POST를 이에 맞춰 검증하고 변환된 값을 저장합니다. team_size는 숫자로, phone은 구분 기호 없이, date는 YYYY-MM-DD로 저장합니다. 규칙을 어긴 POST는 거부되고, 아무것도 저장되지 않으며, 응답이 어느 필드가 실패했는지 알려 줍니다.

필드전송한 값메시지
team_size25Must be at most 20
team_size2.5Enter a whole number
date2026-10-30Must be on or after 2026-11-02
phone415 555 0123Use international format, e.g. +14155550123
codews-12Codes look like WS-1234
website모든 값Unknown field "website"

마지막 행이 중요합니다. 필드를 정의하면 Ikiform은 정의에 없는 필드를 거부하므로, 이름이 바뀐 입력은 잘못된 이름으로 저장되는 대신 분명하게 실패합니다. _로 시작하는 필드와 허니팟은 포함되지 않습니다. 모든 타입과 규칙은 필드와 검증에 있습니다.

일반 POST의 경우

코드가 필요 없습니다. 거부된 일반 POST는 422 상태, 필드 레이블 아래의 각 문제, POST가 출발한 페이지로 가는 링크가 담긴 Ikiform 호스팅 오류 페이지를 보여 줍니다. 브라우저의 Back 버튼은 대개 입력한 내용을 유지하고, 링크는 폼을 빈 상태로 다시 불러옵니다.

서버 오류를 필드에 표시하기

스크립트에서는 각 서버 메시지를 브라우저 자체 오류 UI에 넣으세요. setCustomValidity가 그 텍스트로 필드를 유효하지 않게 표시하고, reportValidity가 이를 평소의 말풍선으로 보여 주며 첫 번째 필드에 포커스를 줍니다.

workshop.js
const form = document.querySelector("#workshop");

for (const control of form.elements) {
  control.addEventListener("input", () => control.setCustomValidity?.(""));
}

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'll confirm the date by email.");
    return;
  }
  const fields = result.error.fields ?? {};
  for (const [name, messages] of Object.entries(fields)) {
    form.elements.namedItem(name)?.setCustomValidity?.(messages[0]);
  }
  if (!form.reportValidity()) return;
  alert(result.error.message);
});
  • input에서 커스텀 메시지를 지우세요. 커스텀 오류가 있는 필드는 빈 문자열로 되돌릴 때까지 유효하지 않은 상태로 남고, 그동안 전혀 제출할 수 없습니다.
  • 브라우저 확인은 여전히 submit 이벤트 전에 실행되므로 서버는 이를 통과한 POST만 봅니다.
  • 429 rate_limited처럼 오류에 fields가 없으면 reportValidity()가 true를 반환하고 스크립트는 일반 메시지로 대체합니다.

보내기 전에 서버 규칙 확인하기

이메일 도메인 목록 같은 서버 전용 규칙을 요청 전에 잡으려면 폼의 공개 정의를 불러와 SDK의 validate를 실행하세요. 같은 필드 규칙과 메시지를 브라우저에서 적용합니다.

workshop.js
import { getSchema, validate } from "@ikiform/sdk";

const form = document.querySelector("#workshop");
const result = await getSchema("frm_xxxxxxxxxxxxxxxx");
const fields = result.ok ? result.schema.fields : [];

form.addEventListener("submit", (event) => {
  const errors = validate(fields, new FormData(form));
  for (const control of form.elements) control.setCustomValidity?.(errors[control.name] ?? "");
  if (!form.reportValidity()) event.preventDefault();
});
이는 편의 기능으로 여기세요. 기준은 여전히 서버이며, getSchema는 어차피 공개인 필드 정의만 노출합니다. 누구나 /f/<formId>/schema에서 읽을 수 있습니다.

작동 확인하기

  1. 폼의 Allowed domains에 사이트 도메인을 추가하고, 개발하는 동안에는 localhost도 추가하세요. 비워 두면 어디서든 POST를 받습니다.
  2. 빈 폼을 제출하세요. 브라우저가 Your name에서 멈추고 Network 패널에는 아무것도 나타나지 않습니다.
  3. 개발자 도구에서 팀 규모의 max를 제거하고 25를 입력해 제출하세요. 일반 POST는 Ikiform의 오류 페이지를 보여 주고, 스크립트 버전은 필드에 Must be at most 20을 보여 줍니다.
  4. 브라우저를 건너뛰고 curl로 POST하세요. curl -H "Accept: application/json" -H "Origin: https://example.com" -d team_size=abc https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx에서 origin은 자신의 허용 도메인으로 바꾸세요. 응답은 team_size와 빠진 모든 필수 필드에 대한 메시지가 담긴 422입니다.
  5. 대시보드에서 Submissions를 여세요. 보낸 모든 필드가 담긴 새 제출이 있습니다.

주의할 점

문제원인과 해결
서버 오류 뒤 폼이 전혀 제출되지 않음커스텀 유효성 메시지를 지우지 않았습니다. input에서 setCustomValidity("")로 초기화하세요
미리 채워진 값에 minlength가 발생하지 않음tooShort는 사람이 필드를 편집한 뒤에만 적용됩니다. 서버 규칙이 이를 잡습니다
pattern이 완전히 무시됨v 플래그 기준으로 유효하지 않으며, 브라우저는 유효하지 않은 패턴을 통째로 건너뜁니다. 클래스 안의 -와 기타 특수 문자를 이스케이프하세요
입력하기 전에 오류가 표시됨CSS가 :invalid를 씁니다. :user-invalid로 바꾸세요
422 validation_failedPOST가 폼의 필드 정의와 맞지 않습니다. error.fields에 각 필드와 이유가 나옵니다. 입력의 name을 필드 ID와 맞추세요.

전체 제출 흐름은 개요에 있고, 오류에 POST가 받을 수 있는 모든 코드가 나열되어 있습니다.

질문

막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.