자바스크립트 실전 #4 폼 다루기
#3 fetch와 비동기 UI 에서 데이터를 받아 화면에 반영하는 패턴을 봤습니다. 이번엔 반대 방향으로 — 사용자 입력을 받아 검증하고 서버로 보내는 단계를 다룹니다.
HTML 빌트인 검증부터 #
자바스크립트로 검증을 직접 짜기 전에, HTML 5 빌트인 검증부터 활용하세요.
<form id="signup">
<input name="email" type="email" required>
<input name="password" type="password" minlength="8" required>
<input name="age" type="number" min="14" max="120">
<input name="username" pattern="[a-z0-9_]{3,20}" required>
<button type="submit">가입</button>
</form>이 자체로 — 빈 필드 막기, 이메일 형식 검사, 길이/범위 검사, 정규식 패턴까지 모두 작동합니다. 폼 제출 시 브라우저가 자동으로 검사해서 첫 invalid 필드에 에러 풍선까지 보여줍니다.
빌트인이 다 되는데 자바스크립트로 다시 만드는 건 보통 낭비입니다. 빌트인을 기본으로 두고, 자바스크립트는 보강으로 들어갑니다.
빌트인 검증 활용 — Constraint Validation API #
자바스크립트에서 빌트인 검증을 활용하는 도구.
const input = document.querySelector('input[name="email"]');
input.validity.valid; // 모두 통과?
input.validity.valueMissing; // required 위반
input.validity.typeMismatch; // type=email 형식 위반
input.validity.tooShort; // minlength 위반
input.validity.patternMismatch; // pattern 위반
// ... 등
input.validationMessage; // 브라우저가 보여줄 메시지
폼 단위로:
form.checkValidity(); // 폼 전체 valid 한가
form.reportValidity(); // 검사하고 invalid 면 풍선 표시
이 API를 쓰면 빌트인 검증을 그대로 활용하면서 — 우리만의 UI 메시지를 출력할 수 있습니다.
커스텀 에러 메시지 #
기본 메시지가 어색하면 (영어/너무 짧음/번역 부족) 우리가 정해줄 수 있습니다.
input.addEventListener('input', () => {
if (input.value && !/^[a-z]+$/.test(input.value)) {
input.setCustomValidity('소문자만 가능합니다');
} else {
input.setCustomValidity(''); // 비워야 valid 로 간주
}
});setCustomValidity('빈 문자열이 아닌 메시지')가 있으면 입력은 invalid로 간주됩니다. 검사를 풀려면 빈 문자열로 다시 설정.
검증 + 표시 — 직접 하는 패턴 #
빌트인 풍선이 마음에 안 들면 우리가 직접 출력. 자주 쓰는 모양:
<form id="signup" novalidate>
<label>
이메일
<input name="email" type="email" required>
<span class="error" data-for="email"></span>
</label>
<label>
비밀번호
<input name="password" type="password" minlength="8" required>
<span class="error" data-for="password"></span>
</label>
<button type="submit">가입</button>
</form>novalidate 어트리뷰트가 핵심 — 브라우저 자동 검증을 끕니다. 우리가 모두 처리합니다.
function showError(name, message) {
form.querySelector(`[data-for="${name}"]`).textContent = message;
}
function clearErrors() {
form.querySelectorAll('.error').forEach((el) => (el.textContent = ''));
}
form.addEventListener('submit', (e) => {
e.preventDefault();
clearErrors();
let hasError = false;
for (const field of form.elements) {
if (!field.name) continue;
if (!field.checkValidity()) {
showError(field.name, field.validationMessage);
hasError = true;
}
}
if (hasError) return;
const formData = new FormData(form);
submit(Object.fromEntries(formData));
});빌트인 검증을 자바스크립트가 활용하면서, 메시지는 우리 마크업에 출력. 이 방식이 가장 흔하고 견고합니다.
FormData — 폼 입력을 한 번에 모으기
#
리액트 시리즈 에서 본 도구가 바닐라에서도 똑같이 동작합니다.
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!form.checkValidity()) return form.reportValidity();
const formData = new FormData(form);
// 단일 필드
formData.get('email'); // 'me@example.com'
// 모든 키-값
for (const [key, value] of formData) {
console.log(key, value);
}
// 객체로 변환
const data = Object.fromEntries(formData);
// { email: '...', password: '...', ... }
await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
});new FormData(form)에 폼을 통째로 넘기면 모든 입력을 자동 수집합니다. name 속성이 있는 필드만 포함됩니다.
FormData 그대로 보내기 — multipart #
JSON으로 변환하지 않고 FormData를 그대로 fetch 본문으로 보낼 수도 있습니다. 파일 업로드가 있을 때 이쪽이 자연스럽습니다.
form.addEventListener('submit', async (e) => {
e.preventDefault();
await fetch('/api/upload', {
method: 'POST',
body: new FormData(form),
// Content-Type 직접 적지 말 것 — 브라우저가 boundary 포함 자동 설정
});
});중급 #6 fetch API 에서 본 패턴.
체크박스 / 다중 선택 다루기 #
<input type="checkbox" name="tags" value="js">
<input type="checkbox" name="tags" value="ts">
<input type="checkbox" name="tags" value="react">
<select name="category" multiple>
<option value="news">뉴스</option>
<option value="tip">팁</option>
</select>const formData = new FormData(form);
formData.get('tags'); // 첫 번째 값만 ('js')
formData.getAll('tags'); // ['js', 'ts'] — 선택된 모두
formData.getAll('category'); // 다중 선택의 모든 값
get은 첫 값, getAll은 배열로 모두. 체크박스 그룹은 항상 getAll을 써야 안전합니다.
Object.fromEntries(formData)는 같은 키 여러 값이 있으면 마지막만 남으니, 다중 값이 있는 폼에서는 직접 처리해야 합니다.
const data = {};
for (const key of new Set(formData.keys())) {
const all = formData.getAll(key);
data[key] = all.length > 1 ? all : all[0];
}자주 쓰는 검증 패턴들 #
1) 두 비밀번호 일치 확인 #
빌트인 검증으로는 못 잡는 경우.
const password = form.elements.password;
const confirm = form.elements.passwordConfirm;
function syncPasswordMatch() {
if (password.value !== confirm.value) {
confirm.setCustomValidity('비밀번호가 일치하지 않습니다');
} else {
confirm.setCustomValidity('');
}
}
password.addEventListener('input', syncPasswordMatch);
confirm.addEventListener('input', syncPasswordMatch);2) 비동기 검증 — 사용자명 중복 확인 #
서버에 물어봐야 알 수 있는 검증.
const username = form.elements.username;
const checkUsername = debounce(async (value) => {
if (!value) return;
const res = await fetch(`/api/check-username?u=${value}`);
const { available } = await res.json();
username.setCustomValidity(available ? '' : '이미 사용 중인 이름입니다');
}, 400);
username.addEventListener('input', (e) => checkUsername(e.target.value));#3 fetch와 비동기 UI 의 디바운스 패턴이 그대로 들어갑니다.
제출 중 — 중복 제출 방지 #
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!form.checkValidity()) return form.reportValidity();
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.disabled = true;
try {
const data = Object.fromEntries(new FormData(form));
await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
showSuccess('가입 완료');
form.reset();
} catch (err) {
showError(err.message);
} finally {
submitBtn.disabled = false;
}
});finally가 핵심 — 성공/실패 무관하게 버튼을 다시 활성화합니다. 사용자가 같은 폼을 두 번 제출하는 사고를 막습니다.
폼 리셋과 초기값 #
form.reset(); // 모든 필드를 초기값으로
input.defaultValue; // 초기 value (HTML 에 적힌 값)
input.value; // 현재 값
reset은 <input value="...">처럼 HTML에 적힌 초기값으로 되돌려요. 자바스크립트로 동적으로 채운 값은 초기값이 아니에요 — 명시적으로 비워주려면 input.value = ''.
change vs input — 어떤 이벤트를 들을까
#
input 매 키 입력 / 매 변화 — 실시간
change 포커스 잃을 때 / select 변경 시 — 변경 완료실시간 검증은 input. “입력이 끝났을 때” 무거운 작업은 change 또는 디바운스된 input 사용.
blur 시점 검증 #
input.addEventListener('blur', () => {
if (!input.checkValidity()) {
input.classList.add('invalid');
showError(input.name, input.validationMessage);
} else {
input.classList.remove('invalid');
showError(input.name, '');
}
});타이핑 중에는 빨간 줄을 안 보여주고, 포커스가 나갈 때만 검사하는 패턴. 사용자에게 덜 거슬려요.
접근성 — 폼은 특히 중요 #
<label for="email">이메일</label>
<input id="email" name="email" type="email" required
aria-describedby="email-error">
<span id="email-error" class="error" role="alert"></span>세 가지 핵심:
<label for="...">또는<label>안에 input을 두기aria-describedby로 에러 영역과 연결- 에러 영역에
role="alert"또는aria-live="polite"로 변경 시 스크린 리더가 읽음
빌트인 메시지는 자동으로 접근성을 가지지만, 우리가 직접 출력하는 에러는 위 셋업이 필요합니다.
마무리 #
이번 글에서 정리한 내용:
- HTML 빌트인 검증부터 활용 (
required,type,pattern,min/max) - ValidityState API로 자바스크립트에서 활용
setCustomValidity로 커스텀 메시지novalidate+ 직접 처리 패턴이 가장 흔함FormData로 폼 입력 한 번에 수집- 파일 포함 시 FormData 그대로 보내기 (Content-Type 자동)
getAll로 다중 값 (체크박스 그룹, multi select)- 비밀번호 일치, 비동기 사용자명 검증 같은 보강 패턴
- 제출 중
disabled+finally로 복구 - 접근성 —
label,aria-describedby,role="alert"
다음 글(#5 로컬 스토리지와 가벼운 상태 관리)에서는 데이터를 브라우저에 저장하는 도구들과, 라이브러리 없이 화면 상태를 깔끔하게 관리하는 패턴을 다룹니다.