Using HTML form validation and the Constraint Validation API

김동현·2026년 3월 2일

mdn 학습 번역 - HTML

목록 보기
19/31

HTML 폼 유효성 검사 및 제약 조건 유효성 검사 API 사용하기 (Using HTML form validation and the Constraint Validation API)

웹 폼(Web forms)을 만드는 건 예전부터 항상 꽤 복잡한 작업이었어요. 폼 자체를 마크업(HTML 작성)하는 건 쉽지만, 사용자가 입력한 각 필드의 값이 유효하고 논리적으로 맞는지 확인하는 건 훨씬 어렵거든요. 게다가 사용자에게 문제가 있다고 알려주는 과정은 골칫거리가 되기 십상입니다.

HTML5는 폼을 위한 새로운 메커니즘을 도입했어요. <input> 요소를 위한 새로운 시맨틱(의미론적) 타입들을 추가했고, 클라이언트 측(브라우저)에서 폼 콘텐츠를 확인하는 작업을 쉽게 만들어주기 위해 제약 조건 유효성 검사(constraint validation)라는 기능을 추가했죠. 덕분에 JavaScript가 없어도 새로운 속성들만 설정해주면 기본적이고 일상적인 제약 조건들을 검사할 수 있게 되었습니다. 좀 더 복잡한 제약 조건들은 '제약 조건 유효성 검사 API(Constraint Validation API)'를 사용해서 테스트할 수 있고요.

예제와 함께 이 개념들에 대한 기본적인 소개를 보시려면 폼 유효성 검사 튜토리얼(Form validation tutorial)을 참고해 보세요.

참고 (Note):
HTML 제약 조건 유효성 검사가 있다고 해서 서버 측(Server side)에서의 유효성 검사가 필요 없어지는 건 절대 아닙니다. 유효하지 않은 폼 요청이 훨씬 줄어들 것으로 예상되긴 하지만, 여전히 다음과 같은 여러 가지 방법으로 유효하지 않은 데이터가 서버로 전송될 수 있거든요:

  • 브라우저의 개발자 도구를 통해 HTML을 조작하는 경우
  • 폼을 거치지 않고 직접 HTTP 요청을 수동으로 조작해서 보내는 경우
  • 프로그램(스크립트)을 이용해 폼에 내용을 작성하는 경우 (일부 제약 조건 유효성 검사는 사용자가 직접 입력할 때만 실행되고, JavaScript를 사용해 폼 필드 값을 설정할 때는 실행되지 않아요).

그러므로 항상 클라이언트 측에서 수행하는 것과 일관되게, 서버 측에서도 폼 데이터를 유효성 검사해야 합니다.

💡 강사의 팁: 실제 포트폴리오 프로젝트에 이런 디테일한 클라이언트 측 폼 검증을 적용해두면, 면접관에게 '훌륭한 사용자 경험(UX)을 고민할 줄 아는 프론트엔드 개발자'라는 아주 좋은 인상을 줄 수 있습니다! 프론트엔드에서 미리 1차적으로 걸러주면 서버의 부담도 덜 수 있고 사용자는 즉각적인 피드백을 받을 수 있어서 일석이조랍니다. 하지만 보안을 위해서는 백엔드에서의 2차 검증이 필수라는 점을 꼭 기억하세요!

내재적 및 기본 제약 조건 (Intrinsic and basic constraints)

HTML에서 기본 제약 조건은 두 가지 방식으로 선언됩니다:

  • <input> 요소의 type 속성에 의미상 가장 적절한 값을 선택하는 방식입니다. 예를 들어, email 타입을 선택하면 값이 유효한 이메일 주소인지 확인하는 제약 조건이 자동으로 생성됩니다.
  • 유효성 검사와 관련된 속성(attributes)에 값을 설정하는 방식입니다. 이를 통해 JavaScript 없이도 기본적인 제약 조건들을 설명할 수 있습니다.

시맨틱 입력 타입 (Semantic input types)

type 속성에 대한 내재적(Intrinsic) 제약 조건들은 다음과 같습니다:

