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

회원가입 폼 만들기

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

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

실습 목표

  1. 프로젝트 설정: React Hook Form과 Yup 관련 라이브러리를 설치합니다.
  2. 폼 컴포넌트 생성: 기본적인 사용자 등록 폼 UI를 만듭니다.
  3. Yup 스키마 정의: 사용자 이름, 이메일, 비밀번호, 비밀번호 확인 필드에 대한 유효성 검사 규칙을 Yup 스키마로 정의합니다.
  4. useForm 훅 적용: useForm 훅을 폼 컴포넌트에 적용하고, Yup 리졸버를 연결합니다.
  5. 필드 등록 및 오류 표시: 각 폼 필드를 register 메서드로 등록하고, errors 객체를 사용하여 유효성 검사 오류 메시지를 표시합니다.
  6. 폼 제출 처리: handleSubmit을 사용하여 폼 데이터를 콘솔에 출력합니다.

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

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

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

프로젝트 준비

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

src/
├── App.js
├── index.css (기본 스타일)
├── components/
│   └── 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
/* 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
// 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
// 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;

실습 진행 방법 및 확인 사항

  1. 프로젝트 생성 및 의존성 설치
    • 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)
  2. 파일 구조 생성: 위에 제시된 디렉토리 구조에 따라 파일들을 생성합니다.
  3. 코드 복사/붙여넣기: 각 파일에 해당하는 코드를 정확히 복사하여 붙여넣습니다. (index.css, components/UserRegistrationForm.js, App.js)
  4. 애플리케이션 실행: npm start (또는 yarn start) 명령어를 실행하여 개발 서버를 시작합니다.
  5. 기능 테스트
    • 폼 필드를 비워두고 제출 버튼을 클릭해 보세요. 각 필드 아래에 필수 입력 메시지가 나타나는지 확인합니다.
    • 사용자 이름에 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)
  • 사용자에게 명확하고 즉각적인 폼 유효성 피드백 제공

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