[JavaScript 시리즈 8편] 폼 검증과 사용자 피드백 설계

English version

상태·렌더 구조 위에 실사용에 가까운 입력 검증 로직을 얹습니다. 서버가 다시 검증하더라도 클라이언트에서 규칙을 명확히 하면 사용자 피로를 줄일 수 있습니다.

이번 글에서 새로 나오는 용어

  1. 폼 검증: 제출 전에 입력값이 규칙을 지켰는지 확인하는 과정입니다.
  2. 패턴 정규식: 이메일처럼 특정 형식을 검사하기 위해 쓰는 규칙 표현식입니다.
  3. blur: 입력 칸에서 포커스가 빠져나갈 때 발생하는 이벤트로, 입력 이후 검증 시점에 활용합니다.
  4. aria-live: 화면 변화 문구를 보조기기가 읽어 주도록 표시하는 접근성 속성입니다.
  5. aria-invalid: 해당 입력이 지금 잘못된 상태임을 보조기기에 알려 주는 속성입니다.

핵심 개념

  • 검증 규칙: 필수 여부, 길이, 패턴, 서로 일치해야 하는 값 등을 정리합니다.
  • 검증 시점: 입력 중 실시간, 입력 후 blur, 제출 순간 중 어떤 시점에 메시지를 보여줄지 결정합니다.
  • 피드백 요소: 경고 텍스트, 테두리 색, aria-live 영역 같은 시각·청각 힌트를 계획합니다.
  • 접근성 고려: aria-invalid, aria-live="polite" 같은 속성을 통해 보조기기가 메시지를 읽을 수 있게 합니다.

코드로 확인하기

const constraints = {
  name: { required: true, minLength: 2 },
  email: { required: true, pattern: /^[\w.-]+@([\w-]+\.)+[\w-]{2,}$/ },
  password: { required: true, minLength: 8 },
  confirmPassword: { matches: "password" },
};

function validateField(name, value, form) {
  const rules = constraints[name];
  if (!rules) return { valid: true };
  if (rules.required && !value.trim()) return { valid: false, message: "필수 입력입니다." };
  if (rules.minLength && value.length < rules.minLength)
    return { valid: false, message: `${rules.minLength}자 이상 입력해주세요.` };
  if (rules.pattern && !rules.pattern.test(value))
    return { valid: false, message: "형식이 올바르지 않습니다." };
  if (rules.matches) {
    const target = form.elements.namedItem(rules.matches);
    if (target && value !== target.value) return { valid: false, message: "값이 일치하지 않습니다." };
  }
  return { valid: true };
}
const form = document.querySelector(".signup-form");
const feedbackEl = document.querySelector(".form-feedback");

form?.addEventListener("input", (event) => {
  const target = event.target;
  if (!(target instanceof HTMLInputElement) || !form) return;
  const { valid, message } = validateField(target.name, target.value, form);
  target.classList.toggle("is-invalid", !valid);
  target.setAttribute("aria-invalid", String(!valid));
  const hint = target.nextElementSibling;
  if (hint) hint.textContent = message ?? "";
});

form?.addEventListener("submit", (event) => {
  event.preventDefault();
  if (!form) return;
  const fields = Array.from(form.elements).filter((el) => el instanceof HTMLInputElement);
  const invalid = fields
    .map((field) => ({ field, result: validateField(field.name, field.value, form) }))
    .find(({ result }) => !result.valid);

  if (invalid) {
    if (feedbackEl) feedbackEl.textContent = invalid.result.message;
    invalid.field.focus();
    return;
  }

  if (feedbackEl) {
    feedbackEl.textContent = "회원가입 요청을 전송했습니다.";
    feedbackEl.classList.add("is-success");
  }
});

실시간 입력과 제출 시 검증을 분리하면 사용자에게 언제 무엇을 고쳐야 하는지 명확히 알려 줄 수 있습니다.

.signup-form input {
  border: 1px solid #c8d0da;
  transition: border-color 0.2s ease;
}

.signup-form input.is-invalid {
  border-color: #ff5757;
  background: rgba(255, 87, 87, 0.1);
}

.signup-form .hint {
  font-size: 0.85rem;
  color: #ff5757;
  min-height: 1.2rem;
}

.form-feedback {
  margin-top: 1rem;
  font-weight: 600;
}

.form-feedback.is-success {
  color: #1ba784;
}

짧은 CSS만으로도 오류 필드를 한눈에 강조할 수 있습니다.

BrowserMock로 검증 흐름 리플레이

폼 검증은 입력 → 메시지 → 제출 차례를 따라야 하므로 BrowserMock으로 UI 상태를 되짚어 보는 것이 도움이 됩니다.


const { document, window } = mock(`
  <form class="signup-form">
    <input name="email" />
    <span class="hint"></span>
    <div class="form-feedback"></div>
  </form>
`);

const form = document.querySelector(".signup-form");
const feedbackEl = document.querySelector(".form-feedback");

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const { valid, message } = validateField("email", form.email.value, form);
  if (!valid) feedbackEl.textContent = message;
});

form.email.value = "broken";
form.dispatchEvent(new window.Event("submit"));
console.log(feedbackEl.textContent); // "형식이 올바르지 않습니다."

이렇게 하면 실제 브라우저를 띄우지 않고도 오류 메시지가 필요한 순간에 나타나는지 즉시 확인할 수 있습니다. 팀 리뷰에서는 BrowserMock 콘솔 로그를 캡처해 상태 변화를 설명하면 UI 합의가 빨라집니다.

왜 중요한가

  • 클라이언트에서 검증을 선행하면 서버까지 요청을 보내기 전에 실수를 바로잡을 수 있어 네트워크 비용과 사용자 좌절감을 줄입니다.
  • 명확한 피드백 구조는 신입생 모집 폼, 동아리 신청서처럼 중요한 입력 흐름에서 신뢰를 높여 줍니다.
  • 접근성을 고려한 메시지는 보조기기를 사용하는 사용자에게도 동일한 정보를 전달합니다.

실습

  • 따라 하기: validateField와 입력/제출 이벤트를 그대로 붙여 하나의 가입 폼에서 동작시키세요.
  • 확장하기: 비밀번호 강도 게이지, 전화번호 마스킹, 서버 오류 메시지 통합 중 두 가지를 추가합니다.
  • 디버깅: 정규식이나 matches 규칙을 일부러 깨서 어떤 메시지가 노출되는지 확인하고, 이벤트가 중복 실행되지 않는지 DevTools로 점검합니다.
  • 완료 기준: 모든 필드가 유효하지 않으면 제출이 막히고, 성공 시 상태 텍스트가 녹색으로 바뀌며 콘솔 경고가 없다면 실습이 끝납니다.

마무리

폼 검증이 탄탄할수록 사용자는 믿음을 느낍니다. 다음 글에서는 웹 스토리지와 데이터 지속성을 연결해 입력값을 안전하게 보관하는 방법을 정리해 보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요