입력 타입 (Input type)제약 조건 설명 (Constraint description)관련된 위반 (Associated violation)
<input type="URL">값은 https://url.spec.whatwg.org 에 정의된 대로 절대 경로 URL이어야 합니다.TypeMismatch 제약 조건 위반
<input type="email">값은 구문적으로 유효한 이메일 주소여야 합니다. 일반적으로 username@hostname.tld 형식을 갖지만, username@hostname과 같은 로컬 주소일 수도 있습니다.TypeMismatch 제약 조건 위반

email 입력 타입의 경우, multiple 속성이 설정되어 있다면 쉼표(,)로 구분된 목록으로 여러 개의 값을 설정할 수 있습니다. 목록 안의 값 중 하나라도 여기에 설명된 조건을 만족하지 않으면, Type mismatch(타입 불일치) 제약 조건 위반이 트리거됩니다.

대부분의 입력 타입들은 내재적인 제약 조건을 가지고 있지 않다는 점을 참고하세요. 일부 타입들은 제약 조건 유효성 검사 대상에서 제외되거나, 잘못된 값을 올바른 기본값으로 변환해주는 살균(sanitization) 알고리즘을 가지고 있기 때문입니다.

위에서 설명한 type 속성에 더해서, 다음과 같은 속성들이 기본적인 제약 조건들을 정의하는 데 사용됩니다:

속성 (Attribute)속성을 지원하는 입력 타입 (Input types supporting the attribute)가능한 값 (Possible values)제약 조건 설명 (Constraint description)관련된 위반 (Associated violation)
patterntext, search, url, tel, email, passwordJavaScript 정규 표현식 (Regular expression) (단, global, ignoreCase, multiline 플래그는 비활성화된 상태로 컴파일됨)값이 패턴과 일치해야 합니다.patternMismatch 제약 조건 위반
minrange, number

date, month, week

datetime-local, time
유효한 숫자

유효한 날짜

유효한 날짜 및 시간
값이 설정된 값보다 크거나 같아야 합니다.rangeUnderflow 제약 조건 위반
maxrange, number

date, month, week

datetime-local, time
유효한 숫자

유효한 날짜

유효한 날짜 및 시간
값이 설정된 값보다 작거나 같아야 합니다.rangeOverflow 제약 조건 위반
requiredtext, search, url, tel, email, password, date, datetime-local, month, week, time, number, checkbox, radio, file; 또한 <select><textarea> 요소에도 사용 가능없음. 이는 불리언(Boolean) 속성이기 때문에, 속성이 존재한다는 것 자체가 true를 의미하고, 없으면 false를 의미합니다.(체크/입력된) 값이 반드시 있어야 합니다.valueMissing 제약 조건 위반
stepdate

month

week

datetime-local, time

range, number
정수(일 단위)

정수(월 단위)

정수(주 단위)

정수(초 단위)

정수
stepany 문자열로 설정되지 않은 한, 값은 min + (step의 정수 배수) 형태여야 합니다.stepMismatch 제약 조건 위반
minlengthtext, search, url, tel, email, password; 또한 <textarea> 요소정수(길이)입력값이 비어있지 않다면, 문자 수(코드 포인트)가 이 속성의 값보다 작아서는 안 됩니다. <textarea>의 경우 모든 줄바꿈은 (CRLF 쌍이 아닌) 단일 문자로 정규화됩니다.tooShort 제약 조건 위반
maxlengthtext, search, url, tel, email, password; 또한 <textarea> 요소정수(길이)문자 수(코드 포인트)가 이 속성의 값을 초과해서는 안 됩니다.tooLong 제약 조건 위반

💡 강사의 팁: 정규표현식(Regex)을 알아두시면 pattern 속성을 사용할 때 그 위력을 확실히 체감하실 수 있을 거예요. 실무에서는 회원가입 폼의 비밀번호 복잡도 검사나 휴대폰 번호 형식을 체크할 때 pattern 속성이 정말 쏠쏠하게 사용됩니다.


제약 조건 유효성 검사 프로세스 (Constraint validation process)

