Updated:

제어 컴포넌트 (Controlled Components)

import { useState } from 'react';

function ControlledForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
    gender: 'male',
    subscribe: false,
    interests: []
  });
  
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    if (type === 'checkbox') {
      if (name === 'subscribe') {
        // 단일 체크박스
        setFormData({
          ...formData,
          [name]: checked
        });
      } else if (name === 'interests') {
        // 체크박스 그룹
        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('Form submitted:', formData);
    // API 호출 등의 로직...
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
      </div>
      
      <div>
        <label htmlFor="message">Message:</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
        />
      </div>
      
      <div>
        <p>Gender:</p>
        <label>
          <input
            type="radio"
            name="gender"
            value="male"
            checked={formData.gender === 'male'}
            onChange={handleChange}
          />
          Male
        </label>
        <label>
          <input
            type="radio"
            name="gender"
            value="female"
            checked={formData.gender === 'female'}
            onChange={handleChange}
          />
          Female
        </label>
      </div>
      
      <div>
        <p>Interests:</p>
        <label>
          <input
            type="checkbox"
            name="interests"
            value="sports"
            checked={formData.interests.includes('sports')}
            onChange={handleChange}
          />
          Sports
        </label>
        <label>
          <input
            type="checkbox"
            name="interests"
            value="music"
            checked={formData.interests.includes('music')}
            onChange={handleChange}
          />
          Music
        </label>
        <label>
          <input
            type="checkbox"
            name="interests"
            value="reading"
            checked={formData.interests.includes('reading')}
            onChange={handleChange}
          />
          Reading
        </label>
      </div>
      
      <div>
        <label>
          <input
            type="checkbox"
            name="subscribe"
            checked={formData.subscribe}
            onChange={handleChange}
          />
          Subscribe to newsletter
        </label>
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}

코멘트: 제어 컴포넌트는 React 상태를 “단일 진실 공급원(single source of truth)”으로 사용합니다. 모든 폼 요소의 값은 React 상태에 저장되고, 상태 변경 함수를 통해서만 업데이트됩니다. 이 방식은 폼 데이터에 대한 완전한 제어를 제공하지만, 많은 입력 필드가 있는 경우 코드가 복잡해질 수 있습니다.

비제어 컴포넌트 (Uncontrolled Components)

import { useRef } from 'react';

  const handleSubmit = (e) => {
    e.preventDefault();
    
    // ref를 통해 직접 DOM 요소의 값에 접근
    const formData = {
      name: nameRef.current.value,
      email: emailRef.current.value,
      message: messageRef.current.value,
      file: fileRef.current.files[0]
    };
    
    console.log('Form submitted:', formData);
    // API 호출 등의 로직...
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="name"
          name="name"
          ref={nameRef}
          defaultValue=""
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          ref={emailRef}
          defaultValue=""
        />
      </div>
      
      <div>
        <label htmlFor="message">Message:</label>
        <textarea
          id="message"
          name="message"
          ref={messageRef}
          defaultValue=""
        />
      </div>
      
      <div>
        <label htmlFor="file">Upload File:</label>
        <input
          type="file"
          id="file"
          name="file"
          ref={fileRef}
        />
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}

코멘트: 비제어 컴포넌트는 DOM 자체를 데이터의 진실 공급원으로 사용합니다. React의 ref를 사용하여 필요할 때(주로 폼 제출 시) DOM 요소의 값에 접근합니다. 이 방식은 코드가 간결하고 파일 입력과 같은 일부 입력 타입에 더 적합할 수 있지만, 실시간 유효성 검사나 조건부 렌더링과 같은 기능을 구현하기 어렵습니다.

폼 라이브러리: React Hook Form

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

function ReactHookFormExample() {
  const { 
    register, 
    handleSubmit, 
    formState: { errors }, 
    watch 
  } = useForm();
  
  const onSubmit = (data) => {
    console.log('Form submitted:', data);
    // API 호출 등의 로직...
  };
  
  // 실시간으로 password 값 감시
  const password = watch('password');
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          id="name"
          {...register('name', { 
            required: 'Name is required',
            minLength: { value: 2, message: 'Name must be at least 2 characters' }
          })}
        />
        {errors.name && <p className="error">{errors.name.message}</p>}
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          type="email"
          {...register('email', { 
            required: 'Email is required',
            pattern: { 
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, 
              message: 'Invalid email address' 
            }
          })}
        />
        {errors.email && <p className="error">{errors.email.message}</p>}
      </div>
      
      <div>
        <label htmlFor="password">Password:</label>
        <input
          id="password"
          type="password"
          {...register('password', { 
            required: 'Password is required',
            minLength: { value: 8, message: 'Password must be at least 8 characters' }
          })}
        />
        {errors.password && <p className="error">{errors.password.message}</p>}
      </div>
      
      <div>
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input
          id="confirmPassword"
          type="password"
          {...register('confirmPassword', { 
            required: 'Please confirm your password',
            validate: value => value === password || 'Passwords do not match'
          })}
        />
        {errors.confirmPassword && <p className="error">{errors.confirmPassword.message}</p>}
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}

코멘트: React Hook Form은 성능과 개발자 경험을 모두 고려한 인기 있는 폼 라이브러리입니다. 비제어 컴포넌트 방식을 기반으로 하면서도 유효성 검사, 에러 처리, 폼 상태 관리 등의 기능을 제공합니다. 복잡한 폼을 다룰 때 코드량을 줄이고 성능을 향상시킬 수 있습니다.

폼 유효성 검사

import { useState } from 'react';

function FormValidation() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  });
  
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  
  // 입력 변경 처리
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
    
    // 입력 변경 시 유효성 검사
    validateField(name, value);
  };
  
  // 필드 포커스 아웃 처리
  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched({
      ...touched,
      [name]: true
    });
    
    // 포커스 아웃 시 유효성 검사
    validateField(name, formData[name]);
  };
  
  // 단일 필드 유효성 검사
  const validateField = (name, value) => {
    let fieldErrors = { ...errors };
    
    switch (name) {
      case 'username':
        if (!value) {
          fieldErrors.username = 'Username is required';
        } else if (value.length < 3) {
          fieldErrors.username = 'Username must be at least 3 characters';
        } else {
          delete fieldErrors.username;
        }
        break;
        
      case 'email':
        if (!value) {
          fieldErrors.email = 'Email is required';
        } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
          fieldErrors.email = 'Invalid email address';
        } else {
          delete fieldErrors.email;
        }
        break;
        
      case 'password':
        if (!value) {
          fieldErrors.password = 'Password is required';
        } else if (value.length < 8) {
          fieldErrors.password = 'Password must be at least 8 characters';
        } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
          fieldErrors.password = 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
        } else {
          delete fieldErrors.password;
        }
        break;
        
      default:
        break;
    }
    
    setErrors(fieldErrors);
    return Object.keys(fieldErrors).length === 0;
  };
  
  // 전체 폼 유효성 검사
  const validateForm = () => {
    let isValid = true;
    
    // 모든 필드 검사
    Object.keys(formData).forEach(name => {
      if (!validateField(name, formData[name])) {
        isValid = false;
      }
    });
    
    // 모든 필드를 touched로 표시
    const allTouched = Object.keys(formData).reduce((acc, field) => {
      acc[field] = true;
      return acc;
    }, {});
    
    setTouched(allTouched);
    return isValid;
  };
  
  // 폼 제출 처리
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      console.log('Form is valid, submitting:', formData);
      // API 호출 등의 로직...
    } else {
      console.log('Form has errors, cannot submit');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username:</label>
        <input
          type="text"
          id="username"
          name="username"
          value={formData.username}
          onChange={handleChange}
          onBlur={handleBlur}
          className={touched.username && errors.username ? 'error' : ''}
        />
        {touched.username && errors.username && (
          <p className="error-message">{errors.username}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          className={touched.email && errors.email ? 'error' : ''}
        />
        {touched.email && errors.email && (
          <p className="error-message">{errors.email}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
          className={touched.password && errors.password ? 'error' : ''}
        />
        {touched.password && errors.password && (
          <p className="error-message">{errors.password}</p>
        )}
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}

코멘트: 이 예제는 수동으로 폼 유효성 검사를 구현하는 방법을 보여줍니다. 실제 프로젝트에서는 Formik, React Hook Form과 같은 라이브러리나 Yup, Zod와 같은 스키마 유효성 검사 라이브러리를 사용하는 것이 더 효율적입니다. 이 패턴의 핵심 요소는 다음과 같습니다:

  • 각 필드의 오류 상태 추적
  • 사용자가 상호작용한 필드만 유효성 검사 메시지 표시 (touched 상태)
  • 실시간 유효성 검사 (입력 변경 시)
  • 최종 유효성 검사 (폼 제출 시)

Categories:

Updated:

Leave a comment