2025. 4. 1. 01:08ㆍFrameworks/React
React 폼 관리와 유효성 검사
웹 애플리케이션에서 폼은 사용자로부터 데이터를 수집하는 핵심 요소입니다. React에서 폼을 효과적으로 관리하고 유효성을 검사하는 방법을 알아보겠습니다.
1. React의 기본 폼 처리
제어 컴포넌트 (Controlled Components)
React에서 폼 요소를 다루는 가장 기본적인 방식은 '제어 컴포넌트'를 사용하는 것입니다. 제어 컴포넌트는 React 상태(state)로 폼 요소의 값을 제어하는 방식입니다.
import React, { useState } from 'react';
function SimpleForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('제출된 데이터:', { name, email });
// API 요청 등의 처리
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">이름:</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="email">이메일:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<button type="submit">제출</button>
</form>
);
}
비제어 컴포넌트 (Uncontrolled Components)
비제어 컴포넌트는 DOM 자체가 폼 데이터를 관리하는 방식입니다. React의 ref를 사용하여 필요할 때 폼 값에 접근합니다.
import React, { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
name: nameRef.current.value,
email: emailRef.current.value
};
console.log('제출된 데이터:', formData);
// API 요청 등의 처리
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">이름:</label>
<input
type="text"
id="name"
ref={nameRef}
defaultValue=""
/>
</div>
<div>
<label htmlFor="email">이메일:</label>
<input
type="email"
id="email"
ref={emailRef}
defaultValue=""
/>
</div>
<button type="submit">제출</button>
</form>
);
}
제어 vs 비제어 컴포넌트
특징 제어 컴포넌트 비제어 컴포넌트
데이터 관리 | React 상태 | DOM |
즉시 유효성 검사 | 가능 | 어려움 |
폼 값 접근 | state에서 바로 접근 | ref를 통해 접근 |
조건부 제출 버튼 비활성화 | 쉬움 | 추가 로직 필요 |
사용 사례 | 대부분의 폼 | 파일 업로드, 라이브러리 통합 |
2. 복잡한 폼 상태 관리
객체로 폼 상태 관리
여러 입력 필드가 있는 폼의 경우, 각 필드마다 별도의 상태를 만드는 대신 객체로 관리하는 것이 효율적입니다.
import React, { useState } from 'react';
function RegistrationForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
age: '',
gender: '',
interests: []
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
// 체크박스 처리 (다중 선택)
const updatedInterests = [...formData.interests];
if (checked) {
updatedInterests.push(value);
} else {
const index = updatedInterests.indexOf(value);
if (index !== -1) {
updatedInterests.splice(index, 1);
}
}
setFormData({
...formData,
interests: updatedInterests
});
} else {
// 일반 입력 필드 처리
setFormData({
...formData,
[name]: value
});
}
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('제출된 데이터:', formData);
// API 요청 등의 처리
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="firstName">이름:</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="lastName">성:</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">이메일:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="confirmPassword">비밀번호 확인:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="age">나이:</label>
<input
type="number"
id="age"
name="age"
value={formData.age}
onChange={handleChange}
/>
</div>
<div>
<label>성별:</label>
<select name="gender" value={formData.gender} onChange={handleChange}>
<option value="">선택하세요</option>
<option value="male">남성</option>
<option value="female">여성</option>
<option value="other">기타</option>
<option value="prefer-not-to-say">답변하지 않음</option>
</select>
</div>
<div>
<label>관심사:</label>
<div>
<label>
<input
type="checkbox"
name="interests"
value="technology"
checked={formData.interests.includes('technology')}
onChange={handleChange}
/>
기술
</label>
<label>
<input
type="checkbox"
name="interests"
value="sports"
checked={formData.interests.includes('sports')}
onChange={handleChange}
/>
스포츠
</label>
<label>
<input
type="checkbox"
name="interests"
value="music"
checked={formData.interests.includes('music')}
onChange={handleChange}
/>
음악
</label>
<label>
<input
type="checkbox"
name="interests"
value="art"
checked={formData.interests.includes('art')}
onChange={handleChange}
/>
예술
</label>
</div>
</div>
<button type="submit">가입하기</button>
</form>
);
}
useReducer를 활용한 폼 상태 관리
useReducer를 사용하면 더 복잡한 폼 상태를 관리할 수 있습니다.
import React, { useReducer } from 'react';
// 초기 상태
const initialState = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
touched: {}, // 필드 터치 여부 추적
errors: {} // 유효성 검사 오류
};
// 폼 리듀서
function formReducer(state, action) {
switch (action.type) {
case 'CHANGE':
return {
...state,
[action.field]: action.value
};
case 'TOUCH':
return {
...state,
touched: {
...state.touched,
[action.field]: true
}
};
case 'VALIDATE':
return {
...state,
errors: action.errors
};
case 'RESET':
return initialState;
default:
return state;
}
}
function AdvancedForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
// 입력값 변경 처리
const handleChange = (e) => {
const { name, value } = e.target;
dispatch({
type: 'CHANGE',
field: name,
value
});
};
// 필드 포커스 해제 처리
const handleBlur = (e) => {
const { name } = e.target;
dispatch({
type: 'TOUCH',
field: name
});
// 포커스 해제 시 유효성 검사
validateForm();
};
// 폼 유효성 검사
const validateForm = () => {
const errors = {};
// 이름 검사
if (!state.firstName) {
errors.firstName = '이름을 입력하세요';
}
// 이메일 검사
if (!state.email) {
errors.email = '이메일을 입력하세요';
} else if (!/\S+@\S+\.\S+/.test(state.email)) {
errors.email = '유효한 이메일 주소를 입력하세요';
}
// 비밀번호 검사
if (!state.password) {
errors.password = '비밀번호를 입력하세요';
} else if (state.password.length < 8) {
errors.password = '비밀번호는 8자 이상이어야 합니다';
}
// 비밀번호 확인 검사
if (state.password !== state.confirmPassword) {
errors.confirmPassword = '비밀번호가 일치하지 않습니다';
}
dispatch({
type: 'VALIDATE',
errors
});
return Object.keys(errors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
// 모든 필드 터치 처리
const touchedFields = {};
Object.keys(initialState).forEach(key => {
if (key !== 'touched' && key !== 'errors') {
touchedFields[key] = true;
}
});
dispatch({
type: 'TOUCH',
field: Object.keys(touchedFields)
});
// 유효성 검사
const isValid = validateForm();
if (isValid) {
console.log('제출된 데이터:', {
firstName: state.firstName,
lastName: state.lastName,
email: state.email,
password: state.password
});
// 폼 초기화
dispatch({ type: 'RESET' });
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="firstName">이름:</label>
<input
type="text"
id="firstName"
name="firstName"
value={state.firstName}
onChange={handleChange}
onBlur={handleBlur}
/>
{state.touched.firstName && state.errors.firstName && (
<span className="error">{state.errors.firstName}</span>
)}
</div>
<div>
<label htmlFor="lastName">성:</label>
<input
type="text"
id="lastName"
name="lastName"
value={state.lastName}
onChange={handleChange}
onBlur={handleBlur}
/>
</div>
<div>
<label htmlFor="email">이메일:</label>
<input
type="email"
id="email"
name="email"
value={state.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{state.touched.email && state.errors.email && (
<span className="error">{state.errors.email}</span>
)}
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<input
type="password"
id="password"
name="password"
value={state.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{state.touched.password && state.errors.password && (
<span className="error">{state.errors.password}</span>
)}
</div>
<div>
<label htmlFor="confirmPassword">비밀번호 확인:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={state.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
/>
{state.touched.confirmPassword && state.errors.confirmPassword && (
<span className="error">{state.errors.confirmPassword}</span>
)}
</div>
<button type="submit">가입하기</button>
</form>
);
}
3. 폼 유효성 검사 (Form Validation)
기본 유효성 검사
React에서 폼 유효성 검사는 주로 다음과 같은 방법으로 구현합니다:
- 입력값 변경 시 유효성 검사
- 폼 제출 시 유효성 검사
- 필드 포커스 해제(blur) 시 유효성 검사
import React, { useState } from 'react';
function ValidationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = (name, value) => {
let errorMessage = '';
switch (name) {
case 'username':
if (!value) {
errorMessage = '사용자 이름을 입력하세요';
} else if (value.length < 3) {
errorMessage = '사용자 이름은 3자 이상이어야 합니다';
}
break;
case 'email':
if (!value) {
errorMessage = '이메일을 입력하세요';
} else if (!/\S+@\S+\.\S+/.test(value)) {
errorMessage = '유효한 이메일 주소를 입력하세요';
}
break;
case 'password':
if (!value) {
errorMessage = '비밀번호를 입력하세요';
} else if (value.length < 8) {
errorMessage = '비밀번호는 8자 이상이어야 합니다';
} else if (!/[A-Z]/.test(value)) {
errorMessage = '비밀번호에는 대문자가 포함되어야 합니다';
} else if (!/[0-9]/.test(value)) {
errorMessage = '비밀번호에는 숫자가 포함되어야 합니다';
}
break;
default:
break;
}
return errorMessage;
};
const handleChange = (e) => {
const { name, value } = e.target;
// 입력값 업데이트
setFormData({
...formData,
[name]: value
});
// 필드가 이미 터치되었다면 실시간 유효성 검사
if (touched[name]) {
const errorMessage = validateField(name, value);
setErrors(prevErrors => ({
...prevErrors,
[name]: errorMessage
}));
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
// 필드 터치 상태 업데이트
setTouched({
...touched,
[name]: true
});
// 포커스 해제 시 유효성 검사
const errorMessage = validateField(name, value);
setErrors(prevErrors => ({
...prevErrors,
[name]: errorMessage
}));
};
const handleSubmit = (e) => {
e.preventDefault();
// 모든 필드 터치 처리
const touchedFields = {};
Object.keys(formData).forEach(key => {
touchedFields[key] = true;
});
setTouched(touchedFields);
// 모든 필드 유효성 검사
const formErrors = {};
Object.entries(formData).forEach(([name, value]) => {
const errorMessage = validateField(name, value);
if (errorMessage) {
formErrors[name] = errorMessage;
}
});
setErrors(formErrors);
// 오류가 없으면 폼 제출
if (Object.keys(formErrors).length === 0) {
console.log('유효한 폼 데이터:', formData);
// API 요청 등의 처리
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">사용자 이름:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
onBlur={handleBlur}
className={touched.username && errors.username ? 'error-input' : ''}
/>
{touched.username && errors.username && (
<div className="error-message">{errors.username}</div>
)}
</div>
<div>
<label htmlFor="email">이메일:</label>
<input
type="text"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error-input' : ''}
/>
{touched.email && errors.email && (
<div className="error-message">{errors.email}</div>
)}
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={touched.password && errors.password ? 'error-input' : ''}
/>
{touched.password && errors.password && (
<div className="error-message">{errors.password}</div>
)}
</div>
<button type="submit">가입하기</button>
</form>
);
}
커스텀 훅을 사용한 폼 유효성 검사
폼 로직을 재사용 가능한 커스텀 훅으로 추출하여 사용할 수 있습니다.
// useForm.js
import { useState } from 'react';
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
// 체크박스 처리
const fieldValue = type === 'checkbox' ? checked : value;
setValues({
...values,
[name]: fieldValue
});
// 필드가 이미 터치되었다면 실시간 유효성 검사
if (touched[name]) {
const fieldErrors = validate({ ...values, [name]: fieldValue });
setErrors(fieldErrors);
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched({
...touched,
[name]: true
});
const fieldErrors = validate(values);
setErrors(fieldErrors);
};
const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
// 모든 필드 터치 처리
const touchedFields = {};
Object.keys(values).forEach(key => {
touchedFields[key] = true;
});
setTouched(touchedFields);
// 유효성 검사
const formErrors = validate(values);
setErrors(formErrors);
// 오류가 없으면 콜백 실행
if (Object.keys(formErrors).length === 0) {
onSubmit(values);
}
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
export default useForm;
사용 예:
import React from 'react';
import useForm from './useForm';
function SignupForm() {
// 유효성 검사 함수
const validateForm = (values) => {
const errors = {};
if (!values.username) {
errors.username = '사용자 이름을 입력하세요';
} else if (values.username.length < 3) {
errors.username = '사용자 이름은 3자 이상이어야 합니다';
}
if (!values.email) {
errors.email = '이메일을 입력하세요';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = '유효한 이메일 주소를 입력하세요';
}
if (!values.password) {
errors.password = '비밀번호를 입력하세요';
} else if (values.password.length < 8) {
errors.password = '비밀번호는 8자 이상이어야 합니다';
}
if (values.password !== values.confirmPassword) {
errors.confirmPassword = '비밀번호가 일치하지 않습니다';
}
return errors;
};
const initialValues = {
username: '',
email: '',
password: '',
confirmPassword: '',
terms: false
};
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
} = useForm(initialValues, validateForm);
const onSubmit = (formData) => {
console.log('폼 제출:', formData);
// API 요청 등의 처리
alert('가입이 완료되었습니다!');
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="username">사용자 이름:</label>
<input
type="text"
id="username"
name="username"
value={values.username}
onChange={handleChange}
onBlur={handleBlur}
className={touched.username && errors.username ? 'error-input' : ''}
/>
{touched.username && errors.username && (
<div className="error-message">{errors.username}</div>
)}
</div>
<div>
<label htmlFor="email">이메일:</label>
<input
type="email"
id="email"
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error-input' : ''}
/>
{touched.email && errors.email && (
<div className="error-message">{errors.email}</div>
)}
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<input
type="password"
id="password"
name="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
className={touched.password && errors.password ? 'error-input' : ''}
/>
{touched.password && errors.password && (
<div className="error-message">{errors.password}</div>
)}
</div>
<div>
<label htmlFor="confirmPassword">비밀번호 확인:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={values.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
className={touched.confirmPassword && errors.confirmPassword ? 'error-input' : ''}
/>
{touched.confirmPassword && errors.confirmPassword && (
<div className="error-message">{errors.confirmPassword}</div>
)}
</div>
<div>
<label>
<input
type="checkbox"
name="terms"
checked={values.terms}
onChange={handleChange}
/>
이용약관에 동의합니다
</label>
{touched.terms && errors.terms && (
<div className="error-message">{errors.terms}</div>
)}
</div>
<div>
<button type="submit" disabled={!values.terms}>
가입하기
</button>
<button type="button" onClick={reset}>
초기화
</button>
</div>
</form>
);
}
4. 폼 라이브러리
복잡한 폼을 더 쉽게 관리하기 위한 여러 라이브러리가 있습니다.
Formik
Formik은 React에서 가장 인기 있는 폼 관리 라이브러리 중 하나입니다.
npm install formik
기본 사용법:
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup'; // 유효성 검사 라이브러리
// 유효성 검사 스키마
const validationSchema = Yup.object({
firstName: Yup.string()
.max(15, '15자 이하여야 합니다')
.required('이름을 입력하세요'),
lastName: Yup.string()
.max(20, '20자 이하여야 합니다')
.required('성을 입력하세요'),
email: Yup.string()
.email('유효한 이메일 주소를 입력하세요')
.required('이메일을 입력하세요'),
password: Yup.string()
.min(8, '비밀번호는 8자 이상이어야 합니다')
.required('비밀번호를 입력하세요'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password'), null], '비밀번호가 일치하지 않습니다')
.required('비밀번호 확인을 입력하세요'),
terms: Yup.boolean()
.oneOf([true], '이용약관에 동의해야 합니다')
});
function FormikExample() {
return (
<Formik
initialValues={{
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
terms: false
}}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting, resetForm }) => {
setTimeout(() => {
console.log('폼 제출 값:', values);
alert('폼이 제출되었습니다!');
resetForm();
setSubmitting(false);
}, 500);
}}
>
{({ isSubmitting, touched, errors }) => (
<Form>
<div>
<label htmlFor="firstName">이름:</label>
<Field
type="text"
id="firstName"
name="firstName"
className={touched.firstName && errors.firstName ? 'error-input' : ''}
/>
<ErrorMessage name="firstName" component="div" className="error-message" />
</div>
<div>
<label htmlFor="lastName">성:</label>
<Field
type="text"
id="lastName"
name="lastName"
className={touched.lastName && errors.lastName ? 'error-input' : ''}
/>
<ErrorMessage name="lastName" component="div" className="error-message" />
</div>
<div>
<label htmlFor="email">이메일:</label>
<Field
type="email"
id="email"
name="email"
className={touched.email && errors.email ? 'error-input' : ''}
/>
<ErrorMessage name="email" component="div" className="error-message" />
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<Field
type="password"
id="password"
name="password"
className={touched.password && errors.password ? 'error-input' : ''}
/>
<ErrorMessage name="password" component="div" className="error-message" />
</div>
<div>
<label htmlFor="confirmPassword">비밀번호 확인:</label>
<Field
type="password"
id="confirmPassword"
name="confirmPassword"
className={touched.confirmPassword && errors.confirmPassword ? 'error-input' : ''}
/>
<ErrorMessage name="confirmPassword" component="div" className="error-message" />
</div>
<div>
<label>
<Field type="checkbox" name="terms" />
이용약관에 동의합니다
</label>
<ErrorMessage name="terms" component="div" className="error-message" />
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '처리 중...' : '가입하기'}
</button>
</Form>
)}
</Formik>
);
}
React Hook Form
React Hook Form은 성능에 중점을 둔 더 가벼운 폼 라이브러리입니다.
npm install react-hook-form
기본 사용법:
import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
// 유효성 검사 스키마
const schema = yup.object({
firstName: yup.string().required('이름을 입력하세요'),
lastName: yup.string().required('성을 입력하세요'),
email: yup.string().email('유효한 이메일을 입력하세요').required('이메일을 입력하세요'),
password: yup.string().min(8, '비밀번호는 8자 이상이어야 합니다').required('비밀번호를 입력하세요'),
confirmPassword: yup.string()
.oneOf([yup.ref('password'), null], '비밀번호가 일치하지 않습니다')
.required('비밀번호 확인을 입력하세요'),
terms: yup.boolean().oneOf([true], '이용약관에 동의해야 합니다')
});
function ReactHookFormExample() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm({
resolver: yupResolver(schema)
});
const onSubmit = (data) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('폼 제출 값:', data);
alert('폼이 제출되었습니다!');
reset();
resolve();
}, 500);
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="firstName">이름:</label>
<input
id="firstName"
{...register('firstName')}
className={errors.firstName ? 'error-input' : ''}
/>
{errors.firstName && (
<div className="error-message">{errors.firstName.message}</div>
)}
</div>
<div>
<label htmlFor="lastName">성:</label>
<input
id="lastName"
{...register('lastName')}
className={errors.lastName ? 'error-input' : ''}
/>
{errors.lastName && (
<div className="error-message">{errors.lastName.message}</div>
)}
</div>
<div>
<label htmlFor="email">이메일:</label>
<input
id="email"
type="email"
{...register('email')}
className={errors.email ? 'error-input' : ''}
/>
{errors.email && (
<div className="error-message">{errors.email.message}</div>
)}
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<input
id="password"
type="password"
{...register('password')}
className={errors.password ? 'error-input' : ''}
/>
{errors.password && (
<div className="error-message">{errors.password.message}</div>
)}
</div>
<div>
<label htmlFor="confirmPassword">비밀번호 확인:</label>
<input
id="confirmPassword"
type="password"
{...register('confirmPassword')}
className={errors.confirmPassword ? 'error-input' : ''}
/>
{errors.confirmPassword && (
<div className="error-message">{errors.confirmPassword.message}</div>
)}
</div>
<div>
<label>
<input type="checkbox" {...register('terms')} />
이용약관에 동의합니다
</label>
{errors.terms && (
<div className="error-message">{errors.terms.message}</div>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '처리 중...' : '가입하기'}
</button>
</form>
);
}
5. 다양한 폼 요소 다루기
1. 선택 필드 (Select)
import React, { useState } from 'react';
function SelectExample() {
const [country, setCountry] = useState('');
const handleChange = (e) => {
setCountry(e.target.value);
};
return (
<div>
<label htmlFor="country">국가:</label>
<select
id="country"
value={country}
onChange={handleChange}
>
<option value="">선택하세요</option>
<option value="kr">한국</option>
<option value="us">미국</option>
<option value="jp">일본</option>
<option value="cn">중국</option>
<option value="uk">영국</option>
</select>
{country && <p>선택한 국가: {country}</p>}
</div>
);
}
2. 체크박스 그룹
import React, { useState } from 'react';
function CheckboxGroupExample() {
const [selectedLanguages, setSelectedLanguages] = useState([]);
const languages = [
{ id: 'js', name: 'JavaScript' },
{ id: 'py', name: 'Python' },
{ id: 'java', name: 'Java' },
{ id: 'csharp', name: 'C#' },
{ id: 'go', name: 'Go' }
];
const handleChange = (e) => {
const { value, checked } = e.target;
if (checked) {
// 항목 추가
setSelectedLanguages([...selectedLanguages, value]);
} else {
// 항목 제거
setSelectedLanguages(selectedLanguages.filter(lang => lang !== value));
}
};
return (
<div>
<p>관심 있는 프로그래밍 언어를 선택하세요:</p>
<div className="checkbox-group">
{languages.map(language => (
<label key={language.id}>
<input
type="checkbox"
value={language.id}
checked={selectedLanguages.includes(language.id)}
onChange={handleChange}
/>
{language.name}
</label>
))}
</div>
{selectedLanguages.length > 0 && (
<p>
선택한 언어: {selectedLanguages.map(id => {
const language = languages.find(lang => lang.id === id);
return language ? language.name : id;
}).join(', ')}
</p>
)}
</div>
);
}
3. 라디오 버튼 그룹
import React, { useState } from 'react';
function RadioGroupExample() {
const [experience, setExperience] = useState('');
const experienceLevels = [
{ id: 'beginner', label: '입문자 (1년 미만)' },
{ id: 'intermediate', label: '중급자 (1-3년)' },
{ id: 'advanced', label: '고급자 (3-5년)' },
{ id: 'expert', label: '전문가 (5년 이상)' }
];
const handleChange = (e) => {
setExperience(e.target.value);
};
return (
<div>
<p>개발 경력을 선택하세요:</p>
<div className="radio-group">
{experienceLevels.map(level => (
<label key={level.id}>
<input
type="radio"
name="experience"
value={level.id}
checked={experience === level.id}
onChange={handleChange}
/>
{level.label}
</label>
))}
</div>
{experience && <p>선택한 경력: {experience}</p>}
</div>
);
}
4. 다중 선택 (Multi-select)
import React, { useState } from 'react';
function MultiSelectExample() {
const [selectedFruits, setSelectedFruits] = useState([]);
const fruits = [
{ id: 'apple', name: '사과' },
{ id: 'banana', name: '바나나' },
{ id: 'orange', name: '오렌지' },
{ id: 'grape', name: '포도' },
{ id: 'strawberry', name: '딸기' }
];
const handleChange = (e) => {
const values = Array.from(
e.target.selectedOptions,
option => option.value
);
setSelectedFruits(values);
};
return (
<div>
<label htmlFor="fruits">좋아하는 과일을 선택하세요 (여러 개 선택 가능):</label>
<select
id="fruits"
multiple
value={selectedFruits}
onChange={handleChange}
style={{ height: '150px' }}
>
{fruits.map(fruit => (
<option key={fruit.id} value={fruit.id}>
{fruit.name}
</option>
))}
</select>
{selectedFruits.length > 0 && (
<p>
선택한 과일: {selectedFruits.map(id => {
const fruit = fruits.find(f => f.id === id);
return fruit ? fruit.name : id;
}).join(', ')}
</p>
)}
</div>
);
}
5. 파일 업로드
import React, { useState } from 'react';
function FileUploadExample() {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState('');
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
setFile(selectedFile);
// 이미지 미리보기 생성
if (selectedFile.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => {
setPreview(reader.result);
};
reader.readAsDataURL(selectedFile);
} else {
setPreview('');
}
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (!file) {
alert('파일을 선택하세요');
return;
}
// 여기서 파일 업로드 처리 (예: FormData 사용)
const formData = new FormData();
formData.append('file', file);
// API 요청 예시
console.log('업로드할 파일:', file);
console.log('FormData:', formData);
// 실제 API 요청 코드 (예시)
// fetch('/api/upload', {
// method: 'POST',
// body: formData
// })
// .then(response => response.json())
// .then(data => console.log('성공:', data))
// .catch(error => console.error('에러:', error));
alert('파일 업로드가 처리되었습니다');
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="file">파일 선택:</label>
<input
type="file"
id="file"
onChange={handleFileChange}
accept="image/*" // 이미지 파일만 허용 (선택 사항)
/>
</div>
{file && (
<div>
<p>선택한 파일: {file.name} ({Math.round(file.size / 1024)} KB)</p>
</div>
)}
{preview && (
<div>
<h4>이미지 미리보기:</h4>
<img
src={preview}
alt="미리보기"
style={{ maxWidth: '300px', maxHeight: '200px' }}
/>
</div>
)}
<button type="submit" disabled={!file}>
업로드
</button>
</form>
);
}
6. 동적 폼 필드
import React, { useState } from 'react';
function DynamicFormExample() {
const [formFields, setFormFields] = useState([
{ name: '', email: '' }
]);
const handleFormChange = (index, event) => {
const data = [...formFields];
data[index][event.target.name] = event.target.value;
setFormFields(data);
};
const addFields = () => {
setFormFields([...formFields, { name: '', email: '' }]);
};
const removeFields = (index) => {
const data = [...formFields];
data.splice(index, 1);
setFormFields(data);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('폼 데이터:', formFields);
alert('폼이 제출되었습니다!');
};
return (
<form onSubmit={handleSubmit}>
<h3>팀원 정보 입력</h3>
{formFields.map((field, index) => (
<div key={index} className="form-row">
<div>
<label htmlFor={`name-${index}`}>이름:</label>
<input
type="text"
id={`name-${index}`}
name="name"
value={field.name}
onChange={event => handleFormChange(index, event)}
required
/>
</div>
<div>
<label htmlFor={`email-${index}`}>이메일:</label>
<input
type="email"
id={`email-${index}`}
name="email"
value={field.email}
onChange={event => handleFormChange(index, event)}
required
/>
</div>
{formFields.length > 1 && (
<button
type="button"
className="remove-btn"
onClick={() => removeFields(index)}
>
제거
</button>
)}
</div>
))}
<div className="form-buttons">
<button type="button" onClick={addFields}>
팀원 추가
</button>
<button type="submit">제출</button>
</div>
</form>
);
}
6. 폼 스타일링과 접근성
폼 스타일링 예시
/* Form.css */
.form-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
select,
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
.error-input {
border-color: #e53935;
}
.error-message {
color: #e53935;
font-size: 14px;
margin-top: 5px;
}
button {
background-color: #4a90e2;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #3a7bc8;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.checkbox-group,
.radio-group {
display: flex;
flex-direction: column;
gap: 10px;
}
/* 반응형 디자인 */
@media (max-width: 600px) {
.form-container {
padding: 15px;
}
input, select, textarea, button {
font-size: 14px;
padding: 8px;
}
}
접근성 향상 기법
import React, { useState } from 'react';
import './Form.css';
function AccessibleForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
// 입력 시 오류 메시지 제거
if (errors[name]) {
setErrors({
...errors,
[name]: ''
});
}
};
const validate = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = '이름을 입력하세요';
}
if (!formData.email.trim()) {
newErrors.email = '이메일을 입력하세요';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = '유효한 이메일 주소를 입력하세요';
}
if (!formData.message.trim()) {
newErrors.message = '메시지를 입력하세요';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
console.log('폼 제출:', formData);
alert('메시지가 전송되었습니다!');
setFormData({ name: '', email: '', message: '' });
}
};
return (
<div className="form-container">
<h2 id="form-title">문의하기</h2>
<p id="form-description">
질문이나 의견이 있으시면 아래 양식을 작성해 주세요.
</p>
<form
onSubmit={handleSubmit}
aria-labelledby="form-title"
aria-describedby="form-description"
>
<div className="form-group">
<label htmlFor="name">
이름:
{errors.name && <span className="sr-only">오류: {errors.name}</span>}
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
aria-invalid={errors.name ? 'true' : 'false'}
aria-describedby={errors.name ? 'name-error' : undefined}
className={errors.name ? 'error-input' : ''}
/>
{errors.name && (
<div id="name-error" className="error-message">
{errors.name}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email">
이메일:
{errors.email && <span className="sr-only">오류: {errors.email}</span>}
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
className={errors.email ? 'error-input' : ''}
/>
{errors.email && (
<div id="email-error" className="error-message">
{errors.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="message">
메시지:
{errors.message && <span className="sr-only">오류: {errors.message}</span>}
</label>
<textarea
id="message"
name="message"
rows="5"
value={formData.message}
onChange={handleChange}
aria-invalid={errors.message ? 'true' : 'false'}
aria-describedby={errors.message ? 'message-error' : undefined}
className={errors.message ? 'error-input' : ''}
></textarea>
{errors.message && (
<div id="message-error" className="error-message">
{errors.message}
</div>
)}
</div>
<button type="submit">전송하기</button>
</form>
</div>
);
}
export default AccessibleForm;
결론
React에서 폼 관리는 사용자 입력을 처리하는 중요한 부분입니다. 간단한 폼은 React의 기본 기능만으로도 충분히 처리할 수 있지만, 복잡한 폼의 경우 상태 관리, 유효성 검사, 오류 처리 등이 까다로울 수 있습니다.
자신만의 커스텀 폼 로직을 구현하거나 Formik, React Hook Form과 같은 라이브러리를 활용하면 복잡한 폼을 더 쉽게 관리할 수 있습니다. 또한, 스타일링과 접근성에 주의를 기울여 모든 사용자가 쉽게 사용할 수 있는 폼을 만드는 것이 중요합니다.
다양한 폼 요소와 패턴을 조합하여 애플리케이션의 요구사항에 맞는 최적의 폼 솔루션을 구현하세요.
'Frameworks > React' 카테고리의 다른 글
React + TypeScript 도입과 패턴 (0) | 2025.04.02 |
---|---|
React 애플리케이션 성능 최적화 (0) | 2025.04.01 |
React 컴포넌트 스타일링 방법 (0) | 2025.04.01 |
에러 처리와 로딩 상태 관리 (0) | 2025.04.01 |
고급 데이터 가져오기 라이브러리 (0) | 2025.04.01 |