본문 바로가기

기본적인 폼 유효성 검사 구현

필수값·비밀번호 규칙·필드 일치 검사를 구현하고 입력·포커스 이탈·제출 시점에 맞춰 오류 메시지를 표시합니다.

유효성 검사는 입력 오류를 빠르게 알려 사용자 경험을 높이고, 불필요한 요청을 줄이는 데 유용합니다. 브라우저 검사는 우회할 수 있으므로 보안과 데이터 무결성의 최종 책임은 서버에 있습니다.

이 장에서는 클라이언트 측 유효성 검사의 중요성과, 이를 구현하는 기본 방법을 함께 정리합니다.


유효성 검사의 중요성

유효성 검사는 단순히 데이터의 형식을 확인하는 것을 넘어, 다음과 같은 여러 중요한 역할을 합니다.

  • 사용자 경험 개선 (UX): 사용자가 폼을 제출하기 전에 오류를 미리 알려줌으로써, 불필요한 서버 통신과 페이지 새로고침 없이 즉각적인 피드백을 제공합니다. 이는 사용자의 답답함을 줄이고 더 나은 인터랙션을 가능하게 합니다.
  • 데이터 품질 안내: 저장 전에 형식 오류를 발견하도록 돕습니다. 실제 데이터 무결성은 서버 검증과 데이터베이스 제약 조건으로 보장해야 합니다.
  • 보안 경계 구분: 클라이언트 검사는 보안 경계가 아닙니다. 서버 검증과 매개변수화된 쿼리, 출력 문맥에 맞는 인코딩처럼 공격 유형에 맞는 방어를 별도로 적용해야 합니다.
  • 서버 부하 감소: 잘못된 요청이 서버에 도달하기 전에 클라이언트에서 걸러내어 서버의 처리 부담을 줄입니다.
클라이언트 측 vs 서버 측 유효성 검사
  • 클라이언트 측 유효성 검사: 사용자 경험을 위해 브라우저에서 즉시 이루어집니다. 빠른 피드백을 제공하지만, 사용자가 쉽게 우회할 수 있으므로 보안 목적으로는 충분하지 않습니다.
  • 서버 측 유효성 검사: 데이터가 서버에 도달했을 때 최종적으로 이루어지는 검사입니다. 보안 및 데이터 무결성 측면에서 필수적이며, 클라이언트 측 검사와 병행되어야 합니다.

이번 장에서는 주로 클라이언트 측 유효성 검사에 초점을 맞추겠습니다.


앞 절의 커스텀 훅 제출 경로와 연결하면 values는 입력, errors는 그 입력을 검사한 표시 결과라는 차이가 보입니다. 이 절의 SimpleValidationForm은 이름과 이메일을 렌더하는 완성 예제이고, 다음 비밀번호 블록들은 추가 위치를 보여 주는 부분 코드입니다. state Hook은 컴포넌트 최상위에 두고 입력 JSX·핸들러·제출 검사까지 연결해야 새 필드를 실제로 조작할 수 있습니다.

원 email의 includes('@')는 연습용으로 약한 규칙입니다. 원 type=email의 브라우저 형식 검사와 JS 검사는 서로 다른 층이므로, '@'처럼 JS 조건만 참인 문자열이 실제 기본 submit까지 통과한다고 예상하지 않습니다. 서버의 주소 소유 확인이나 계정 존재 확인도 이 규칙으로 구현된 것이 아닙니다.

기본적인 유효성 검사 구현 패턴

가장 기본적인 유효성 검사는 useState를 사용하여 폼 필드와 에러 메시지를 관리하는 것입니다.

필수 필드 검사

가장 흔한 유효성 검사 중 하나는 필드가 비어 있는지 확인하는 것입니다.

src/components/SimpleValidationForm.js
import React, { useState } from 'react';

