회원가입 양식 리팩토링 및 UX 향상

BangDori·2023년 9월 16일
1

RooTrip

목록 보기
1/6

0. 들어가기 전에

기존 회원가입 화면의 경우, 화면이 깔끔하지 않을 뿐 아니라 코드가 굉장히 복잡해 수정이 힘들었습니다. 그래서 회원가입 폼에 문제점들을 나열하고 하나씩 리팩토링 하는 과정을 거쳤습니다.

우선 회원가입 폼에 대한 문제점들을 나열해보았습니다.

  1. 사용자 친화적이지 않다.
    1-1. 사용자에게 입력받는 요소가 너무 많다.
    1-2. 이메일, 닉네임, 비밀번호 등에 대해 정규표현식을 통과하지 못하더라도, 가입하기 버튼을 클릭하기 전까지는 확인할 수 없다.
    1-3. 회원가입을 작성하다가 실수로 회원가입 양식을 벗어날 경우, 상태가 저장되지 못한다.
    1-4. 회원가입이 진행 중이라는 상태를 사용자에게 알려주어야 한다.
  2. 코드의 가독성이 떨어져, 확장이 힘들다.
  3. 양식 작성 시간이 너무 오래걸린다.

리팩토링을 통해 위 문제점들을 어떻게 해결하였는지, 확인해보겠습니다 !

1. 사용자 친화적인 화면

1-1. 사용자에게 입력받는 요소를 줄이자!

처음에는 서비스에서 모든 정보들을 입력받아 관리하는 방향으로 생각을 하여 디자인을 했기에,

  1. 이름
  2. 이메일
  3. 이메일 인증
  4. 닉네임
  5. 비밀번호
  6. 비밀번호 확인

글로만 적어도 많은 양의 정보들을 입력받고 있었습니다. 그래서 "이 모든 정보를 받을 필요가 있을 까"부터 시작을 했습니다.

그래서 이메일 인증과 비밀번호 확인란에 대해 수 많은 고민을 팀원과 진행하였습니다. 이메일 인증의 경우 이메일을 입력한 사용자를 식별하기 위한 정보로 반드시 진행되어야 하는 과정이였습니다.

하지만, 개발자의 입장에서는 단순히 코드 몇 줄을 입력해 이메일 입력과 인증 로직을 추가하면 되지만, 사용자의 입장에서는

  • 이메일 입력 -> 이메일 인증번호 전송 -> 인증번호 입력 -> 인증번호 확인

이 모든 과정을 거쳐야 했기 때문에, 사용자의 UX를 저하시킬 수 있다고 생각하여 이메일 인증 로직을 이메일 주소로 Redirect URL을 보내 인증하는 방식으로 사용하게 되었습니다.

비밀번호 확인의 경우, 사용자가 한번 더 비밀번호를 입력함으로써 사용자가 오타로 인해 비밀번호를 잘못 입력하는 경우를 방지할 수 있는 중요한 요소이지만 서비스를 이용하기 위해 로그인을 해야 하는 특성상 회원가입 양식을 최대한 줄여 빠르게 서비스를 이용할 수 있는 것이 더 좋을 것 같다라는 생각을 하여 비밀번호 확인란을 지우기로 팀원들과 결정하였습니다.

입력하는 양식을 줄이고, 화면에 불필요한 정보들을 줄여 사용자가 최대한 간편하게 회원가입을 진행할 수 있도록 하였습니다.

1-2. 입력란에 대해 통과 및 실패 정보 알려주기

기존에는 입력란을 입력하더라도, 정상적인 입력인지, 정규표현식을 통과하지 못하였는지를 출력해주지 않기에 사용자에게 혼란을 줄 수 있다고 생각하였습니다.

사용자가 더 편한 입력을 할 수 있게 하기 위해, react-hook-form 라이브러리의 formState 속성 내부에 있는 errors를 활용하여 에러가 발생할 시 ErrorMessage를 이용하여 쉽게 문제가 발생했다는 것을 감지할 수 있게 하였습니다.

정상적으로 통과할 경우에는 다음과 같이 정상적인인 입력임을 알 수 있게 하였습니다.

