icon안동민 개발노트

회원가입 폼 만들기


 이 실습에서는 react-hook-form을 사용하여 완전한 회원가입 폼을 구현해보겠습니다.

 이 과정을 통해 폼 처리, 유효성 검사, 접근성, 그리고 사용자 경험 향상 기법 등을 종합적으로 적용해볼 수 있습니다.

1단계 : 프로젝트 설정

 먼저 필요한 의존성을 설치합니다.

npm install react-hook-form @hookform/resolvers yup

 여기서 yup은 스키마 기반 유효성 검사를 위해 사용됩니다.

2단계 : 회원가입 폼 컴포넌트 생성

 SignupForm.js 파일을 생성하고 기본 구조를 작성합니다.

src/SignupForm.js
import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
 
function SignupForm() {
  // 폼 로직은 여기에 구현됩니다
  return (
    <form>
      {/* 폼 필드들은 여기에 구현됩니다 */}
    </form>
  );
}
 
export default SignupForm;

3단계 : 유효성 검사 스키마 정의

 yup을 사용하여 유효성 검사 스키마를 정의합니다.

const schema = yup.object().shape({
  name: yup.string().required('이름은 필수입니다'),
  email: yup.string().email('유효한 이메일 주소를 입력하세요').required('이메일은 필수입니다'),
  password: yup.string()
    .min(8, '비밀번호는 최소 8자 이상이어야 합니다')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, 
      '비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다')
    .required('비밀번호는 필수입니다'),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password'), null], '비밀번호가 일치하지 않습니다')
    .required('비밀번호 확인은 필수입니다'),
});

4단계 : useForm 훅 설정

 useForm 훅을 사용하여 폼 상태를 관리합니다.

function SignupForm() {
  const { register, handleSubmit, formState: { errors }, watch } = useForm({
    resolver: yupResolver(schema)
  });
 
  const onSubmit = async (data) => {
    try {
      // API 호출 로직
      console.log('Form submitted:', data);
      // 성공 처리
    } catch (error) {
      // 오류 처리
    }
  };
 
  // ... 폼 JSX
}

5단계 : 폼 필드 구현

 각 폼 필드를 구현하고 유효성 검사를 연결합니다.

return (
  <form onSubmit={handleSubmit(onSubmit)}>
    <div>
      <label htmlFor="name">이름</label>
      <input
        id="name"
        {...register('name')}
        aria-invalid={errors.name ? "true" : "false"}
      />
      {errors.name && <span role="alert">{errors.name.message}</span>}
    </div>
 
    <div>
      <label htmlFor="email">이메일</label>
      <input
        id="email"
        type="email"
        {...register('email')}
        aria-invalid={errors.email ? "true" : "false"}
      />
      {errors.email && <span role="alert">{errors.email.message}</span>}
    </div>
 
    <div>
      <label htmlFor="password">비밀번호</label>
      <input
        id="password"
        type="password"
        {...register('password')}
        aria-invalid={errors.password ? "true" : "false"}
      />
      {errors.password && <span role="alert">{errors.password.message}</span>}
    </div>
 
    <div>
      <label htmlFor="confirmPassword">비밀번호 확인</label>
      <input
        id="confirmPassword"
        type="password"
        {...register('confirmPassword')}
        aria-invalid={errors.confirmPassword ? "true" : "false"}
      />
      {errors.confirmPassword && <span role="alert">{errors.confirmPassword.message}</span>}
    </div>
 
    <button type="submit">가입하기</button>
  </form>
);

6단계 : 비밀번호 강도 체크 기능 추가

 비밀번호 입력 필드 아래에 비밀번호 강도를 표시하는 컴포넌트를 추가합니다.

