팀프로젝트: Wingle(3.0) Refactor - signUpInput 리팩토링

윤뿔소·2023년 6월 6일
0

팀프로젝트: Wingle

목록 보기
16/16
post-thumbnail

오랜만의 윙글이다.

API 연결 이후 그동안 자잘한 수정점은 있었지만 기록을 따로 하진 않았다.

QA를 진행하면서 디자인 수정점이 있었고, 디자인 수정 뿐만 아니라 공통 UI 제작이 있었다. 적용을 하기 전 코드 분리를 먼저 해 운영 보수를 보다 쉽게 하겠다.

signUpInput.tsx 분석

500줄 가까이나 되는 엄청 큰 파일이고, 이메일부터 패스워드까지 모든 로직과 UI가 한 파일에 있었다. 사실 만들면서도 리팩토링해야겠다 생각은 했지만 API 연결이 급선무였기 때문에 먼저 연결을 하고, 리팩토링을 진행하려고 했다.

  1. 이메일 인증메일 보내기
    sendEmail 함수를 호출하여 이메일 인증메일을 보냄. useMutation 훅을 사용하여 비동기 요청을 처리하고, onMutate, onSuccess, onError 콜백을 사용하여 요청의 상태에 따라 UI를 업데이트.
  2. 이메일 인증번호 확인
    verifyEmail 함수를 호출하여 이메일 인증번호를 확인. 마찬가지로 useMutation 훅을 사용하여 비동기 요청을 처리하고, onSuccess, onError 콜백을 사용하여 요청의 상태에 따라 UI를 업데이트.
  3. 비밀번호, 비밀번호 확인, 이름 입력 시 회원가입 폼 데이터 저장
    useEffect 훅을 사용하여 비밀번호, 비밀번호 확인, 이름이 유효할 경우 회원가입 폼 데이터를 저장. 이를 위해 setSignUpFormData 함수를 사용하여 Recoil 상태를 업데이트.
  4. 비밀번호, 비밀번호 확인, 이름, 닉네임의 유효성 검사
    각 입력 필드의 값이 변경될 때마다 해당 필드의 유효성을 검사하고 에러 상태를 업데이트.
  5. 닉네임 중복 확인
    CheckNickname 함수를 호출하여 입력한 닉네임이 중복되는지 확인. useMutation 훅을 사용하여 비동기 요청을 처리하고, onSuccess, onError 콜백을 사용하여 요청의 상태에 따라 UI를 업데이트.

이 외에도 스타일 코드 등 나눠줘야할 게 많다..

어떻게 리팩토링?

일단 유효성 검사, 리액트 쿼리 등의 비즈니스 로직 구역과 리턴에 쓴 UI 구역으로 나눠서 리팩토링 규칙을 세울 것이다.

UI 구역

먼저 리턴을 보고 input마다 구역을 나눠서 리팩토링 할 것이다.

  1. 이메일 및 인증번호 구역
  2. 패스워드 및 확인 입력 구역
  3. 이름 입력 구역
  4. 닉네임 입력 및 확인 구역

이렇게 나눠서 리팩토링 할 것이다. src/components/authpage/signup/signUpInput 폴더를 만들고 index.tsx를 만들어 구역을 먼저 만들자.

import { Margin, Text } from "@/src/components/ui";
import EmailVerify from "./emailVerify";
import PasswordVerify from "./passwordVerify";
import NameInput from "./nameInput";
import NicknameVerify from "./nicknameVerify";

export default function InputBox() {
  return (
    <>
      <Text.Title1 color="gray900">학생 정보</Text.Title1>
      <Margin direction="column" size={16} />

      <EmailVerify />
      <PasswordVerify />
      <NameInput />
      <NicknameVerify />
    </>
  );
}

물론 이렇게 하고 emailVerify.tsx 식으로 파일을 만들고

export default function EmailVerify() {
  return (
    <>
      <Text.Body1 color="gray700">이메일</Text.Body1>
      <Margin direction="column" size={8} />
      ... // Input
    </>
  );
}

이렇게 리턴에 UI를 배치했다. 나머지 3개 파일도 똑같이 만들었다.

비즈니스 로직 구역

여기는 되게 귀찮다. 유효성 검사는 UI 구역 별로 나눠서 그 구역에 검사 코드를 배치하면 되고, 나머지 상태들도 나눠주면 된다.

  const [inputData, setInputData] = useState<SignupInputData>({
    email: "",
    emailCertification: "",
    password: "",
    passwordCheck: "",
    name: "",
    nickname: "",
  });
  const { email, emailCertification, password, passwordCheck, name, nickname } =
    inputData;
  const setSignUpFormData = useSetRecoilState(signUpFormDataAtom);

  const [isErrorEmailCertify, setErrorEmailCertify] = useState(true);
  const [isErrorPassword, setErrorPassword] = useState(true);
  const [isErrorPasswordCheck, setErrorPasswordCheck] = useState(true);
  const [isErrorName, setErrorName] = useState(true);
  const [isErrorNickName, setErrorNickName] = useState(true);
  const [isCheckedNickname, setCheckedNickname] = useState(false);
  const [isVerifiedNickname, setVerifiedNickname] = useState(false);

  const handleInputData = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setInputData((prevData) => ({
        ...prevData,
        [e.target.name]: e.target.value,
      }));
    },
    []
  );