제약 조건 유효성 검사는 제약 조건 유효성 검사 API를 통해서 이루어집니다. 이는 단일 폼 요소에서 수행될 수도 있고, 폼 레벨인 <form> 요소 자체에서 수행될 수도 있어요. 유효성 검사는 다음과 같은 방식들로 진행됩니다:

  • 폼과 연결된 DOM 인터페이스(HTMLInputElement, HTMLSelectElement, HTMLButtonElement, HTMLOutputElement 또는 HTMLTextAreaElement)의 checkValidity() 또는 reportValidity() 메서드를 호출하는 방식. 이 방식은 해당 요소의 제약 조건만 평가하기 때문에 스크립트가 정보를 얻는 데 유용합니다. checkValidity() 메서드는 해당 요소의 값이 제약 조건을 통과했는지를 나타내는 불리언(Boolean) 값을 반환합니다. (이 과정은 보통 사용자 에이전트(브라우저)가 CSS 가상 클래스인 :valid 또는 :invalid 중 어떤 것을 적용할지 결정할 때 수행됩니다.) 이와 달리, reportValidity() 메서드는 제약 조건 실패 결과를 사용자에게 직접 시각적으로 보고해줍니다.
  • HTMLFormElement 인터페이스(form 태그 자체)에서 checkValidity() 또는 reportValidity() 메서드를 호출하는 방식.
  • 폼 자체를 제출(Submit)하는 방식.

checkValidity()를 호출하는 것은 제약 조건을 정적으로(statically) 검증한다고 부르고, reportValidity()를 호출하거나 폼을 제출하는 것은 제약 조건을 상호작용적으로(interactively) 검증한다고 부릅니다.

참고 (Note):

  • 만약 <form> 요소에 novalidate 속성이 설정되어 있다면, 제약 조건에 대한 상호작용적 유효성 검사는 일어나지 않습니다.
  • HTMLFormElement 인터페이스에서 submit() 메서드를 프로그래밍 방식으로 호출하는 것은 제약 조건 유효성 검사를 트리거하지 않아요. 다시 말해, 이 메서드는 데이터가 제약 조건을 만족하지 않더라도 서버로 폼 데이터를 그냥 보내버립니다. 대신 제출 버튼 요소의 click() 메서드를 호출하세요.
  • minlengthmaxlength 제약 조건은 오직 사용자가 직접 제공한(입력한) 값에 대해서만 검사됩니다. JavaScript 코드로 프로그래밍 방식으로 값을 설정한 경우에는 명시적으로 checkValidity()reportValidity()를 호출하더라도 검사되지 않습니다.

제약 조건 유효성 검사 API를 사용한 복잡한 제약 조건 (Complex constraints using the Constraint Validation API)

JavaScript와 제약 조건 API(Constraint API)를 함께 사용하면 훨씬 더 복잡한 제약 조건들을 구현할 수 있습니다. 예를 들어, 여러 개의 입력 필드를 결합해서 검사하거나 복잡한 계산이 포함된 제약 조건 말이죠.

기본적인 아이디어는 이렇습니다. 어떤 폼 필드의 이벤트(예: onchange)가 발생했을 때 JavaScript를 트리거하여 제약 조건이 위반되었는지 계산하는 겁니다. 그런 다음 field.setCustomValidity() 메서드를 사용해서 유효성 검사 결과를 설정합니다: 빈 문자열("")은 제약 조건이 만족되었음을 의미하고, 다른 어떤 문자열이든 에러가 있다는 뜻이며, 이 문자열 자체가 사용자에게 보여줄 에러 메시지가 됩니다.

여러 필드를 결합한 제약 조건: 우편번호 유효성 검사 (Constraint combining several fields: Postal code validation)

우편번호 형식은 국가마다 다릅니다. 많은 국가에서 국가 코드 접두사(예: 독일의 D-, 프랑스의 F-, 스위스의 CH-)를 선택적으로 허용하죠. 어떤 국가들은 우편번호에 정해진 개수의 숫자만 사용하지만, 영국 같은 다른 국가들은 특정 위치에 문자가 들어갈 수 있는 더 복잡한 형식을 사용하기도 합니다.