function SimpleValidationForm() {
  const [username, setUsername] = useState('');
  const [email, setEmail] = useState('');
  const [usernameError, setUsernameError] = useState('');
  const [emailError, setEmailError] = useState('');
  const [formValid, setFormValid] = useState(false); // 폼 전체 유효성 상태

  // 입력 변경 핸들러
  const handleUsernameChange = (e) => {
    setUsername(e.target.value);
    // 실시간 유효성 검사 (선택 사항)
    if (e.target.value.trim() === '') {
      setUsernameError('사용자 이름은 필수입니다.');
    } else {
      setUsernameError('');
    }
  };

  const handleEmailChange = (e) => {
    setEmail(e.target.value);
    // 실시간 유효성 검사 (선택 사항)
    if (!e.target.value.includes('@')) {
      setEmailError('유효한 이메일 주소를 입력해주세요.');
    } else {
      setEmailError('');
    }
  };

  // 폼 제출 핸들러
  const handleSubmit = (e) => {
    e.preventDefault();

    let valid = true;
    // 제출 시 최종 유효성 검사
    if (username.trim() === '') {
      setUsernameError('사용자 이름은 필수입니다.');
      valid = false;
    } else {
      setUsernameError('');
    }

    if (!email.includes('@')) {
      setEmailError('유효한 이메일 주소를 입력해주세요.');
      valid = false;
    } else {
      setEmailError('');
    }

    if (valid) {
      console.log('폼 데이터 유효:', { username, email });
      alert(`제출 완료!\n사용자 이름: ${username}\n이메일: ${email}`);
      // 폼 초기화
      setUsername('');
      setEmail('');
    } else {
      console.log('폼 데이터 유효하지 않음.');
      alert('폼 입력값을 확인해주세요.');
    }
    setFormValid(valid); // 폼 전체 유효성 상태 업데이트
  };

  return (
    <div style={{ maxWidth: '500px', margin: '30px auto', padding: '25px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 10px rgba(0,0,0,0.05)', backgroundColor: '#fff' }}>
      <h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '30px' }}>기본적인 유효성 검사</h2>
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '15px' }}>
          <label htmlFor="username" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>사용자 이름:</label>
          <input
            type="text"
            id="username"
            value={username}
            onChange={handleUsernameChange}
            style={{ width: '100%', padding: '10px', border: usernameError ? '1px solid red' : '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }}
          />
          {usernameError && <p style={{ color: 'red', fontSize: '0.8em', marginTop: '5px' }}>{usernameError}</p>}
        </div>
        <div style={{ marginBottom: '15px' }}>
          <label htmlFor="email" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>이메일:</label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={handleEmailChange}
            style={{ width: '100%', padding: '10px', border: emailError ? '1px solid red' : '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }}
          />
          {emailError && <p style={{ color: 'red', fontSize: '0.8em', marginTop: '5px' }}>{emailError}</p>}
        </div>
        <button type="submit" className="button" style={{ width: '100%', padding: '12px', fontSize: '1.1em' }}>
          제출
        </button>
      </form>
    </div>
  );
}

export default SimpleValidationForm;

이 예시에서는 각 입력 필드에 대한 상태(username, email)와 에러 메시지 상태(usernameError, emailError)를 별도로 관리합니다.

onChange 이벤트에서 실시간으로 유효성을 검사하고, handleSubmit에서 최종적으로 모든 필드를 다시 검사하여 제출 여부를 결정합니다.

복잡한 규칙 검사: 비밀번호 유효성

비밀번호는 길이 제한, 특수 문자 포함 등 여러 복잡한 규칙을 가질 수 있습니다.

src/components/SimpleValidationForm.js (이전 코드에 추가 또는 별도 컴포넌트로 분리)
// ... 기존 코드 ...

const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState('');

const validatePassword = (value) => {
  let error = '';
  if (value.length < 8) {
    error = '비밀번호는 최소 8자 이상이어야 합니다.';
  } else if (!/[A-Z]/.test(value)) {
    error = '비밀번호는 최소 하나의 대문자를 포함해야 합니다.';
  } else if (!/[a-z]/.test(value)) {
    error = '비밀번호는 최소 하나의 소문자를 포함해야 합니다.';
  } else if (!/[0-9]/.test(value)) {
    error = '비밀번호는 최소 하나의 숫자를 포함해야 합니다.';
  } else if (!/[!@#$%^&*()]/.test(value)) { // 예시: 특정 특수 문자 허용
    error = '비밀번호는 최소 하나의 특수 문자(!@#$%^&*())를 포함해야 합니다.';
  }
  return error;
};

const handlePasswordChange = (e) => {
  const value = e.target.value;
  setPassword(value);
  setPasswordError(validatePassword(value));
};

// ... handleSubmit 함수 내에서 password 유효성 검사 추가 ...
let valid = true;
// ... 기존 유효성 검사 ...
const passwordValidationError = validatePassword(password);
setPasswordError(passwordValidationError);
if (passwordValidationError) {
  valid = false;
}
// ...

이처럼 여러 규칙을 적용할 때는 검증 함수를 하나로 분리해 onChange와 handleSubmit에서 재사용할 수 있습니다. 제출 시에는 현재 password로 지역 검증값을 다시 계산하고, 그 값을 에러 상태 저장과 제출 여부 판정에 함께 사용합니다.

규칙 순서의 예측

원 validatePassword는 모든 오류를 모으지 않고 첫 실패만 돌려줍니다. Ab1!는 길이 4여서 길이 오류, abcdefgh는 길이 8 뒤 대문자 오류, Abcdefgh는 숫자 오류, Abcdef1!는 빈 오류 문자열입니다. 이 조합 규칙은 여러 검사를 연습하는 조건이며 인증 서비스의 권장 보안 정책 전체가 아닙니다. 각 서비스를 위한 길이·금지값·저장·복구 정책은 별도로 정해야 합니다.

필드 간 상호 의존성 검사: 비밀번호 확인

비밀번호 확인 필드는 비밀번호 필드와 값이 일치하는지 확인해야 하는 상호 의존성을 가집니다.

src/components/SimpleValidationForm.js (이전 코드에 추가)
// ... 기존 코드 ...

const [confirmPassword, setConfirmPassword] = useState('');
const [confirmPasswordError, setConfirmPasswordError] = useState('');

const handleConfirmPasswordChange = (e) => {
  const value = e.target.value;
  setConfirmPassword(value);
  // 실시간 검사: 비밀번호와 일치하는지
  if (password !== value) {
    setConfirmPasswordError('비밀번호가 일치하지 않습니다.');
  } else {
    setConfirmPasswordError('');
  }
};

// ... handleSubmit 함수 내에서 confirmPassword 유효성 검사 추가 ...
let valid = true;
// ... 기존 유효성 검사 ...
if (password !== confirmPassword) {
  setConfirmPasswordError('비밀번호가 일치하지 않습니다.');
  valid = false;
} else {
  setConfirmPasswordError('');
}
// ...

이 경우, confirmPassword의 onChange 핸들러나 handleSubmit 함수에서 password 상태의 값을 참조하여 비교 검사를 수행합니다.

비밀번호와 확인 필드 검증 흐름 — 상태 추적

두 값 Abcdef1! 일치 뒤 password만 Abcdef2!로 고치면 확인 오류 표시는 아직 비어도 현재 값은 불일치한다. 재확인/submit의 지역 검증 결과로 값과 표시의 시간 차이를 보여 줄 가치가 크다.

현재 password와 confirmPassword 값을 각각 읽어 비밀번호 규칙과 두 필드 일치 여부를 검사하고, 제출 시 두 검증을 다시 계산하는 흐름

React · dependent fields · current values

필드 규칙과 필드 간 관계를 분리한다

validatePassword는 입력 변경과 제출에 같은 규칙 순서를 씁니다. 확인 입력 변경은 현재 두 값을 비교하고, 제출은 저장 오류 대신 현재 값으로 두 결과를 다시 계산합니다.

비밀번호 규칙과 확인 값 의존성 및 검사 이벤트비밀번호 변경은 비밀번호 자체 규칙을, 확인 입력 변경은 현재 두 값의 일치를 검사합니다. password는 일치 검사에서 읽는 의존 값입니다. 제출은 현재 두 값을 읽어 두 검사를 다시 수행합니다.의존 값 읽기submit · 현재 값 재검사현재 비밀번호password현재 확인 값confirmPassword비밀번호 자체 규칙validatePassword(value)비밀번호 입력 변경 시현재 두 값의 일치password === confirmPassword확인 입력 변경 시passwordError첫 오류 또는 빈 문자열confirmPasswordError불일치 오류 또는 빈 문자열handleSubmit현재 값에서 두 결과 재계산오류 상태는 표시 결과이며 제출은 지역 검증 결과로 결정합니다.
비밀번호 자체 규칙과 현재 두 값의 일치password→자체규칙→passwordError, confirmPassword와 의존 password→두값일치→확인오류의 두 입력경로를 유지한다. password와 confirmPassword는 각각 제출 재검사에도 전달된다. 원 sevennodes/sevenedges이며 상태박스들의 세로배치가 순서대로 실행된다는 뜻은 아니다.현재 password비밀번호 입력현재 확인 값confirmPassword비밀번호 자체규칙validatePassword현재 두 값의 일치password === 확인passwordError첫 오류 또는 빈 값확인 오류불일치 또는 빈 값handleSubmit두 결과 재계산두 검사 결과는 별개submit은 현재 입력 읽음저장된 옛 오류로 결정 안 함
password

비밀번호 규칙 검사

validatePassword(value)가 규칙을 정해진 순서로 확인하고 첫 오류 메시지를 반환합니다.

passwordError

규칙 위반 표시

입력 변경에서 계산한 첫 오류를 상태에 둡니다.

dependent values

현재 두 값 비교

password === confirmPassword를 확인 입력 변경 시 계산합니다. password는 비교에 필요한 의존 값입니다.

confirmPasswordError

불일치 표시

확인 입력 변경 시 현재 두 값이 다르면 오류 메시지를 설정합니다. 이 코드에서는 password 변경만으로 이 오류를 자동 갱신하지 않습니다.

submit

제출 시 재검사

현재 비밀번호 규칙과 확인값 일치를 다시 계산합니다.

이 예제에서 password를 다시 바꾼 뒤의 확인 오류는 확인 입력 변경 또는 제출 시 재검사됩니다. 두 필드 변경 때 즉시 동기화하려면 별도 검사를 추가해야 합니다. 상태 설정은 다음 렌더를 예약합니다. 제출 여부는 방금 저장한 오류 상태가 아니라 현재 입력에서 계산한 지역 결과로 정합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 마지막 장면은 confirmPassword=Abcdef1!를 그대로 둔 채 submit하는 선택 추적입니다. confirm을 Abcdef2!로 고쳐 일치시키는 입력은 별도 대안입니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명validatePassword는 입력 변경과 제출에 같은 규칙 순서를 씁니다. 확인 입력 변경은 현재 두 값을 비교하고, 제출은 저장 오류 대신 현재 값으로 두 결과를 다시 계산합니다.
전체 관계·조건 설명password→자체 규칙→passwordError, confirmPassword와 의존 password→두 값 일치→확인 오류의 두 입력 경로를 유지한다. password와 confirmPassword는 각각 제출 재검사에도 전달된다. 원래 도표의 노드 일곱 개와 간선 일곱 개를 유지하며 상태 박스들의 세로 배치가 순서대로 실행된다는 뜻은 아니다.
원도표 설명validatePassword(value)가 규칙을 정해진 순서로 확인하고 첫 오류 메시지를 반환합니다.
원도표 설명password === confirmPassword를 확인 입력 변경 시 계산합니다. password는 비교에 필요한 의존 값입니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
두 값 일치두 값: Abcdef1! / confirm 오류: 빈 문자열password와 confirmPassword는 Abcdef1!로 같다오류 문구는 현재 검사 결과의 표시다
password만 수정password: Abcdef2! / confirm: Abcdef1!password는 Abcdef2!, confirm은 Abcdef1!이고 confirm 오류는 아직 비어 있을 수 있다원 handler는 비밀번호 자체의 규칙만 갱신한다
확인 값 유지 · submit현재 값: Abcdef2! / Abcdef1! / 지역 검증·요청: 불일치 · 차단password Abcdef2!와 confirm Abcdef1!를 submit 시 재검사해 불일치 요청을 차단한다기존 표시 오류가 비었다는 이유로 현재 제출을 허용하지 않는다

비밀번호와 확인 필드 검증 흐름 — 상태 추적

두 값 Abcdef1! 일치 뒤 password만 Abcdef2!로 고치면 확인 오류 표시는 아직 비어도 현재 값은 불일치한다. 재확인/submit의 지역 검증 결과로 값과 표시의 시간 차이를 보여 줄 가치가 크다.

현재 password와 confirmPassword 값을 각각 읽어 비밀번호 규칙과 두 필드 일치 여부를 검사하고, 제출 시 두 검증을 다시 계산하는 흐름

React · dependent fields · current values

필드 규칙과 필드 간 관계를 분리한다

validatePassword는 입력 변경과 제출에 같은 규칙 순서를 씁니다. 확인 입력 변경은 현재 두 값을 비교하고, 제출은 저장 오류 대신 현재 값으로 두 결과를 다시 계산합니다.

비밀번호 규칙과 확인 값 의존성 및 검사 이벤트비밀번호 변경은 비밀번호 자체 규칙을, 확인 입력 변경은 현재 두 값의 일치를 검사합니다. password는 일치 검사에서 읽는 의존 값입니다. 제출은 현재 두 값을 읽어 두 검사를 다시 수행합니다.의존 값 읽기submit · 현재 값 재검사현재 비밀번호password현재 확인 값confirmPassword비밀번호 자체 규칙validatePassword(value)비밀번호 입력 변경 시현재 두 값의 일치password === confirmPassword확인 입력 변경 시passwordError첫 오류 또는 빈 문자열confirmPasswordError불일치 오류 또는 빈 문자열handleSubmit현재 값에서 두 결과 재계산오류 상태는 표시 결과이며 제출은 지역 검증 결과로 결정합니다.
비밀번호 자체 규칙과 현재 두 값의 일치password→자체규칙→passwordError, confirmPassword와 의존 password→두값일치→확인오류의 두 입력경로를 유지한다. password와 confirmPassword는 각각 제출 재검사에도 전달된다. 원 sevennodes/sevenedges이며 상태박스들의 세로배치가 순서대로 실행된다는 뜻은 아니다.현재 password비밀번호 입력현재 확인 값confirmPassword비밀번호 자체규칙validatePassword현재 두 값의 일치password === 확인passwordError첫 오류 또는 빈 값확인 오류불일치 또는 빈 값handleSubmit두 결과 재계산두 검사 결과는 별개submit은 현재 입력 읽음저장된 옛 오류로 결정 안 함
password

비밀번호 규칙 검사

validatePassword(value)가 규칙을 정해진 순서로 확인하고 첫 오류 메시지를 반환합니다.

passwordError

규칙 위반 표시

입력 변경에서 계산한 첫 오류를 상태에 둡니다.

dependent values

현재 두 값 비교

password === confirmPassword를 확인 입력 변경 시 계산합니다. password는 비교에 필요한 의존 값입니다.

confirmPasswordError

불일치 표시

확인 입력 변경 시 현재 두 값이 다르면 오류 메시지를 설정합니다. 이 코드에서는 password 변경만으로 이 오류를 자동 갱신하지 않습니다.

submit

제출 시 재검사

현재 비밀번호 규칙과 확인값 일치를 다시 계산합니다.

이 예제에서 password를 다시 바꾼 뒤의 확인 오류는 확인 입력 변경 또는 제출 시 재검사됩니다. 두 필드 변경 때 즉시 동기화하려면 별도 검사를 추가해야 합니다. 상태 설정은 다음 렌더를 예약합니다. 제출 여부는 방금 저장한 오류 상태가 아니라 현재 입력에서 계산한 지역 결과로 정합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 마지막 장면은 confirmPassword=Abcdef1!를 그대로 둔 채 submit하는 선택 추적입니다. confirm을 Abcdef2!로 고쳐 일치시키는 입력은 별도 대안입니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명validatePassword는 입력 변경과 제출에 같은 규칙 순서를 씁니다. 확인 입력 변경은 현재 두 값을 비교하고, 제출은 저장 오류 대신 현재 값으로 두 결과를 다시 계산합니다.
전체 관계·조건 설명password→자체 규칙→passwordError, confirmPassword와 의존 password→두 값 일치→확인 오류의 두 입력 경로를 유지한다. password와 confirmPassword는 각각 제출 재검사에도 전달된다. 원래 도표의 노드 일곱 개와 간선 일곱 개를 유지하며 상태 박스들의 세로 배치가 순서대로 실행된다는 뜻은 아니다.
원도표 설명validatePassword(value)가 규칙을 정해진 순서로 확인하고 첫 오류 메시지를 반환합니다.
원도표 설명password === confirmPassword를 확인 입력 변경 시 계산합니다. password는 비교에 필요한 의존 값입니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
두 값 일치두 값: Abcdef1! / confirm 오류: 빈 문자열password와 confirmPassword는 Abcdef1!로 같다오류 문구는 현재 검사 결과의 표시다
password만 수정password: Abcdef2! / confirm: Abcdef1!password는 Abcdef2!, confirm은 Abcdef1!이고 confirm 오류는 아직 비어 있을 수 있다원 handler는 비밀번호 자체의 규칙만 갱신한다
확인 값 유지 · submit현재 값: Abcdef2! / Abcdef1! / 지역 검증·요청: 불일치 · 차단password Abcdef2!와 confirm Abcdef1!를 submit 시 재검사해 불일치 요청을 차단한다기존 표시 오류가 비었다는 이유로 현재 제출을 허용하지 않는다

입력 변경과 제출의 시간 차이

password와 confirmPassword에 Abcdef1!를 넣어 일치시킨 뒤 password만 Abcdef2!로 고치면 두 값은 달라집니다. 원 handlePasswordChange는 자체 규칙만 저장하므로 confirmPasswordError가 즉시 갱신되는 것은 아닙니다. 확인 입력을 다시 바꾸거나 제출하면 현재 두 값을 비교합니다. 표시 오류가 비어 있었다는 이유로 제출도 유효하다고 결정하지 않습니다.

유효성 검사 메시지 표시

유효성 검사 결과를 사용자에게 명확하고 즉각적으로 피드백하는 것이 중요합니다.

  • 인라인 에러 메시지: 각 입력 필드 아래에 에러 메시지를 직접 표시하는 것이 가장 일반적입니다.
  • 스타일 변경: 에러가 있는 필드의 테두리 색상을 빨간색으로 변경하는 등 시각적인 힌트를 제공합니다.
  • 폼 제출 버튼 비활성화: 폼에 유효성 에러가 하나라도 있다면 제출 버튼을 비활성화하여 사용자가 잘못된 데이터를 제출하는 것을 막을 수 있습니다.
// 컴포넌트를 렌더링할 때 현재 입력값에서 파생합니다.
const formValid =
  username.trim() !== '' &&
  email.includes('@') &&
  validatePassword(password) === '' &&
  password === confirmPassword;

return (
  // ...
  <button type="submit" className="button" disabled={!formValid}>
    제출
  </button>
  // ...
);

formValid는 다른 상태에서 완전히 계산할 수 있는 값이므로 별도 상태와 useEffect로 동기화하지 않습니다. Hook은 컴포넌트 최상위에서만 호출해야 하며 이벤트 핸들러 안에 둘 수 없습니다. 버튼을 비활성화하더라도 handleSubmit은 현재 값으로 같은 규칙을 다시 검사해야 합니다. 클라이언트 상태와 무관하게 서버도 요청을 독립적으로 검증합니다.


사용자 경험을 고려한 유효성 검사 시점

폼 검증 시점과 서버 신뢰 경계 — 상태 추적

change/blur/submit의 현재 값·피드백·제출 검사 시점이 다르며 유효 요청도 서버에서 독립 검증한다. 빈 폼에서 바로 제출하는 분기까지 전체 모델을 보존한다.

입력 준비, 입력 중, 필드 피드백, 제출 검사, 서버 최종 판정의 다섯 상태와 change, blur, submit 전이로 보는 폼 검증 시점

React · validation timing · trust boundary

빠른 피드백과 최종 판정의 경계를 나눈다

change·blur는 피드백, submit은 현재 값을 검사합니다. 브라우저 검증은 우회돼 서버가 보안·무결성을 판정합니다.

폼 검증의 다섯 상태와 입력·제출 전이빈 폼이나 입력 중 또는 필드 피드백 상태에서도 제출하면 현재 값을 검사합니다. 실패하면 오류 피드백으로 돌아가며 통과한 요청은 서버에서 독립적으로 검증됩니다.서버 검증 경계change · 입력 시작빈 폼 제출도 검사change · blurchange · 수정submitsubmit · 현재 값invalid · 요청 차단valid폼 준비초기 값 · 빈 오류입력 중현재 필드 값 갱신필드 피드백규칙 결과를 가까이 표시제출 검사현재 값으로 재계산서버 최종 판정업무 규칙 · 무결성클라이언트 검사는 피드백과 요청 차단을 돕습니다.서버는 받은 요청을 독립적으로 검증합니다.
입력 피드백과 제출·서버 판단을 분리폼 준비→입력중→필드피드백, 피드백→입력수정의 관계를 유지한다. 준비·입력중·피드백 어디서든 submit은 현재 값 검사로 가고 실패는 피드백, 통과는 서버의 독립 판정으로 간다. 기본예제는 서버 요청이 없는 관찰 코드이고 서버노드는 실제서비스의 확장경계다.폼 준비초기 값입력 중현재 값 갱신필드 피드백change · blur제출 검사현재 값 재계산서버 최종 판정업무 규칙 ·무결성변경 뒤피드백어느상태에서도제출통과한 요청만서버로실패는필드교정으로
ready

폼 준비

초기 값을 보여 줍니다. 아직 입력하지 않은 빈 폼을 제출해도 현재 값을 전체 검사합니다.

change

입력 중

현재 값을 갱신하고 필드 규칙을 확인합니다.

change · blur

필드 피드백

오류를 가까이 보여 주고 변경 때 다시 확인합니다.

submit

제출 검사

입력 중·피드백 상태 어디서 제출하든 현재 값을 전체 검사합니다. 실패하면 오류를 표시하고 요청을 막으며, 통과한 요청만 서버로 보냅니다.

server

서버 최종 판정

클라이언트 통과 여부와 별개로 보안과 데이터 무결성을 다시 확인합니다.

change·blur·submit은 검사 시점을 정합니다. 제출 핸들러는 현재 값을 다시 검사해 상태 갱신 순서와 무관하게 요청 여부를 정합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명change·blur는 피드백, submit은 현재 값을 검사합니다. 브라우저 검증은 우회될 수 있으므로 서버가 보안·무결성을 판정합니다.
전체 관계·조건 설명폼 준비→입력 중→필드 피드백, 피드백→입력 수정의 관계를 유지한다. 준비·입력 중·피드백 어디서든 submit은 현재 값 검사로 가고 실패는 피드백, 통과는 서버의 독립 판정으로 간다. 기본 예제는 서버 요청이 없는 관찰 코드이고 서버 노드는 실제 서비스의 확장 경계다.
원도표 설명초기 값을 보여 줍니다. 아직 입력하지 않은 빈 폼을 제출해도 현재 값 전체를 검사합니다.
원도표 설명입력 중·피드백 상태 어디서 제출하든 현재 값 전체를 검사합니다. 실패하면 오류를 표시하고 요청을 막으며, 통과한 요청만 서버로 보냅니다.
원도표 설명change·blur·submit은 검사 시점을 정합니다. 제출 핸들러는 현재 값을 다시 검사해 상태 갱신 순서와 무관하게 요청 여부를 정합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
빈 값의 submit값: 현재 snapshot / 검사: 전체 규칙입력 중이거나 blur 전이어도 submit은 현재 값을 검사한다blur가 선행되어야 한다는 가정을 두지 않는다
피드백과 교정요청: 차단 / UI: 가까운 필드 피드백invalid라면 요청 없이 오류를 표시하고 change로 고친다입력의 표시와 다음 제출의 검사는 서로 다른 시점이다
server 검증client: valid / server: 독립적인 최종 판정valid인 클라이언트 검사 뒤 요청은 서버 업무 규칙을 다시 통과해야 한다브라우저 검사는 신뢰 경계를 대신하지 않는다

폼 검증 시점과 서버 신뢰 경계 — 상태 추적

change/blur/submit의 현재 값·피드백·제출 검사 시점이 다르며 유효 요청도 서버에서 독립 검증한다. 빈 폼에서 바로 제출하는 분기까지 전체 모델을 보존한다.

입력 준비, 입력 중, 필드 피드백, 제출 검사, 서버 최종 판정의 다섯 상태와 change, blur, submit 전이로 보는 폼 검증 시점

React · validation timing · trust boundary

빠른 피드백과 최종 판정의 경계를 나눈다

change·blur는 피드백, submit은 현재 값을 검사합니다. 브라우저 검증은 우회돼 서버가 보안·무결성을 판정합니다.

폼 검증의 다섯 상태와 입력·제출 전이빈 폼이나 입력 중 또는 필드 피드백 상태에서도 제출하면 현재 값을 검사합니다. 실패하면 오류 피드백으로 돌아가며 통과한 요청은 서버에서 독립적으로 검증됩니다.서버 검증 경계change · 입력 시작빈 폼 제출도 검사change · blurchange · 수정submitsubmit · 현재 값invalid · 요청 차단valid폼 준비초기 값 · 빈 오류입력 중현재 필드 값 갱신필드 피드백규칙 결과를 가까이 표시제출 검사현재 값으로 재계산서버 최종 판정업무 규칙 · 무결성클라이언트 검사는 피드백과 요청 차단을 돕습니다.서버는 받은 요청을 독립적으로 검증합니다.
입력 피드백과 제출·서버 판단을 분리폼 준비→입력중→필드피드백, 피드백→입력수정의 관계를 유지한다. 준비·입력중·피드백 어디서든 submit은 현재 값 검사로 가고 실패는 피드백, 통과는 서버의 독립 판정으로 간다. 기본예제는 서버 요청이 없는 관찰 코드이고 서버노드는 실제서비스의 확장경계다.폼 준비초기 값입력 중현재 값 갱신필드 피드백change · blur제출 검사현재 값 재계산서버 최종 판정업무 규칙 ·무결성변경 뒤피드백어느상태에서도제출통과한 요청만서버로실패는필드교정으로
ready

폼 준비

초기 값을 보여 줍니다. 아직 입력하지 않은 빈 폼을 제출해도 현재 값을 전체 검사합니다.

change

입력 중

현재 값을 갱신하고 필드 규칙을 확인합니다.

change · blur

필드 피드백

오류를 가까이 보여 주고 변경 때 다시 확인합니다.

submit

제출 검사

입력 중·피드백 상태 어디서 제출하든 현재 값을 전체 검사합니다. 실패하면 오류를 표시하고 요청을 막으며, 통과한 요청만 서버로 보냅니다.

server

서버 최종 판정

클라이언트 통과 여부와 별개로 보안과 데이터 무결성을 다시 확인합니다.

change·blur·submit은 검사 시점을 정합니다. 제출 핸들러는 현재 값을 다시 검사해 상태 갱신 순서와 무관하게 요청 여부를 정합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명change·blur는 피드백, submit은 현재 값을 검사합니다. 브라우저 검증은 우회될 수 있으므로 서버가 보안·무결성을 판정합니다.
전체 관계·조건 설명폼 준비→입력 중→필드 피드백, 피드백→입력 수정의 관계를 유지한다. 준비·입력 중·피드백 어디서든 submit은 현재 값 검사로 가고 실패는 피드백, 통과는 서버의 독립 판정으로 간다. 기본 예제는 서버 요청이 없는 관찰 코드이고 서버 노드는 실제 서비스의 확장 경계다.
원도표 설명초기 값을 보여 줍니다. 아직 입력하지 않은 빈 폼을 제출해도 현재 값 전체를 검사합니다.
원도표 설명입력 중·피드백 상태 어디서 제출하든 현재 값 전체를 검사합니다. 실패하면 오류를 표시하고 요청을 막으며, 통과한 요청만 서버로 보냅니다.
원도표 설명change·blur·submit은 검사 시점을 정합니다. 제출 핸들러는 현재 값을 다시 검사해 상태 갱신 순서와 무관하게 요청 여부를 정합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
빈 값의 submit값: 현재 snapshot / 검사: 전체 규칙입력 중이거나 blur 전이어도 submit은 현재 값을 검사한다blur가 선행되어야 한다는 가정을 두지 않는다
피드백과 교정요청: 차단 / UI: 가까운 필드 피드백invalid라면 요청 없이 오류를 표시하고 change로 고친다입력의 표시와 다음 제출의 검사는 서로 다른 시점이다
server 검증client: valid / server: 독립적인 최종 판정valid인 클라이언트 검사 뒤 요청은 서버 업무 규칙을 다시 통과해야 한다브라우저 검사는 신뢰 경계를 대신하지 않는다

유효성 검사를 언제 수행할 것인가도 중요한 UX 고려 사항입니다.

  • onChange (입력 중): 사용자가 타이핑하는 동안 실시간으로 오류를 표시합니다. 즉각적인 피드백을 제공하지만, 너무 민감하면 사용자를 방해할 수 있습니다 (예: 한 글자 입력하자마자 에러).
  • onBlur (필드가 포커스를 잃을 때): 사용자가 필드 입력을 마치고 다른 곳을 클릭했을 때 검사합니다. onChange보다 덜 방해적이지만, onBlur 전에 이미 제출을 시도할 수도 있습니다.
  • onSubmit (폼 제출 시): 폼 제출 버튼을 클릭했을 때 현재 값으로 모든 필드를 다시 검사하는 브라우저 측 최종 관문입니다. 통과한 요청도 서버에서 독립적으로 검증해야 합니다.
권장 패턴

onBlur 시 검사: 사용자가 필드를 벗어났을 때 해당 필드의 유효성 검사를 수행하고 에러 메시지를 표시합니다.

onSubmit 시 모든 필드 검사: 폼 제출 시 모든 필드를 검사하고, 유효하지 않은 필드에는 에러 메시지를 표시하여 제출을 막습니다.

이 패턴은 실시간 피드백과 최종 제출 유효성 검사의 균형을 맞춥니다.


useReducer와 커스텀 훅으로 유효성 검사

앞서 9장 2절에서 다루었던 useReducer 또는 커스텀 훅(useForm)은 유효성 검사 로직을 통합하여 관리하는 데 매우 효과적입니다.

  • useReducer의 경우: 리듀서 함수 내에서 각 필드의 상태 변화에 따라 유효성 검사를 수행하고, 에러 메시지 상태도 함께 업데이트하도록 로직을 추가할 수 있습니다.
  • 커스텀 훅 (useForm): 유효성 검사 로직 자체를 훅의 인자로 받아들이고, handleSubmit 내부에서 이 유효성 검사 함수를 호출하여 에러를 처리하는 방식이 가장 일반적입니다. (9장 2절의 useForm 예시 참고)

이러한 접근 방식은 코드 중복을 줄이고, 폼 로직의 응집성을 높여 유지보수성을 향상시킵니다.


9장 3절 기본적인 폼 유효성 검사 구현은 여기까지입니다.

이 장에서는 폼 유효성 검사의 중요성을 이해하고, 클라이언트 측 유효성 검사의 기본적인 구현 패턴을 살펴보았습니다.

필수 필드, 복잡한 규칙, 필드 간 상호 의존성을 검사하는 방법을 배우고, 사용자 경험을 고려하여 에러 메시지를 표시하는 방법과 유효성 검사 시점을 결정하는 원칙에 대해서도 알아보았습니다.

붙여 넣는 범위를 확인하고 독립 계산하기

마지막 파생 formValid 블록을 복사본에 적용할 때 원 const [formValid, setFormValid] 선언과 그 setter 호출을 같이 유지하면 이름이 중복됩니다. 복사본에서는 그 미사용 상태 선언과 setFormValid(valid)를 제거하고, 모든 입력 state·validatePassword를 정의한 컴포넌트의 return 직전에 파생 const formValid 하나만 둡니다. 비밀번호 input과 확인 input도 value·onChange·label까지 연결해야 합니다. 원 부분 블록의 // ... 자체가 누락된 화면을 만들어 주지는 않습니다.

사용자 이름이 공백 세 개, email이 [email protected], password와 확인이 Abcdef1!라면 제출 여부와 이름 오류를 답하세요. 이름만 Mina로 바꾸고 확인은 그대로 둔 채 password를 Abcdef2!로 바꾼 뒤 제출하면 자체 규칙과 필드 관계 중 무엇이 실패하나요? password가 abcdefgh라면 확인 값과 같다는 사실만으로 제출할 수 있을까요?

지역 결과로 판정하는 풀이

공백 이름은 trim 뒤 빈 값이어서 이름 오류로 제출을 막습니다. Mina와 두 비밀번호 Abcdef2!/Abcdef1! 조합에서는 자체 규칙은 통과하지만 확인 일치 검사가 실패합니다. abcdefgh는 길이는 8이지만 첫 실패인 대문자 규칙 오류가 있으므로 확인 값이 같아도 제출하지 않습니다.

비밀번호 오류와 확인 오류를 현재 값에서 계산해 지역 valid에 함께 반영합니다. setter 직후의 옛 errors나 이전 제출의 formValid 상태를 읽어 결론을 내리지 않습니다. 실제 화면 오류는 입력 id와 메시지 id를 aria-describedby로 연결하고 aria-invalid를 해당 필드에 표시하면 색 외의 단서도 제공할 수 있습니다. 버튼만 잠그면 무엇을 고칠지 알 수 없으므로 각 메시지를 입력 가까이에 보여 줍니다.