... // 유효성 검사 코드

이 부분들을 각각 상태에 맞춰서 해야한다..

또한 기억해둬야할 것이 이메일 검증 및 인증이나 닉네임 확인 시 요청이 성공한다면 리코일 데이터에 넣어서 갱신시켜준다.

  // useEffect로 비밀번호, 비밀번호 확인, 이름 존재 시 회원가입 폼 데이터 저장
  useEffect(() => {
    if (!isErrorPassword && !isErrorPasswordCheck && !isErrorName) {
      setSignUpFormData((prev) => ({
        ...prev,
        password: password,
        name: name,
      }));
    }
  }, [
    isErrorName,
    isErrorPassword,
    isErrorPasswordCheck,
    name,
    password,
    setSignUpFormData,
  ]);

하지만 비번, 비번 확인, 이름은 입력만 하기에 데이터 입력 시 감지하여 넣어주는 것이라 이것도 나눠줘야한다. 이런 걸 유의하면서 나눠주자.

import { signUpFormDataAtom } from "@/src/atoms/auth/signUpAtoms";
import { Margin, Text } from "@/src/components/ui";
import { useCallback, useState } from "react";
import { useSetRecoilState } from "recoil";
import styled from "styled-components";
import { ErrorMent } from "../errorMent";
import {
  sendEmailAuth,
  verifyEmailCertification,
} from "@/src/api/auth/emailAPI";
import { useMutation } from "react-query";

interface StyledInputProps {
  small: boolean;
  error: boolean;
}

export default function EmailVerify() {
  const [buttonMessage, setButtonMessage] = useState("인증 전송");
  const [emailMent, setEmailMent] = useState("");

  const [inputData, setInputData] = useState({
    email: "",
    emailCertification: "",
  });
  const { email, emailCertification } = inputData;

  const setSignUpFormData = useSetRecoilState(signUpFormDataAtom);

  const [isErrorEmailCertify, setErrorEmailCertify] = useState(true);

  const handleInputData = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setInputData((prevData) => ({
        ...prevData,
        [e.target.name]: e.target.value,
      }));
    },
    []
  );

  // 이메일 인증메일 보내기
  const { mutate: sendEmail } = useMutation(() => sendEmailAuth(email), {
    onMutate: () => {
      setButtonMessage("전송 중");
    },
    onSuccess: () => {
      setButtonMessage("재전송");
      setEmailMent("인증메일을 전송했습니다.");
    },
    onError: (error) => {
      setErrorEmailCertify(true);
      alert(error);
      throw error;
    },
  });

  const handleSendEmail = useCallback(() => {
    if (email === "") {
      alert("이메일을 입력해주세요.");
      return;
    }
    sendEmail();
  }, [email, sendEmail]);

  // 이메일 인증번호 확인
  const { mutate: verifyEmail, isLoading: isLoadingVerifyEmail } = useMutation(
    () => verifyEmailCertification({ email, emailCertification }),
    {
      onSuccess: () => {
        setErrorEmailCertify(false);
        setSignUpFormData((prev) => ({
          ...prev,
          email,
        }));
      },
      onError: (error) => {
        setErrorEmailCertify(true);
        throw error;
      },
    }
  );

  const handleVerifyEmail = useCallback(() => {
    if (email === "") {
      alert("이메일을 입력해주세요.");
      return;
    }
    verifyEmail();
  }, [email, verifyEmail]);

  return (
    <>
      <Text.Body1 color="gray700">이메일</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={true} error={false}>
            <input
              name="email"
              value={email}
              type="email"
              placeholder="abc@naver.com"
              onChange={(e) => {
                handleInputData(e);
              }}
            />
          </S.InputField>
          <S.ButtonWrapper small={true} error={false}>
            <S.Button onClick={() => handleSendEmail()}>
              {buttonMessage}
            </S.Button>
          </S.ButtonWrapper>
        </S.Content>
        <ErrorMent error={false} errorMent="" ment={emailMent} />
      </S.ContentWrapper>
      <Text.Body1 color="gray700">인증번호 입력</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={true} error={isErrorEmailCertify}>
            <input
              name="emailCertification"
              value={emailCertification}
              type="string"
              placeholder="인증번호"
              onChange={(e) => {
                handleInputData(e);
              }}
            />
          </S.InputField>
          <S.ButtonWrapper small={true} error={isErrorEmailCertify}>
            <S.Button onClick={() => handleVerifyEmail()}>인증 확인</S.Button>
          </S.ButtonWrapper>
        </S.Content>
        {isLoadingVerifyEmail ? (
          <ErrorMent error={false} errorMent="" ment="인증 확인 중 입니다." />
        ) : (
          <ErrorMent
            error={isErrorEmailCertify}
            errorMent="인증정보가 일치하지 않습니다."
            ment="인증이 완료되었습니다."
          />
        )}
      </S.ContentWrapper>
    </>
  );
}