참고 (Note):
이것은 완벽한 우편번호 유효성 검사 라이브러리가 아니라, 핵심 개념을 보여드리기 위한 데모입니다.

예시로 폼의 제약 조건을 검증하는 스크립트를 하나 추가해 보겠습니다:

<form>
  <label for="postal-code">Postal Code: </label>
  <input type="text" id="postal-code" />
  <label for="country">Country: </label>
  <select id="country">
    <option value="ch">Switzerland</option>
    <option value="fr">France</option>
    <option value="de">Germany</option>
    <option value="nl">The Netherlands</option>
  </select>
  <input type="submit" value="Validate" />
</form>

(MDN Playground에서 실행해볼 수 있습니다: Play)

이 코드는 폼을 하나 화면에 렌더링합니다.

먼저 제약 조건 자체를 확인하는 함수를 하나 작성합니다:

const countrySelect = document.getElementById("country");
const postalCodeField = document.getElementById("postal-code");

function checkPostalCode() {
  // 각 국가별로 우편번호가 따라야 할 패턴을 정의합니다.
  const constraints = {
    ch: [
      "^(CH-)?\\d{4}$",
      "Swiss postal codes must have exactly 4 digits: e.g. CH-1950 or 1950",
    ],
    fr: [
      "^(F-)?\\d{5}$",
      "French postal codes must have exactly 5 digits: e.g. F-75012 or 75012",
    ],
    de: [
      "^(D-)?\\d{5}$",
      "German postal codes must have exactly 5 digits: e.g. D-12345 or 12345",
    ],
    nl: [
      "^(NL-)?\\d{4}\\s*([A-RT-Z][A-Z]|S[BCE-RT-Z])$",
      "Dutch postal codes must have exactly 4 digits, followed by 2 letters except SA, SD and SS",
    ],
  };

  // 선택된 국가 id를 읽어옵니다.
  const country = countrySelect.value;

  // 제약 조건 검사기(정규식)를 만듭니다.
  const constraint = new RegExp(constraints[country][0], "");
  console.log(constraint);

  // 검사해 봅시다!
  if (constraint.test(postalCodeField.value)) {
    // 우편번호가 제약조건을 통과하면 ConstraintAPI를 이용해 브라우저에 알려줍니다 (빈 문자열)
    postalCodeField.setCustomValidity("");
  } else {
    // 제약조건을 통과하지 못했다면, ConstraintAPI를 이용해 
    // 해당 국가에 필요한 형식에 대한 에러 메시지를 설정해 줍니다.
    postalCodeField.setCustomValidity(constraints[country][1]);
  }
}

그런 다음, 이 함수를 <select>change 이벤트와 <input>input 이벤트에 연결해 줍니다:

countrySelect.addEventListener("change", checkPostalCode);
postalCodeField.addEventListener("input", checkPostalCode);

💡 강사의 팁: 여기서 input 이벤트에 리스너를 달았다는 점에 주목하세요! 사용자가 키보드로 타이핑을 할 때마다 실시간으로 유효성 검사를 하겠다는 뜻입니다. UX 측면에서 아주 좋은 접근법이죠!

업로드 전 파일 크기 제한하기 (Limiting the size of a file before its upload)

또 다른 일반적인 제약 조건은 업로드할 파일의 크기를 제한하는 것입니다. 파일이 서버로 전송되기 전에 클라이언트 측에서 미리 파일 크기를 확인하려면, 제약 조건 유효성 검사 API(특히 field.setCustomValidity() 메서드)와 또 다른 JavaScript API인 'File API'를 결합해야 합니다.

아래는 HTML 부분입니다:

<label for="fs">Select a file smaller than 75 kB: </label>
<input type="file" id="fs" />

(MDN Playground에서 실행해볼 수 있습니다: Play)

이 코드는 화면에 파일 선택 버튼을 렌더링합니다.

