icon
9장 : 폼 처리와 유효성 검사

회원가입 폼 만들기


이 실습을 통해 여러분은 다음과 같은 능력을 습득하게 될 것입니다.

  • React Hook Form을 이용해 폼 필드를 등록하고, 값을 가져오고, 제출을 처리하는 방법
  • Yup 스키마 유효성 검사 라이브러리를 사용해 복잡한 유효성 규칙을 정의하는 방법
  • @hookform/resolvers 를 이용해 React Hook Form과 Yup을 통합하는 방법
  • 유효성 검사 오류 메시지를 사용자에게 효과적으로 표시하는 방법

실습 목표

프로젝트 설정: React Hook Form과 Yup 관련 라이브러리를 설치합니다.

폼 컴포넌트 생성: 기본적인 사용자 등록 폼 UI를 만듭니다.

Yup 스키마 정의: 사용자 이름, 이메일, 비밀번호, 비밀번호 확인 필드에 대한 유효성 검사 규칙을 Yup 스키마로 정의합니다.

useForm 훅 적용: useForm 훅을 폼 컴포넌트에 적용하고, Yup 리졸버를 연결합니다.

필드 등록 및 오류 표시: 각 폼 필드를 register 메서드로 등록하고, errors 객체를 사용하여 유효성 검사 오류 메시지를 표시합니다.

폼 제출 처리: handleSubmit을 사용하여 폼 데이터를 콘솔에 출력합니다.


시나리오: 사용자 회원가입 폼

