목록으로
React

React 폼 처리 완벽 가이드

React에서 폼을 효과적으로 다루는 방법. 제어 컴포넌트, 유효성 검사, React Hook Form 활용법을 알아봅니다.

폼은 웹 애플리케이션에서 사용자 입력을 받는 핵심 요소입니다. React에서 폼을 효과적으로 다루는 방법을 알아봅니다.

제어 컴포넌트

React 상태로 폼 값을 관리합니다.

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="이메일"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="비밀번호"
      />
      <button type="submit">로그인</button>
    </form>
  );
}

여러 필드 처리

function SignupForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: '',
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      {/* ... */}
    </form>
  );
}

유효성 검사

function SignupForm() {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const newErrors = {};

    if (!formData.email) {
      newErrors.email = '이메일을 입력하세요';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = '올바른 이메일 형식이 아닙니다';
    }

    if (!formData.password) {
      newErrors.password = '비밀번호를 입력하세요';
    } else if (formData.password.length < 8) {
      newErrors.password = '비밀번호는 8자 이상이어야 합니다';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      // 제출 로직
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      {/* ... */}
    </form>
  );
}

React Hook Form

대규모 폼에서는 라이브러리를 사용하는 것이 효율적입니다.

import { useForm } from 'react-hook-form';

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    watch,
  } = useForm();

  const onSubmit = async (data) => {
    await submitForm(data);
  };

  const password = watch('password');

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          {...register('email', {
            required: '이메일을 입력하세요',
            pattern: {
              value: /\S+@\S+\.\S+/,
              message: '올바른 이메일 형식이 아닙니다',
            },
          })}
          type="email"
        />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <input
          {...register('password', {
            required: '비밀번호를 입력하세요',
            minLength: {
              value: 8,
              message: '비밀번호는 8자 이상이어야 합니다',
            },
          })}
          type="password"
        />
        {errors.password && <span>{errors.password.message}</span>}
      </div>

      <div>
        <input
          {...register('confirmPassword', {
            required: '비밀번호 확인을 입력하세요',
            validate: (value) =>
              value === password || '비밀번호가 일치하지 않습니다',
          })}
          type="password"
        />
        {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '처리 중...' : '가입하기'}
      </button>
    </form>
  );
}

Zod와 함께 사용

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('올바른 이메일 형식이 아닙니다'),
  password: z.string().min(8, '비밀번호는 8자 이상이어야 합니다'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: '비밀번호가 일치하지 않습니다',
  path: ['confirmPassword'],
});

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
  });

  // ...
}

폼 UX 개선

실시간 유효성 검사

const { register, formState: { errors, touchedFields } } = useForm({
  mode: 'onBlur', // 포커스 벗어날 때 검사
  // mode: 'onChange', // 입력할 때마다 검사
});

// 터치된 필드만 에러 표시
{touchedFields.email && errors.email && (
  <span>{errors.email.message}</span>
)}

로딩 상태 처리

function Form() {
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await submitData();
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 입력 필드들 */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? (
          <>
            <Spinner /> 처리 중...
          </>
        ) : (
          '제출'
        )}
      </button>
    </form>
  );
}

에러 메시지 스타일링

.field-error {
  color: #dc2626;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

.input-error {
  border-color: #dc2626;
}

.input-error:focus {
  outline-color: #dc2626;
}

마무리

간단한 폼은 useState로 충분하지만, 복잡한 폼에서는 React Hook Form 같은 라이브러리가 효율적입니다.

사용자 경험을 위해 실시간 유효성 검사, 명확한 에러 메시지, 로딩 상태 표시를 신경 쓰면 좋습니다.