JavaScript 코드는 선택된 파일을 읽고, File.size() 메서드를 사용해 크기를 알아낸 다음, 하드코딩된 제한(limit) 값과 비교합니다. 그리고 위반 사항이 있을 경우 Constraint API를 호출하여 브라우저에 알립니다:

const fs = document.getElementById("fs");

function checkFileSize() {
  const files = fs.files;

  // 파일이 (최소) 하나라도 선택되었다면
  if (files.length > 0) {
    if (files[0].size > 75 * 1000) {
      // 제약 조건을 확인합니다.
      fs.setCustomValidity("The selected file must not be larger than 75 kB");
      fs.reportValidity();
      return;
    }
  }
  // 사용자가 정의한 제약 조건 위반 사항이 없음
  fs.setCustomValidity("");
}

마지막으로, 적절한 이벤트에 이 메서드를 연결해 줍니다:

fs.addEventListener("change", checkFileSize);

제약 조건 유효성 검사의 시각적 스타일링 (Visual styling of constraint validation)

제약 조건을 설정하는 것 외에도, 웹 개발자들은 사용자에게 어떤 메시지가 표시되는지, 그리고 그 메시지들이 시각적으로 어떻게 스타일링되는지 제어하고 싶어 합니다.

요소의 시각적 모습 제어하기 (Controlling the look of elements)

요소의 디자인은 CSS 가상 클래스(pseudo-classes)를 통해 제어할 수 있습니다.

:required 및 :optional CSS 가상 클래스

:required:optional 가상 클래스를 사용하면 required 속성을 가지고 있거나 가지고 있지 않은 폼 요소에만 매칭되는 CSS 선택자를 작성할 수 있습니다.

:placeholder-shown CSS 가상 클래스

:placeholder-shown을 참조하세요.

:valid 및 :invalid CSS 가상 클래스

:valid:invalid 가상 클래스는 입력 타입 설정에 따라 내용이 유효성 검사를 통과한 <input> 요소와 통과하지 못한 요소를 각각 나타내는 데 사용됩니다. 이 클래스들을 활용하면 사용자가 올바르게 형식에 맞춰 작성한 폼 요소와 잘못 작성한 요소를 시각적으로 쉽게 구별할 수 있도록 스타일링할 수 있습니다.

제약 조건 위반 텍스트 제어하기 (Controlling the text of constraint violation)

다음 항목들을 통해 제약 조건 위반 텍스트를 제어할 수 있습니다:

  • 다음 요소들에서 제공하는 setCustomValidity(message) 메서드:

    • <fieldset>.
      참고: fieldset 요소에 커스텀 유효성 검사 메시지를 설정하더라도, 대부분의 브라우저에서 폼 제출을 막지는 않습니다. <fieldset>은 '입력 요소'가 아닙니다
      브라우저가 폼을 제출할 때 유효성 검사(Validation)를 수행하고 제출을 막는 대상은 보통 값을 직접 입력받는 요소들입니다. <fieldset>은 여러 입력 요소들을 하나로 묶어주는 '컨테이너' 역할을 할 뿐, 그 자체가 어떤 '값'을 가지지는 않습니다. 브라우저는 fieldset을 검사 대상(제출을 막아야 할 필수 요소)으로 간주하지 않는 경우가 많습니다. 따라서 내부의 input들이 모두 정상이라면, fieldset에 에러 메시지가 떠 있더라도 브라우저는 그냥 폼을 서버로 보내버립니다.
    • <input>
    • <output>
    • <select>
    • 제출 버튼들 (submit 타입을 가진 <button> 요소이거나, submit 타입을 가진 input 요소로 생성됨). 다른 종류의 버튼들은 제약 조건 유효성 검사에 관여하지 않습니다.
    • <textarea>
  • ValidityState 인터페이스는 위에 나열된 요소들의 validity 속성을 통해 반환되는 객체를 설명합니다. 이 객체는 입력된 값이 유효하지 않게 된 다양한 원인들을 나타내며, 요소의 값이 유효하지 않을 때 왜 유효성 검사를 통과하지 못했는지 설명하는 데 도움을 줍니다.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글