1-3. 페이지 벗어나기 방지

기존에 React Router v5에서 제공하는 usePrompt를 이용하여 실수로 페이지를 벗어나는 것을 방지하려고 시도하였습니다. 하지만 error가 발생하는 것을 확인하고 React Router v6로 업데이트되면서 중단되었다는 사실을 알게되었습니다.

그렇다고 해서, 현재 프로젝트에서 React Router v6.4의 loader, action 등의 다양한 기능을 적용하고 있는 터라 쉽사리 React Router v5로 다운그레이드 하는 것을 선택할 수 없었습니다.

구글링을 통해, 페이지 이탈을 방지하기 윟나 usePreventLeave hook을 찾아볼 수 있었고 이를 프로젝트에 적용하였습니다.

페이지 벗어나기 방지에 대한 코드의 경우에는 어떻게 보면 사소하다고 생각할 수 있지만, 이렇게 사소한 것들이 모여 사용자들을 더 편하게 만들어줄 수 있다고 생각하여 도입하였습니다.

1-4. 회원가입 스피너

기존에는 가입하기 버튼을 클릭하더라도, 바로 화면 페이지로 전환되었기 때문에 스피너를 도입해야 할 필요가 있을까에 생각해보았습니다.

하지만, 인터넷을 이용하는 모든 사용자들의 속도가 빠르지 않기 때문에 개발자도구의 Network Fast 3G 모드로 설정하고 테스트를 진행하였습니다.

React Router의 useNavigation hook과 react-spinners을 이용하여, navigation의 상태가 submitting 상태인지를 확인하고, submitting 상태라면 spinner가 동작하도록 스피너를 적용할 수 있게 되었습니다. 아래는 회원가입 스피너를 적용한 코드입니다.

<button className='submit-button' type='submit' disabled={isSubmitting}>
  {isSubmitting && (
  <BeatLoader
	className='beat-spinner'
    color='#ffff'
    size='5'
    aria-label='Loading Spinner'
    data-testid='loader'
    />
  )}
  {!isSubmitting && '가입하기'}
</button>

회원가입이 진행중이라는 표시와 함께 사용자와 원활한 상호작용을 할 수 있게 되었으며, 회원가입 버튼 클릭시 회원가입 버튼이 disabled 되기 때문에 회원가입을 중복으로 클릭하는 문제도 해결할 수 있었습니다.

2. 코드의 가독성 향상

이전 코드의 에서는 회원 가입 양식을 아래와 같이 관리하였습니다.

const [form, setForm, resetForm] = useInitialState({
  name: '',
  gender: 'M',
  email: '',
  nickname: '',
  password: '',
  password2: '',
});

이렇게 관리하니, 실제로 모든 form에 대해 validation도 따로 설정해줘야 했고 error도 따로 설정해줘야 하고 SignUpForm에서 발생하는 로직도 처리해야하고,,, 불필요한 코드가 너무 많아지게 되었습니다.

실제로, 다음과 같은 코드가 구성되었습니다.

