frontend/javascript

🟨 2-3. 폼(Form)과 유효성 검사(Validation) — submit, input, change 이벤트로 배우는 입력 검증의 모든 것

mirabo01 2025. 11. 7. 08:48

1. 폼 유효성 검사가 왜 중요한가

웹에서 사용자가 입력한 정보는 항상 신뢰할 수 없다.
잘못된 이메일, 빈칸, 특수문자, 중복 비밀번호…
이런 데이터를 서버에 그대로 보내면 보안과 서비스 품질 모두 떨어진다.

그래서 브라우저가 보내기 전에 클라이언트 측 검증(Client-side Validation) 을 거친다.
이는 단순히 에러를 막는 용도가 아니라,

“사용자가 실수를 덜 하게 돕는 UX 기능”이다.


2. 예시: 회원가입 폼 만들기

먼저 실습용 HTML을 구성하자.
이건 로그인, 댓글 입력 등에도 바로 응용할 수 있는 가장 기본형이다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <title>폼 유효성 검사</title>
  <style>
    body { font-family: sans-serif; margin: 40px; }
    form { width: 320px; display: flex; flex-direction: column; gap: 12px; }
    input { padding: 8px; border: 1px solid #ccc; border-radius: 6px; }
    button { padding: 8px; background: #4a90e2; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
    .error { color: red; font-size: 13px; }
    .success { color: green; margin-top: 10px; }
  </style>
</head>
<body>

  <h1>회원가입</h1>

  <form id="signupForm" novalidate>
    <label>
      이메일
      <input type="email" id="email" placeholder="example@email.com" />
      <div id="emailError" class="error"></div>
    </label>

    <label>
      비밀번호
      <input type="password" id="password" placeholder="8자 이상" />
      <div id="passwordError" class="error"></div>
    </label>

    <label>
      비밀번호 확인
      <input type="password" id="confirm" placeholder="비밀번호 재입력" />
      <div id="confirmError" class="error"></div>
    </label>

    <button type="submit">가입하기</button>
  </form>

  <div id="result" class="success"></div>

  <script type="module" src="main.js"></script>
</body>
</html>

설명:

  • novalidate 속성으로 브라우저 기본 검증을 비활성화하고 JS로 제어
  • 각 입력 아래에 <div class="error"> 를 만들어 에러 메시지를 표시할 공간 확보

3. 이벤트 구조 설계

우리가 사용할 이벤트는 세 가지다.

이벤트 설명 활용 목적

input 입력값이 바뀔 때마다 발생 실시간 검증
change 입력 후 포커스를 잃을 때 느슨한 검증
submit 폼 전송 시점 최종 전체 검증

이 세 이벤트를 적절히 섞으면
실시간 피드백 + 제출 시 최종 확인 둘 다 가능하다.


4. 자바스크립트 로직 — 유효성 검사 구현

// main.js
const form = document.getElementById("signupForm");
const emailInput = document.getElementById("email");
const passwordInput = document.getElementById("password");
const confirmInput = document.getElementById("confirm");

const emailError = document.getElementById("emailError");
const passwordError = document.getElementById("passwordError");
const confirmError = document.getElementById("confirmError");
const result = document.getElementById("result");

function validateEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

function validatePassword(pw) {
  return pw.length >= 8;
}

function showError(element, message) {
  element.textContent = message;
}

function clearError(element) {
  element.textContent = "";
}

4-1. 실시간 이메일 검증 (input 이벤트)

emailInput.addEventListener("input", () => {
  if (!validateEmail(emailInput.value)) {
    showError(emailError, "유효한 이메일 주소를 입력하세요.");
  } else {
    clearError(emailError);
  }
});

이메일이 입력될 때마다 정규식으로 즉시 검증.


4-2. 비밀번호 검증 (input 이벤트)

passwordInput.addEventListener("input", () => {
  if (!validatePassword(passwordInput.value)) {
    showError(passwordError, "비밀번호는 8자 이상이어야 합니다.");
  } else {
    clearError(passwordError);
  }
});

비밀번호 길이를 실시간으로 확인하고 피드백 제공.
입력값이 길어질 때마다 에러 문구가 사라지는 식의 즉각적 UX는
사용자가 실수를 줄이게 도와준다.


4-3. 비밀번호 일치 확인 (change 이벤트)

confirmInput.addEventListener("change", () => {
  if (passwordInput.value !== confirmInput.value) {
    showError(confirmError, "비밀번호가 일치하지 않습니다.");
  } else {
    clearError(confirmError);
  }
});

비밀번호 확인은 실시간보다
입력 완료(change) 시점에 검증하는 것이 일반적이다.


4-4. 폼 제출(submit) 이벤트로 최종 확인

form.addEventListener("submit", (event) => {
  event.preventDefault(); // 기본 새로고침 방지
  let valid = true;

  if (!validateEmail(emailInput.value)) {
    showError(emailError, "유효한 이메일을 입력하세요.");
    valid = false;
  }

  if (!validatePassword(passwordInput.value)) {
    showError(passwordError, "비밀번호는 8자 이상이어야 합니다.");
    valid = false;
  }

  if (passwordInput.value !== confirmInput.value) {
    showError(confirmError, "비밀번호가 일치하지 않습니다.");
    valid = false;
  }

  if (valid) {
    result.textContent = "✅ 회원가입 성공!";
    form.reset();
  } else {
    result.textContent = "";
  }
});

preventDefault() 로 폼의 기본 새로고침을 막고,
검증 통과 시 메시지를 표시하거나, 실패 시 각 필드에 피드백을 유지한다.


5. UX 개선: 실시간 하이라이트 효과 추가

조금만 다듬으면 실제 서비스 UX와 비슷해진다.

input.valid {
  border-color: #4caf50;
}
input.invalid {
  border-color: #f44336;
}
function toggleValidClass(input, isValid) {
  input.classList.toggle("valid", isValid);
  input.classList.toggle("invalid", !isValid);
}

emailInput.addEventListener("input", () => {
  const ok = validateEmail(emailInput.value);
  toggleValidClass(emailInput, ok);
  ok ? clearError(emailError) : showError(emailError, "이메일 형식이 올바르지 않습니다.");
});

이제 입력창의 테두리 색이 실시간으로 바뀌며,
사용자가 “지금 맞게 입력하고 있는지” 즉시 피드백을 받는다.


6. 고급 예시 — 조건부 비밀번호 검증

좀 더 실전처럼 조건을 여러 개 넣을 수도 있다.
예를 들어 비밀번호는 다음을 모두 포함해야 한다면?

  • 최소 8자
  • 대문자 하나
  • 숫자 하나
function validatePasswordComplex(pw) {
  const length = pw.length >= 8;
  const upper = /[A-Z]/.test(pw);
  const number = /[0-9]/.test(pw);
  return length && upper && number;
}

이런 함수를 기존 validatePassword 대신 적용하면 된다.
이건 은행, 공공기관, 쇼핑몰 등에서 자주 쓰이는 방식이다.


7. change vs input 의 실제 차이

이벤트 발생 시점 실시간 반응 대표 사례

input 입력될 때마다 ✅ 가능 검색창, 이메일, 비밀번호
change 포커스 이탈 시 ❌ 늦음 셀렉트박스, 체크박스
submit 폼 전체 전송 시 전체 확인 가입 버튼, 로그인

→ 실시간 반응이 필요한 입력은 input,
확정 후만 처리하면 되는 경우는 change를 쓰는 게 좋다.


8. 실무 확장 포인트

1) 서버 검증과 연결

  • 실제 서비스에서는 JS 검증 후, 백엔드에서도 한 번 더 확인해야 한다.
  • 클라이언트 검증은 UX 목적, 서버 검증은 보안 목적이다.

2) 중복 확인 기능

  • 이메일 중복 검사는 input 이벤트 + debounce(지연 요청) 을 활용
  • setTimeout 기반으로 “입력 멈춤 후 0.5초 뒤”에 서버 요청을 보내면 좋다.

3) 다국어 메시지 처리

  • 에러 문구를 한 곳에서 관리 (예: errors.js)
  • 향후 언어별로 번역 가능하게 구조화

4) 접근성 고려

  • aria-live="polite" 속성으로 에러 메시지 읽히게
  • 스크린 리더 사용자도 검증 결과를 들을 수 있음

9. 정리

포인트 설명

이벤트 input, change, submit 의 구분이 핵심
검증 단계 실시간 + 최종(전송) 검증을 함께 구성
UX 입력 도중 즉시 피드백 제공
코드 구조 “검증 함수 + 표시 함수”로 역할 분리
확장성 서버 검증, 중복 확인, 다국어 메시지로 발전 가능

10. 마무리

이제 단순히 form.submit()만 아는 게 아니라,
“입력 흐름을 설계하는 개발자” 수준으로 올라왔다.

자바스크립트의 이벤트는 데이터 흐름의 입구다.
입력 → 검증 → 피드백 → 제출,
이 모든 단계가 이벤트로 이어진다.