새로운 사용자가 서비스에 가입할 때 필요한 회원가입 폼을 만듭니다. 이 폼에는 다음과 같은 필드와 유효성 검사 규칙이 필요합니다.

  • 사용자 이름 (username)
    • 필수 입력
    • 최소 3자 이상
    • 최대 20자 이하
  • 이메일 (email)
    • 필수 입력
    • 유효한 이메일 형식
  • 비밀번호 (password)
    • 필수 입력
    • 최소 8자 이상
    • 최소 하나의 대문자 포함
    • 최소 하나의 소문자 포함
    • 최소 하나의 숫자 포함
    • 최소 하나의 특수 문자 (예: !@#$%^&*) 포함
  • 비밀번호 확인 (confirmPassword)
    • 필수 입력
    • password 필드와 일치해야 함

프로젝트 준비

create-react-app으로 생성된 프로젝트가 있다고 가정합니다. src 폴더에 다음과 같은 파일들을 생성하고 코드를 작성하겠습니다.

App.js
index.css
UserRegistrationForm.js

프로젝트 설정 및 라이브러리 설치

먼저 React Hook Form과 Yup, 그리고 두 라이브러리를 연결해 줄 @hookform/resolvers를 설치합니다.

npm install react-hook-form yup @hookform/resolvers
# 또는
yarn add react-hook-form yup @hookform/resolvers

기본 스타일링 (index.css)

이전 장에서 사용했던 기본 스타일을 유지하며, 폼에 적합한 스타일을 추가합니다. (만약 이미 가지고 있다면 생략 가능합니다.)

src/index.css
body {
  font-family: 'Arial', sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f4f7f6;
  color: #333;
  line-height: 1.6;
}

#root {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.main-content {
  flex-grow: 1;
  padding: 20px;
  max-width: 960px;
  margin: 20px auto;
  background-color: var(--background-color-main, #ffffff);
  color: var(--text-color-main, #333);
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
  transition: background-color 0.3s ease, color 0.3s ease;
}

h1, h2, h3 {
  color: var(--header-color, #2c3e50);
}

.button {
  display: inline-block;
  padding: 10px 20px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  text-decoration: none;
  font-size: 1em;
  transition: background-color 0.2s ease;
  margin-right: 10px;
}

.button:hover {
  background-color: #2980b9;
}

.button.secondary {
  background-color: #7f8c8d;
}
.button.secondary:hover {
  background-color: #616e78;
}

.button.danger {
  background-color: #e74c3c;
}
.button.danger:hover {
  background-color: #c0392b;
}
.button.success {
  background-color: #2ecc71;
}
.button.success:hover {
  background-color: #27ae60;
}

/* 테마 변수 정의 (CSS 변수 활용) */
body.light-theme {
  --background-color-main: #ffffff;
  --text-color-main: #333;
  --header-color: #2c3e50;
  --header-bg: #eee;
  --header-text: #333;
  --card-bg: #fdfdfd;
  --card-border: #eee;
}

body.dark-theme {
  --background-color-main: #333;
  --text-color-main: #eee;
  --header-color: #eee;
  --header-bg: #222;
  --header-text: #eee;
  --card-bg: #444;
  --card-border: #555;
}

/* 로딩 스피너 (이 실습에는 직접 사용되지 않지만, 다른 곳에서 사용 가능) */
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* 폼 관련 추가 스타일 */
.form-container {
  max-width: 600px;
  margin: 30px auto;
  padding: 25px;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
  background-color: #fff;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: var(--text-color-main);
}

.form-group input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  font-size: 1em;
}

.form-group .error-message {
  color: red;
  font-size: 0.85em;
  margin-top: 5px;
}

.form-group input.input-error {
  border-color: red;
}

사용자 등록 폼 컴포넌트

이제 실습의 핵심인 UserRegistrationForm 컴포넌트를 작성해 보겠습니다.

src/components/UserRegistrationForm.js
import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup'; // Yup 리졸버 임포트
import * as yup from 'yup'; // Yup 라이브러리 임포트

// 1. Yup 스키마 정의
const schema = yup.object().shape({
  username: yup.string()
    .required("사용자 이름은 필수입니다.")
    .min(3, "사용자 이름은 최소 3자 이상이어야 합니다.")
    .max(20, "사용자 이름은 최대 20자 이하여야 합니다."),
  
  email: yup.string()
    .required("이메일은 필수입니다.")
    .email("유효한 이메일 주소를 입력해주세요."),
  
  password: yup.string()
    .required("비밀번호는 필수입니다.")
    .min(8, "비밀번호는 최소 8자 이상이어야 합니다.")
    .matches(/[A-Z]/, "비밀번호는 최소 하나의 대문자를 포함해야 합니다.")
    .matches(/[a-z]/, "비밀번호는 최소 하나의 소문자를 포함해야 합니다.")
    .matches(/[0-9]/, "비밀번호는 최소 하나의 숫자를 포함해야 합니다.")
    .matches(/[!@#$%^&*()]/, "비밀번호는 최소 하나의 특수 문자(!@#$%^&*)를 포함해야 합니다."),
  
  confirmPassword: yup.string()
    .required("비밀번호 확인은 필수입니다.")
    .oneOf([yup.ref('password'), null], "비밀번호가 일치하지 않습니다."), // password 필드와 일치하는지 검사
});


function UserRegistrationForm() {
  // 2. useForm 훅 적용 및 Yup 리졸버 연결
  const { 
    register, 
    handleSubmit, 
    formState: { errors, isSubmitting }, // isSubmitting 추가 (폼 제출 중 상태)
    reset 
  } = useForm({
    resolver: yupResolver(schema), // 🌟 Yup 스키마를 리졸버로 연결
    mode: "onBlur", // 유효성 검사 트리거 시점 (onBlur: 포커스를 잃었을 때, onChange, onSubmit 등)
    defaultValues: { // 초기값 설정 (선택 사항, 비제어 컴포넌트 방식에서는 defaultValue가 더 일반적)
        username: '',
        email: '',
        password: '',
        confirmPassword: ''
    }
  });

  // 3. 폼 제출 시 실행될 함수
  const onSubmit = async (data) => {
    // isSubmitting이 true가 되면서 버튼이 비활성화됩니다.
    console.log('폼 제출 데이터 (유효성 검사 통과):', data);
    // 실제 서버 통신을 시뮬레이션
    try {
      await new Promise(resolve => setTimeout(resolve, 1500)); // 1.5초 지연
      console.log('서버로 데이터 전송 완료!');
      alert('회원가입이 성공적으로 완료되었습니다!');
      reset(); // 폼 초기화
    } catch (error) {
      console.error('회원가입 실패:', error);
      alert('회원가입 중 오류가 발생했습니다.');
    }
    // isSubmitting은 자동으로 false로 돌아갑니다.
  };

  // 폼 제출 실패 시 (유효성 검사 실패) 실행될 함수 (선택 사항)
  const onError = (errors, e) => console.log('폼 유효성 검사 실패:', errors, e);

  return (
    <div className="form-container">
      <h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '30px' }}>사용자 등록 폼</h2>
      <form onSubmit={handleSubmit(onSubmit, onError)}> {/* handleSubmit에 성공/실패 콜백 전달 */}
        {/* 사용자 이름 필드 */}
        <div className="form-group">
          <label htmlFor="username">사용자 이름:</label>
          <input
            type="text"
            id="username"
            className={errors.username ? 'input-error' : ''}
            {...register("username")} {/* 🌟 필드 등록 */}
          />
          {errors.username && <p className="error-message">{errors.username.message}</p>}
        </div>

        {/* 이메일 필드 */}
        <div className="form-group">
          <label htmlFor="email">이메일:</label>
          <input
            type="email"
            id="email"
            className={errors.email ? 'input-error' : ''}
            {...register("email")} {/* 🌟 필드 등록 */}
          />
          {errors.email && <p className="error-message">{errors.email.message}</p>}
        </div>

        {/* 비밀번호 필드 */}
        <div className="form-group">
          <label htmlFor="password">비밀번호:</label>
          <input
            type="password"
            id="password"
            className={errors.password ? 'input-error' : ''}
            {...register("password")} {/* 🌟 필드 등록 */}
          />
          {errors.password && <p className="error-message">{errors.password.message}</p>}
        </div>

        {/* 비밀번호 확인 필드 */}
        <div className="form-group">
          <label htmlFor="confirmPassword">비밀번호 확인:</label>
          <input
            type="password"
            id="confirmPassword"
            className={errors.confirmPassword ? 'input-error' : ''}
            {...register("confirmPassword")} {/* 🌟 필드 등록 */}
          />
          {errors.confirmPassword && <p className="error-message">{errors.confirmPassword.message}</p>}
        </div>

        <button type="submit" className="button success" disabled={isSubmitting} style={{ width: '100%', padding: '12px', fontSize: '1.1em', marginTop: '20px' }}>
          {isSubmitting ? '등록 중...' : '회원가입'}
        </button>
      </form>
    </div>
  );
}

export default UserRegistrationForm;

App.js (최종 설정)

UserRegistrationForm 컴포넌트를 App.js에 추가하여 렌더링합니다.

src/App.js
import React from 'react';
import './index.css'; // 기본 스타일 임포트
import UserRegistrationForm from './components/UserRegistrationForm'; // 폼 컴포넌트 임포트

// 7장의 AppContext나 Header 등은 이 실습에 필수는 아니므로 포함하지 않았습니다.
// 필요하다면 기존 App.js에 UserRegistrationForm을 추가하거나, 아래처럼 간단히 구성할 수 있습니다.

function App() {
  return (
    <div className="main-content">
      {/* <h1 style={{ textAlign: 'center', color: 'var(--header-color)' }}>폼 처리 및 유효성 검사 실습</h1> */}
      <UserRegistrationForm />
    </div>
  );
}

export default App;

실습 진행 방법 및 확인 사항

프로젝트 생성 및 의존성 설치

  • npx create-react-app react-hook-form-yup-app
  • cd react-hook-form-yup-app
  • npm install react-hook-form yup @hookform/resolvers (또는 yarn add react-hook-form yup @hookform/resolvers)

파일 구조 생성: 위에 제시된 디렉토리 구조에 따라 파일들을 생성합니다.

코드 복사/붙여넣기: 각 파일에 해당하는 코드를 정확히 복사하여 붙여넣습니다. (index.css, components/UserRegistrationForm.js, App.js)

애플리케이션 실행: npm start (또는 yarn start) 명령어를 실행하여 개발 서버를 시작합니다.

기능 테스트

  • 폼 필드를 비워두고 제출 버튼을 클릭해 보세요. 각 필드 아래에 필수 입력 메시지가 나타나는지 확인합니다.
  • 사용자 이름에 1~2자만 입력해 보세요. "최소 3자 이상" 메시지가 나타나는지 확인합니다.
  • 이메일에 유효하지 않은 형식(예: abc@)을 입력해 보세요. "유효한 이메일 주소를 입력해주세요." 메시지가 나타나는지 확인합니다.
  • 비밀번호에 길이나 특정 문자(대문자, 소문자, 숫자, 특수문자) 규칙을 위반하여 입력해 보세요. 해당 규칙에 대한 에러 메시지가 나타나는지 확인합니다.
  • 비밀번호와 비밀번호 확인 필드를 다르게 입력하고 제출해 보세요. "비밀번호가 일치하지 않습니다." 메시지가 나타나는지 확인합니다.
  • 모든 필드를 유효하게 입력하고 제출 버튼을 클릭해 보세요.
    • "등록 중..." 메시지와 함께 버튼이 비활성화되는지 확인합니다.
    • 잠시 후(1.5초 지연 후) "회원가입이 성공적으로 완료되었습니다!" 알림이 뜨는지 확인합니다.
    • 폼 필드가 초기화되는지 확인합니다.
  • mode: "onBlur"로 설정했으므로, 필드에 입력 후 다른 필드로 포커스를 옮겼을 때 해당 필드의 유효성 검사가 즉시 실행되어 에러 메시지가 나타나는지 확인합니다.

이제 여러분은 React Hook Form과 Yup 라이브러리를 사용하여 복잡한 폼의 상태를 효율적으로 관리하고, 강력한 유효성 검사 규칙을 적용하는 방법을 완벽하게 익히셨습니다.

이 실습을 통해 여러분은 다음과 같은 중요한 개발 기술을 습득했습니다.

  • React Hook Form의 핵심 API (useForm, register, handleSubmit, formState.errors) 활용
  • Yup 스키마를 이용한 구조적이고 재사용 가능한 유효성 검사 규칙 정의
  • 외부 유효성 검사 라이브러리를 React Hook Form과 통합하는 방법 (@hookform/resolvers)
  • 사용자에게 명확하고 즉각적인 폼 유효성 피드백 제공

이 장에서 리액트 개발에서 매우 중요한 폼 처리와 유효성 검사에 대한 깊이 있는 이해를 제공했습니다. 이제 여러분은 사용자 입력을 안전하고 효율적으로 다룰 수 있는 견고한 기반을 갖추게 되었습니다.