const RegisterForm = ({ onRegister }) => {
  const [form, setForm, resetForm] = useInitialState({
    name: '',
    gender: 'M',
    email: '',
    nickname: '',
    password: '',
    password2: '',
  });
  const [validation, setValidation, validateForm] = useValidateForm({
    name: false,
    email: false,
    nickname: '※ 한글, 영어, 숫자를 조합한 닉네임을 입력해주세요.',
    password: '※ 숫자, 영어, 특수문자를 포함해 8~16자리로 입력해주세요.',
    password2: '※ 위 입력한 비밀번호를 다시 입력해주세요.',
  });
  const [isLoading, setIsLoading] = useState(false);

  const onInput = useCallback(
    (e) => {
     // ...
    },
    [...],
  );

  const confirmPassword = useCallback(
    (e) => {
      // ...
    },
    [...],
  );

  const handleSubmit = useCallback(
    async (e) => {
      // ...
    },
    [...],
  );

  return (
    <form className='register-form' onSubmit={handleSubmit}>
      <section className='register-section'>
        <div className='input-register_form'>
          <Input
            name='name'
            value={form.name}
            onChange={onInput}
            onBlur={validateForm}
            placeholder='이름을 입력해주세요'
          />
          <div className='check_box'>
            <span className='gender'>
              <Button
                type='button'
                name='gender'
                value='M'
                className={cn({ checked: form.gender === 'M' })}
                onClick={onInput}
                content='남'
              />
            </span>
            <span className='gender'>
              <Button
                type='button'
                name='gender'
                value='W'
                className={cn({ checked: form.gender === 'W' })}
                onClick={onInput}
                content='여'
              />
            </span>
          </div>
        </div>
        <RegisterEmailAuth
          type={'register'}
          email={form.email}
          onInput={onInput}
          setValidation={setValidation}
        />
        <div className='input-register_form'>
          <Input
            name='nickname'
            value={form.nickname}
            onChange={onInput}
            onBlur={validateForm}
            placeholder='닉네임을 입력해주세요'
          >
            <p>{validation.nickname}</p>
          </Input>
        </div>
        <div className='input-register_form'>
          <Input
            className='password'
            type='password'
            name='password'
            value={form.password}
            onChange={onInput}
            onBlur={validateForm}
            placeholder='비밀번호를 입력해주세요'
          >
            <p>{validation.password}</p>
          </Input>
        </div>
        <div className='input-register_form'>
          <Input
            className='password2'
            type='password'
            name='password2'
            value={form.password2}
            onChange={onInput}
            onBlur={confirmPassword}
            placeholder='비밀번호를 한번 더 입력해주세요'
          >
            <p>{validation.password2}</p>
          </Input>
        </div>
      </section>

      <RegisterButton />
    </form>
  );
};

export default RegisterForm;

그래서 만약, 위 코드에서 새로운 양식을 추가하게 된다면 아주 끔찍한 코드 분석 시간을 거쳐야 했습니다.

그래서 이를 React Router의 action과 react-hook-form을 사용하여 코드의 가독성을 향상시킬 수 있었습니다.

2-1. react-hook-form 적용하기

// SignUp Component

const SignUpForm = ({ error, isSubmitting }) => {
  const {
    register,
    handleSubmit,
    formState: { errors, isDirty },
    setError,
    reset,
    setFocus,
    getValues,
  } = useForm({
    mode: 'onBlur',
  });

  useEffect(() => setFocus('email'), [setFocus]);

  const onSignUp = (signupForm) => {
  	// ...
  };

  const onSuccessEmail = regExpEmail.test(getValues('email'))
    ? 'input-success'
    : undefined;
  const onSuccessName = regExpName.test(getValues('name'))
    ? 'input-success'
    : undefined;
  const onSuccessNickname =
    regExpNickname.test(getValues('nickname')) && !errors.nickname
      ? 'input-success'
      : undefined;
  const onSuccessPassword = regExpPassword.test(getValues('password'))
    ? 'input-success'
    : undefined;

  return (
    <form
      method='post'
      className='signup-form'
      onSubmit={handleSubmit(onSignUp)}
    >
      <div className='input-container'>
        <input
          type='text'
          placeholder='이메일 (test@test.com)'
          className={onSuccessEmail}
          {...register('email', {
            required: '※ 필수 항목 입니다.',
            pattern: {
              value: regExpEmail,
              message: '※ 이메일이 올바르지 않습니다.',
            },
          })}
        />
        <ErrorMessage
          errors={errors}
          name='email'
          render={({ message }) => <p className='error-message'>{message}</p>}
        />
      </div>
      <div className='input-container'>
        <input
          type='text'
          placeholder='이름을 입력해주세요.'
          className={onSuccessName}
          {...register('name', {
            required: '※ 필수 항목 입니다.',
            pattern: {
              value: regExpName,
              message: '※ 2자 이상의 한글 이름을 입력해주세요.',
            },
          })}
        />
        <ErrorMessage
          errors={errors}
          name='name'
          render={({ message }) => <p className='error-message'>{message}</p>}
        />
      </div>
      <div className='input-container'>
        <input
          type='text'
          placeholder='닉네임을 입력해주세요.'
          className={onSuccessNickname}
          {...register('nickname', {
            required: '※ 필수 항목 입니다.',
            pattern: {
              value: regExpNickname,
              message: '※ 2~8자 이내의 닉네임을 입력해주세요.',
            },
          })}
        />

        <ErrorMessage
          errors={errors}
          name='nickname'
          render={({ message }) => <p className='error-message'>{message}</p>}
        />
      </div>
      <div className='input-container'>
        <input
          type='password'
          placeholder='비밀번호를 입력해주세요.'
          className={onSuccessPassword}
          {...register('password', {
            required: '※ 필수 항목 입니다.',
            pattern: {
              value: regExpPassword,
              message:
                '※ 비밀번호는 8~16자 사이, 영문, 숫자, 특수문자($@$!%*?&)를 모두 포함해야 합니다.',
            },
          })}
        />
        <ErrorMessage
          errors={errors}
          name='password'
          render={({ message }) => <p className='error-message'>{message}</p>}
        />
      </div>

      <button className='submit-button' type='submit' disabled={isSubmitting}>
        {isSubmitting && (
          <BeatLoader
            className='beat-spinner'
            color='#ffff'
            size='5'
            aria-label='Loading Spinner'
            data-testid='loader'
          />
        )}
        {!isSubmitting && '가입하기'}
      </button>
    </form>
  );
};

