회원가입 폼 만들기
이 실습을 통해 여러분은 다음과 같은 능력을 습득하게 될 것입니다.
- 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
폴더에 다음과 같은 파일들을 생성하고 코드를 작성하겠습니다.
프로젝트 설정 및 라이브러리 설치
먼저 React Hook Form과 Yup, 그리고 두 라이브러리를 연결해 줄 @hookform/resolvers
를 설치합니다.
npm install react-hook-form yup @hookform/resolvers
# 또는
yarn add react-hook-form yup @hookform/resolvers
기본 스타일링 (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
컴포넌트를 작성해 보겠습니다.
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
에 추가하여 렌더링합니다.
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
) - 사용자에게 명확하고 즉각적인 폼 유효성 피드백 제공
이 장에서 리액트 개발에서 매우 중요한 폼 처리와 유효성 검사에 대한 깊이 있는 이해를 제공했습니다. 이제 여러분은 사용자 입력을 안전하고 효율적으로 다룰 수 있는 견고한 기반을 갖추게 되었습니다.