04.1 기본 검증만으론 왜 부족해요?
브라우저 기본 검증은 정말 편해요. required, pattern, min/max만으로도 웬만한 건 다 막히죠. 그런데 실무에서 딱 막히는 순간이 와요. 바로 여러 칸을 서로 비교해야 할 때예요.
가장 흔한 예가 비밀번호 확인이에요. "비밀번호"와 "비밀번호 확인" 두 칸의 값이 같은지 검사해야 하는데, required나 pattern으론 이걸 못 해요. 이 속성들은 한 칸만 볼 뿐, 옆 칸이랑 비교하는 건 못 하거든요.
이럴 때 자바스크립트로 직접 검증을 짜는 게 커스텀 검증(custom validation, 맞춤 검증)이에요. 그런데 여기서 브라우저가 준 좋은 도구가 하나 있어요. Constraint Validation API예요.
04.2 Constraint Validation API가 뭐예요?
Constraint Validation API(제약 검증 API)는 브라우저의 검증 기능을 자바스크립트에서 직접 다룰 수 있게 해주는 도구 모음이에요. 이름은 거창한데, 실무에서 쓰는 건 몇 개 안 돼요.
핵심은 세 가지예요. 값이 규칙에 맞는지 물어보는 checkValidity(), 뭐가 왜 틀렸는지 알려주는 validity 객체, 그리고 내 에러 메시지를 심는 setCustomValidity()예요. 이 셋만 알아도 커스텀 검증의 대부분을 만들 수 있어요.
왜 이걸 쓰냐면요. 순수 if문으로 다 짜도 되지만, 이 API를 쓰면 브라우저의 기본 검증(required 같은 것)과 내 검증이 하나로 합쳐지거든요. 따로 노는 게 아니라 같은 체계 안에서 굴러가요.
04.3 checkValidity랑 validity 객체는 어떻게 봐요?
먼저 어떤 칸이 규칙을 지켰는지 물어보는 방법이에요. 입력 요소에 대고 checkValidity()를 부르면 true나 false를 돌려줘요.
상황: 이메일 칸이 유효한지 확인해요.
const email = document.querySelector('#email');
if (email.checkValidity()) {
console.log('통과');
} else {
console.log('문제 있음');
}
그런데 false가 나왔을 때 뭐가 틀렸는지 알고 싶잖아요. 그때 보는 게 validity 객체예요. 이 안에는 어떤 규칙을 어겼는지가 항목별로 true/false로 담겨 있어요.
const v = email.validity;
v.valueMissing // required인데 비었으면 true
v.typeMismatch // 이메일 형식이 아니면 true
v.tooShort // minlength보다 짧으면 true
v.patternMismatch // pattern에 안 맞으면 true
v.valid // 전부 통과면 true
이걸 활용하면 상황마다 다른 메시지를 보여줄 수 있어요. 비었을 땐 "필수 항목이에요", 형식이 틀렸을 땐 "이메일 형식을 확인하세요"처럼요.
04.4 setCustomValidity로 내 메시지를 넣어봐요
이제 진짜 커스텀 검증의 핵심이에요. setCustomValidity()는 입력 칸에 "이 칸은 지금 이 이유로 잘못됐다"고 도장을 찍는 함수예요.
사용법은 간단해요. 문제가 있으면 에러 메시지 문자열을 넣어주고, 문제가 없으면 빈 문자열('')을 넣어줘요. 빈 문자열을 넣는 게 "이제 괜찮다"는 뜻이라 이걸 빼먹으면 안 돼요.
const age = document.querySelector('#age');
if (Number(age.value) < 19) {
age.setCustomValidity('만 19세 이상만 가입돼요');
} else {
age.setCustomValidity(''); // 통과 처리
}
이렇게 도장을 찍어두면, 그 칸은 브라우저 눈에도 "유효하지 않은 칸"으로 보여요. 그래서 폼을 제출하려 하면 내가 넣은 그 메시지가 말풍선에 뜨면서 제출이 막혀요. 기본 검증과 완전히 똑같은 방식으로 동작하는 거죠. 이게 이 API를 쓰는 이유예요.
04.5 비밀번호 확인 일치는 어떻게 검사해요?
앞에서 예고한 비밀번호 확인, 이제 진짜로 만들어볼게요. 두 칸의 값이 같은지 비교해서 다르면 에러 도장을 찍는 거예요.
상황: 비밀번호와 확인 칸이 같아야 제출돼요.
const pw = document.querySelector('#pw');
const pw2 = document.querySelector('#pw2');
function checkMatch() {
if (pw.value !== pw2.value) {
pw2.setCustomValidity('비밀번호가 서로 달라요');
} else {
pw2.setCustomValidity('');
}
}
pw2.addEventListener('input', checkMatch);
결과를 볼게요. 사용자가 확인 칸에 글자를 칠 때마다 checkMatch가 돌아요. 두 값이 다르면 확인 칸에 "비밀번호가 서로 달라요" 도장이 찍히고, 그 상태로 제출하면 그 메시지가 뜨며 막혀요. 두 값이 같아지는 순간 빈 문자열로 도장이 지워져서 통과되고요.
이 패턴이 커스텀 검증의 기본 뼈대예요. 입력이 바뀔 때마다 검사하고, 결과에 따라 setCustomValidity로 도장을 찍거나 지운다. 이거 하나만 손에 익히면 응용은 거의 다 돼요.
04.6 기본 말풍선 대신 내가 에러를 그리려면요?
회색 기본 말풍선이 브랜드랑 안 어울릴 때가 많죠. 그럴 땐 03편에서 본 novalidate를 켜고, 에러를 우리가 직접 화면에 그려요.
상황: 제출 시 내가 직접 에러를 렌더링해요.
const form = document.querySelector('form');
const email = document.querySelector('#email');
const msg = document.querySelector('#email-error');
form.addEventListener('submit', (e) => {
if (!email.checkValidity()) {
e.preventDefault(); // 제출 막기
if (email.validity.valueMissing) {
msg.textContent = '이메일은 필수예요';
} else if (email.validity.typeMismatch) {
msg.textContent = '이메일 형식을 확인하세요';
}
}
});
흐름을 보면요. form에 novalidate가 있으니 브라우저는 말풍선을 안 띄워요. 대신 우리가 checkValidity()로 직접 물어보고, 문제가 있으면 preventDefault()로 제출을 막은 뒤, validity 객체를 보고 상황에 맞는 메시지를 우리 요소(#email-error)에 넣어요. 그러면 우리가 CSS로 예쁘게 꾸민 빨간 안내 문구가 원하는 자리에 떠요.
여기서 textContent를 쓴 걸 눈여겨보세요. 사용자가 넣은 값을 다룰 땐 innerHTML 대신 textContent를 써야 보안 사고(입력에 섞인 코드가 실행되는 문제)를 막을 수 있어요. 폼은 사용자 입력을 다루는 곳이니 이 습관이 특히 중요해요.
04.7 오늘 배운 걸 정리해볼게요
커스텀 검증은 기본 검증이 못 하는 부분을 자바스크립트로 메우는 일이었어요. 핵심 도구를 다시 묶어볼게요.
여러 칸을 비교해야 할 땐 Constraint Validation API를 써요. checkValidity()로 유효한지 묻고, validity 객체(valueMissing, typeMismatch 등)로 뭐가 틀렸는지 알아내고, setCustomValidity()로 내 메시지 도장을 찍거나(에러) 지워요(빈 문자열). 기본 말풍선 대신 직접 그리고 싶으면 novalidate를 켜고 preventDefault로 막은 뒤 우리 요소에 textContent로 메시지를 넣고요.
비밀번호 확인 예제 하나만 직접 타이핑해보세요. 두 칸 만들고, input 이벤트에 값 비교를 붙이고, setCustomValidity로 도장을 찍어보면 커스텀 검증의 감이 딱 와요. 그리고 잊지 마세요. 프론트에서 이렇게 꼼꼼히 막아도, 서버 검증은 여전히 따로 있어야 해요. 프론트 검증은 사용자를 위한 친절이지, 마지막 방어선은 아니니까요.