이번 편의 결과물: 문의 폼에 잘못된 값을 입력하면 브라우저 기본 메시지 대신 우리가 정한 문구가 표시됩니다. 다루는 개념: ValidityState와 제약 플래그, setCustomValidity로 커스텀 오류 메시지, checkValidity/reportValidity, :valid/:invalid/:user-invalid
이 편에서 만드는 파일
web-practice/portfolio/
├─ contact.html ~ (각 필드에 오류 문구를 담을 요소 추가, novalidate 유지)
├─ js/
│ └─ main.js ~ (Constraint Validation API로 실시간 검증 로직 추가)
└─ css/
└─ styles.css ~ (:user-invalid 스타일 추가)개념 정리
브라우저 기본 검증 말풍선은 위치·언어·디자인을 바꿀 수 없습니다. Constraint Validation API를 쓰면 검증 로직은 브라우저에 맡기면서 메시지와 표시 방식만 직접 만들 수 있습니다.
ValidityState
모든 <input>/<textarea>/<select>는 .validity 속성에 ValidityState 객체를 갖습니다. 각 값이 true면 해당 문제가 있다는 뜻입니다.
| 플래그 | 참(true)인 경우 |
|---|---|
valueMissing | required인데 값이 비어 있음 |
typeMismatch | email/url 등 형식에 안 맞음 |
tooShort | minlength보다 짧음 |
patternMismatch | pattern과 안 맞음 |
valid | 모든 제약을 통과함 |
setCustomValidity
input.setCustomValidity('메시지')를 호출하면 그 입력은 메시지가 있는 동안 항상 무효 상태가 됩니다. 빈 문자열('')을 넣으면 오류가 해제됩니다. 즉, 검사할 때마다 먼저 ''로 초기화한 뒤 문제가 있으면 메시지를 다시 넣는 순서를 지켜야 합니다.
checkValidity vs reportValidity
| 메서드 | 하는 일 |
|---|---|
checkValidity() | 유효성만 검사하고 true/false 반환. 화면에 아무것도 표시 안 함 |
reportValidity() | 검사 후 유효하지 않으면 브라우저 기본 말풍선(또는 우리가 그린 오류)을 화면에 표시 |
:valid / :invalid / :user-invalid
CSS로 상태에 따라 스타일을 줄 수 있습니다. :invalid는 페이지를 열자마자(사용자가 아직 아무것도 안 건드려도) 빈 필수 필드에 바로 걸립니다. 사용자가 손대지도 않았는데 빨간 테두리가 뜨면 어색하므로, 사용자가 값을 입력한 뒤에만 걸리는 :user-invalid를 씁니다.
실습
1. 파일 열기
web-practice/portfolio/contact.html, js/main.js, css/styles.css를 엽니다.
2. 코드 작성
각 필드 아래에 오류 문구를 담을 <p>를 추가합니다(이름 필드 예시, 나머지 필드도 같은 패턴으로 추가).
<!-- web-practice/portfolio/contact.html -->
<div class="field">
<label for="name">이름</label>
<input type="text" id="name" name="name" autocomplete="name" required aria-describedby="name-error" />
<p class="error" id="name-error" aria-live="polite"></p>
</div>js/main.js에 검증 로직을 추가합니다.
// web-practice/portfolio/js/main.js
const form = document.querySelector('.contact-form')
const messages = {
name: {
valueMissing: '이름을 입력해 주세요.',
},
email: {
valueMissing: '이메일을 입력해 주세요.',
typeMismatch: '올바른 이메일 형식이 아닙니다.',
},
message: {
valueMissing: '메시지를 입력해 주세요.',
tooShort: '메시지는 10자 이상 입력해 주세요.',
},
agree: {
valueMissing: '개인정보 수집·이용에 동의해야 제출할 수 있습니다.',
},
}
function validateField(field) {
field.setCustomValidity('')
const rules = messages[field.name]
if (!rules) return
const validity = field.validity
let text = ''
for (const [key, msg] of Object.entries(rules)) {
if (validity[key]) {
text = msg
break
}
}
field.setCustomValidity(text)
const errorEl = document.getElementById(`${field.id}-error`)
if (errorEl) {
errorEl.textContent = text
}
}
if (form) {
const fields = form.querySelectorAll('input[name], textarea[name]')
fields.forEach((field) => {
field.addEventListener('input', () => validateField(field))
field.addEventListener('blur', () => validateField(field))
})
form.addEventListener('submit', (event) => {
fields.forEach((field) => validateField(field))
if (!form.checkValidity()) {
event.preventDefault()
form.reportValidity()
}
})
}css/styles.css에 사용자가 값을 건드린 뒤에만 오류 스타일이 보이도록 규칙을 추가합니다.
/* web-practice/portfolio/css/styles.css */
.contact-form input:user-invalid,
.contact-form textarea:user-invalid {
border-color: var(--color-accent);
}
.contact-form .error {
color: var(--color-accent);
font-size: var(--text-sm);
min-height: 1.2em;
}3. 실행
VS Code Live Server로 contact.html을 엽니다.
4. 확인
- 이메일 필드에
abc를 입력하고 다른 필드로 포커스를 옮기면(blur) “올바른 이메일 형식이 아닙니다.”가 필드 아래에 나타나는지 확인합니다. - 메시지 필드에 5자만 입력하면 “메시지는 10자 이상 입력해 주세요.”가 나타나는지 확인합니다.
- 아무것도 입력하지 않은 채 “보내기”를 누르면 각 필수 필드 아래 문구가 채워지고 제출이 막히는지 확인합니다.
- 개발자 도구 콘솔에서
document.querySelector('#email').validity.typeMismatch를 실행해true/false값을 직접 확인합니다.
직접 해보기
- 전화번호 필드(
pattern사용)에도patternMismatch용 메시지를 추가해 보세요.
답 보기
messages 객체에 phone: { patternMismatch: '010-1234-5678 형식으로 입력해 주세요.' }를 추가하면 됩니다. phone은 선택 입력이라 valueMissing은 필요 없습니다.
- 참고 링크(
url타입) 필드가 비어 있어도 되지만 값이 있으면 반드시https://로 시작하게 만들고 싶다면 어떻게 할지 생각해 보세요.
답 보기
pattern="https://.*"를 추가하면 값이 있을 때만 검사되고(빈 값은 required가 없으면 통과), patternMismatch 메시지로 “https://로 시작하는 주소를 입력해 주세요.”를 보여줄 수 있습니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 한 번 오류가 뜨면 값을 고쳐도 안 사라짐 | setCustomValidity를 검사 전에 ''로 초기화하지 않음 | 매 검사 시작 시 setCustomValidity('')부터 호출 |
| 페이지를 열자마자 모든 필수 필드가 빨갛게 표시됨 | :invalid를 써서 사용자가 손대기 전에도 스타일 적용 | :user-invalid로 교체 |
| 제출 버튼을 눌러도 폼이 그대로 전송됨 | submit 이벤트에서 checkValidity만 하고 preventDefault를 안 함 | 유효하지 않을 때 event.preventDefault() 호출 |
| 오류 문구가 스크린 리더에 안 읽힘 | aria-live나 aria-describedby 연결 누락 | 오류 요소에 aria-live="polite", 입력에 aria-describedby 연결 |