function PasswordStrengthMeter({ password }) {
  const getPasswordStrength = (password) => {
    let strength = 0;
    if (password.length >= 8) strength++;
    if (password.match(/[a-z]+/)) strength++;
    if (password.match(/[A-Z]+/)) strength++;
    if (password.match(/[0-9]+/)) strength++;
    if (password.match(/[$@#&!]+/)) strength++;
    return strength;
  };
 
  const strength = getPasswordStrength(password);
 
  return (
    <div className="password-strength-meter">
      <div className={`strength-${strength}`}></div>
      <span>비밀번호 강도: {['매우 약함', '약함', '보통', '강함', '매우 강함'][strength]}</span>
    </div>
  );
}
 
// SignupForm 컴포넌트 내부
const password = watch('password');
// ...
<PasswordStrengthMeter password={password} />

7단계 : 폼 제출 후 처리

 폼 제출 후 성공 또는 실패 메시지를 표시하는 로직을 추가합니다.

function SignupForm() {
  // ...이전 코드...
  const [submitStatus, setSubmitStatus] = useState(null);
 
  const onSubmit = async (data) => {
    try {
      // API 호출 시뮬레이션
      await new Promise(resolve => setTimeout(resolve, 1000));
      setSubmitStatus('success');
    } catch (error) {
      setSubmitStatus('error');
    }
  };
 
  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        {/* ...폼 필드... */}
      </form>
      {submitStatus === 'success' && <div role="alert">회원가입에 성공했습니다!</div>}
      {submitStatus === 'error' && <div role="alert">회원가입 중 오류가 발생했습니다. 다시 시도해주세요.</div>}
    </>
  );
}

8단계 : 접근성 개선

 폼의 접근성을 개선하기 위해 추가적인 ARIA 속성을 적용합니다.

<form onSubmit={handleSubmit(onSubmit)} noValidate aria-label="회원가입 폼">
  {/* ...폼 필드... */}
  <div role="status" aria-live="polite">
    {submitStatus === 'success' && "회원가입에 성공했습니다!"}
    {submitStatus === 'error' && "회원가입 중 오류가 발생했습니다. 다시 시도해주세요."}
  </div>
</form>

완성된 회원가입 폼 코드

src/SignupForm.js
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
 
const schema = yup.object().shape({
  name: yup.string().required('이름은 필수입니다'),
  email: yup.string().email('유효한 이메일 주소를 입력하세요').required('이메일은 필수입니다'),
  password: yup.string()
    .min(8, '비밀번호는 최소 8자 이상이어야 합니다')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, 
      '비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다')
    .required('비밀번호는 필수입니다'),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password'), null], '비밀번호가 일치하지 않습니다')
    .required('비밀번호 확인은 필수입니다'),
});
 
function PasswordStrengthMeter({ password }) {
  // ... (이전에 정의한 대로)
}
 
function SignupForm() {
  const { register, handleSubmit, formState: { errors }, watch } = useForm({
    resolver: yupResolver(schema)
  });
  const [submitStatus, setSubmitStatus] = useState(null);
 
  const onSubmit = async (data) => {
    try {
      await new Promise(resolve => setTimeout(resolve, 1000)); // API 호출 시뮬레이션
      setSubmitStatus('success');
    } catch (error) {
      setSubmitStatus('error');
    }
  };
 
  const password = watch('password');
 
  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate aria-label="회원가입 폼">
      <div>
        <label htmlFor="name">이름</label>
        <input
          id="name"
          {...register('name')}
          aria-invalid={errors.name ? "true" : "false"}
        />
        {errors.name && <span role="alert">{errors.name.message}</span>}
      </div>
 
      <div>
        <label htmlFor="email">이메일</label>
        <input
          id="email"
          type="email"
          {...register('email')}
          aria-invalid={errors.email ? "true" : "false"}
        />
        {errors.email && <span role="alert">{errors.email.message}</span>}
      </div>
 
      <div>
        <label htmlFor="password">비밀번호</label>
        <input
          id="password"
          type="password"
          {...register('password')}
          aria-invalid={errors.password ? "true" : "false"}
        />
        {errors.password && <span role="alert">{errors.password.message}</span>}
        <PasswordStrengthMeter password={password} />
      </div>
 
      <div>
        <label htmlFor="confirmPassword">비밀번호 확인</label>
        <input
          id="confirmPassword"
          type="password"
          {...register('confirmPassword')}
          aria-invalid={errors.confirmPassword ? "true" : "false"}
        />
        {errors.confirmPassword && <span role="alert">{errors.confirmPassword.message}</span>}
      </div>
 
      <button type="submit">가입하기</button>
 
      <div role="status" aria-live="polite">
        {submitStatus === 'success' && "회원가입에 성공했습니다!"}
        {submitStatus === 'error' && "회원가입 중 오류가 발생했습니다. 다시 시도해주세요."}
      </div>
    </form>
  );
}
 
export default SignupForm;

 이 실습을 통해 우리는 react-hook-form을 사용하여 완전한 회원가입 폼을 구현했습니다.

 이 폼은 유효성 검사, 실시간 피드백, 비밀번호 강도 체크, 접근성 고려, 그리고 제출 후 처리 등 실제 애플리케이션에서 필요한 다양한 기능을 포함하고 있습니다.

 주요 특징

  1. yup을 사용한 스키마 기반 유효성 검사
  2. 실시간 에러 메시지 표시
  3. 비밀번호 강도 체크 기능
  4. 접근성을 고려한 ARIA 속성 사용
  5. 폼 제출 후 성공/실패 처리

 이 예제는 실제 프로젝트에서 사용할 수 있는 기본적인 틀을 제공하며, 필요에 따라 추가적인 기능(예 : 이메일 인증, CAPTCHA 등)을 구현하여 확장할 수 있습니다.

 또한, 스타일링을 추가하여 사용자 인터페이스를 개선하고, 서버 측 유효성 검사와 연동하여 보안을 강화할 수 있습니다.