export default SignUpForm;

react-hook-form의 useForm을 이용하여 입력 정보들을 ref로 관리하여 input, validation, error_message 들을 만들지 않고도 재사용성있게 입력 란들을 생성 및 관리할 수 있었습니다.

2-2. React Router action 적용하기

위 코드에서 onSignUp 함수만 해도 30라인이 넘어 코드가 급격하게 길어졌습니다. 이를 page component의 action으로 분리하고, action으로 전송하기 위해 useSubmit을 활용하여 더욱 가독성 있게 코드를 작성할 수 있었습니다.

// SignUp Component
  
  ...
  const submit = useSubmit()
  const onSignUp = (signupForm) => submit(signupForm, { method: 'post' });

  ...
// SignUp Page

const SignUpPage = () => {
  const error = useActionData();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === 'submitting';

  return <SignUp error={error} isSubmitting={isSubmitting} />;
};

export default SignUpPage;

export async function action({ request }) {
  // server에 API 요청
  ...

  // 중복된 이메일
  ...

  // 중복된 닉네임
  ...
  
  // 회원가입 성공
  return redirect('/');
}
=

실제로, SignUpPage에서 action을 통해 api 요청들을 처리하고, SignUpComponent에서는 회원가입 양식에 대한 정보들만을 담당할 수 있게 되었습니다.

이를 통해 코드가 더욱 가독성있게 되었고, 컴포넌트들을 더 쉽게 확장할 수 있게 되었습니다.

3. 성능 및 양식 시간 비교

천천히 입력하였을 때를 기준으로 총 18.93초가 걸리는 것을 확인할 수 있었습니다. 그렇다면 새롭게 리팩토링 된 회원가입 양식에서는 몇초가 걸릴까요?

천천히 입력하였을 때를 기준으로 리팩토링 된 코드에서는 총 12.04초가 걸리는 것을 확인할 수 있었습니다.

입력 양식이 줄어듬으로써, Rendering 시간과 Painting 시간이 약 절반 가량 줄어들게 되었으며 가장 중요햇던 Scripting 시간이 줄어들게 되었습니다.

앞에서도 설명했듯이 로그인을 해야만 사용할 수 있는 서비스의 특성 상 회원가입 양식 작성 시간을 줄이는 것에 최대한 포커스를 맞추고 제작하였는데, 10배 가량 입력 시간이 단축되었습니다. 정말 뿌듯하네요!! 😊😊

4. 마치며

오늘의 포스팅은 여기까지 입니다 !!

리액트는 매번 새롭고 신기한 기능들이 많아서 너무 재미있네요!! 모든 사용자들이 편하게 사용할 수 있는 그날까지 화이팅하겠습니당!!

오타가 있거나 혹은 궁금한 점이 있으시다면 댓글로 남겨주세요!

profile
Happy Day 😊❣️

0개의 댓글