약간 이런 식으로 나눴다. 상태 값도 나눠줬고, 나머지 비즈니스 로직도 그대로 옮겨 왔다. 다른 파일도 비슷하게 했다.

아래는 위 이메일과는 다른 성격인 이름 입력 관련 컴포넌트도 보여주겠다.

import { signUpFormDataAtom } from "@/src/atoms/auth/signUpAtoms";
import { Margin, Text } from "@/src/components/ui";
import { useCallback, useEffect, useState } from "react";
import { useSetRecoilState } from "recoil";
import styled from "styled-components";
import { ErrorMent } from "../errorMent";

interface StyledInputProps {
  small: boolean;
  error: boolean;
}

export default function NameInput() {
  const [inputData, setInputData] = useState({
    name: "",
  });
  const { name } = inputData;
  const setSignUpFormData = useSetRecoilState(signUpFormDataAtom);

  const [isErrorName, setErrorName] = useState(true);

  // useEffect로 이름 존재 시 회원가입 폼 데이터 저장
  useEffect(() => {
    if (!isErrorName) {
      setSignUpFormData((prev) => ({
        ...prev,
        name: name,
      }));
    }
  }, [isErrorName, name, setSignUpFormData]);

  const handleInputData = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setInputData((prevData) => ({
        ...prevData,
        [e.target.name]: e.target.value,
      }));
    },
    []
  );

  // 이름 유효성 검사
  const handleErrorName = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const special_pattern = /^[a-zA-Z가-힣\s]+$/;
      if (!special_pattern.test(e.target.value)) {
        setErrorName(true);
      } else {
        setErrorName(false);
      }
    },
    []
  );

  return (
    <>
      <Text.Body1 color="gray700">이름</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={false} error={isErrorName}>
            <input
              name="name"
              value={name}
              type="string"
              placeholder="김윙글"
              onChange={(e) => {
                handleInputData(e);
                handleErrorName(e);
              }}
            />
          </S.InputField>
        </S.Content>
        <ErrorMent
          error={isErrorName}
          errorMent="실명을 입력하세요 (한글, 영어 대/소문자 사용 가능) "
          ment=" 실명을 입력하세요 (한글, 영어 대/소문자 사용 가능) "
        />
      </S.ContentWrapper>
    </>
  );
}

const { name } = inputData; 이런 곳 최적화 진행하고 이렇게 나눴다.

결론

거의 노가다라 설명이 별로 없지만 이번 기회에 큰 교훈을 얻었다.

  1. 프론트라도 처음 아키텍쳐를 잘 짜서 코드 분리를 잘하자.
  2. 코드 분리가 잘 됐다면 비즈니스 로직을 짜는 것도 쉬워진다.
  3. 나중에 코드 분리하고 리팩토링 하는게 만드는 것보다 훨씬 어렵다;;

이런 교훈을 얻었다. 물론 처음 코드가 내 코드가 아니라서 이렇게 만들었지만 나중에 내가 만든다고 생각하면 꼭 꼭 간단하면서도 로직을 잘 분리시켜 만들어야겠다고 생각했다.. 끝!

profile
코뿔소처럼 저돌적으로

6개의 댓글

comment-user-thumbnail
2023년 6월 7일

저도 항상 일단 급한거부터 하고 리펙토링하자! 마음먹게 되는데 나중에 하는게 훨씬 어렵고 복잡하게 느껴지는 것 같아요 ㅠㅠ 고생하셨습니다!

답글 달기
comment-user-thumbnail
2023년 6월 7일

고생하셨습니다~!

답글 달기
comment-user-thumbnail
2023년 6월 8일

리팩토링... 고생하셨습니다 !

답글 달기
comment-user-thumbnail
2023년 6월 11일

맞아요.. 코드가 너무 길어지면 나중에 리팩토링할 때 두 배 세 배로 힘들어지는 것 같아요 😂 고생하셨슴다!!

답글 달기
comment-user-thumbnail
2023년 6월 11일

처음에 잘 분리하는게 너무 어려워요.. 그래도 중간중간 리팩토링하는게 훨 낫더라구여 나중되면 너무 복잡해져서 ㅠㅠ

답글 달기
comment-user-thumbnail
2023년 6월 11일

기능을 먼저 정리하고 그 안에서 UI로직, 비즈니스로직 구분하여 작업 하셨군요! 멋진 리팩토링 이에요!

답글 달기