본문 바로가기
Project/모익

Atomic 패턴과 react-hook-form으로 재사용성을 고려한 로그인&회원가입 컴포넌트 만들기

by 이의찬 2023. 11. 30.

1. Why?

로그인, 회원가입 등 회원관리와 프론트엔드에서의 보안, 토큰 처리 등을 해보고 싶었었다. 그런데 마침 프로젝트 파트 분배 과정에서 하고 싶은거 없냐길래 냉큼 회원관리 부분을 가져왔다.

 

유효성 검사와 form의 데이터 처리를 쉽게 할 수 있고, 최소한의 리렌더링만을 일으켜 성능면에서도 우수한 react-hook-form 라이브러리를 도입하기로 결정했고, 이와 함께 atomic 패턴도 사용해서 공통적으로 사용할 수 있는 form 컴포넌트를 만들고, 이를 이용해서 재사용성이 뛰어난 회원 관리 기능을 구현하는 것을 목표로 삼았다. 


2. atomic하게 atom과 molcule부터 만들어보기

그런데 막상 찾아보니 TypeScript로 react-hook-form + Atomic 패턴을 사용한 사람이 없었고 죄다 next-auth 라이브러리를 사용하고 있었다. 결국 react hook form 공식문서와 다른 사람이 작성한 코드를 참고해서 직접 구현했다.

 

전체 코드는 https://github.com/Fintech-Moic/Moic 이 링크에서 볼 수 있다.

 

우선 가장 작은 atom인 InputBox컴포넌트는 다음과 같다.

import { forwardRef, ForwardedRef } from 'react';
import { UseFormRegister, FieldValues } from 'react-hook-form';

type InputBoxProps = {
  register: UseFormRegister<FieldValues>;
  width: string;
  height: string;
  id: string;
  name: string;
  type: string;
  placeholder: string;
  pattern?: string;
};

/** InputBox Component
 * @param {ForwardedRef<HTMLInputElement>} ref
 * @param {String} width
 * @param {String} height
 * @param {String} id InputBox Id
 * @param {String} name InputBox 이름
 * @param {String} type InputBox type
 * @param {String} placeholder InputBox placeholder
 * @returns {JSX.Element} InputBox Component 반환
 */

const InputBox = forwardRef(
  (
    {
      width,
      height,
      register,
      id,
      name,
      type,
      placeholder,
      pattern,
    }: InputBoxProps,
    ref: ForwardedRef<HTMLInputElement>
  ) => {
    return (
      <input
        className={`${width} ${height} border-solid border-2 rounded-[10px] px-2`}
        {...register(name)}
        id={id}
        ref={ref}
        name={name}
        type={type}
        pattern={pattern}
        placeholder={placeholder}
      />
    );
  }
);

export default InputBox;

이 부분에서 가장 오랜 시간을 잡아먹었는데, 우선 forwardRef의 개념을 이해하는데 제법 오랜 시간이 걸렸다. refforwardRef에 대해서는 이미 글을 작성했으니 개념 설명은 생략하겠다.

 

다음으로 molcule인 InputForm이다.

// 생략

export default function InputForm({
  register,
  validation,
  width,
  height,
  id,
  name,
  type,
  notice,
  isError,
  placeholder,
}: InputFormProps) {
  return (
    <div className={`${width} flex flex-col`}>
      <InputBox
        {...register(name, { pattern: validation })}
        id={id}
        name={name}
        type={type}
        placeholder={placeholder}
        register={register}
        width={width}
        height={height}
      />
      {notice && (
        <InputNoticeMessage isError={isError}>{notice}</InputNoticeMessage>
      )}
    </div>
  );
}

 

위 두 가지의 컴포넌트들을 이용해서 로그인/회원가입과 아이디/비밀번호 찾기 등 회원 관리 기능을 구현했다.

 

로직을 간략하게 설명하자면 기존에 설명했던 forwardRef를 이용해 InputBox컴포넌트 내부에 Input이 변경되면 react-form-hook을 사용하여 이를 추적, page 부분에서 api 함수를 호출하도록 했다.

 

아래의 로그인 페이지를 참조하면 이해하는데 도움이 될 것이다.

export default function Page() {
  useAlreadySignInChecker();
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();
  const router = useRouter();
  const goingTo = (target: string) => {
    router.push(target);
  };
  const onSubmit = handleSubmit(async (data) => {
    const result = await signInApi(data);
    if (result) goingTo('/home');
  });
 // 생략

  return (
    <div className="h-full flex flex-col">
      <div className="h-1/4 flex items-center">
        <TitleSentence title="로그인" sentence="더 많은 서비스를 누려보세요" />
      </div>
      <div className="h-3/4 flex flex-col items-center justify-between">
        <SignInForm register={register} errors={errors} onSubmit={onSubmit} />
        <TextButton onClick={() => goingTo('/auth/find')}>
          아이디 혹은 비밀번호를 잊으셨나요?
        </TextButton>
 		// 생략
      </div>
    </div>
  );
}

전체 결과물은 아래 github에서 볼 수 있다. 

 

GitHub - Fintech-Moic/Moic: 카드 혜택과 기프티콘 간편 제공 웹앱 프로젝트

카드 혜택과 기프티콘 간편 제공 웹앱 프로젝트. Contribute to Fintech-Moic/Moic development by creating an account on GitHub.

github.com


3. 고민

이렇게 재사용성 높은 회원관리 컴포넌트들을 구현할 수 있었다. 하지만 Trade-off 역시 존재했는데, 크게 두 가지가 있었다. 우선 첫번째로 page내부의 form들은 위의 컴포넌트 + 다른 atoms, molecules를 합쳐서 만든 organism을 이용했는데, 이 organisms들의 재사용성이 매우 떨어지다 못해 결합되는 수준이였다.

 

물론 아무리 atomic 패턴을 써서 재사용성을 높이려고 해도 organisms 부분은 재사용성이 떨어질 수 밖에 없는 부분도 있었고, 이미 UI 구현이 완료된 상태에서 수정하는 것은 무리가 있었기에 다음 프로젝트부터 진행시에는 UI 기획을 할 때 부터 다시 고민해봐야할 듯 하다.

 

두 번째 고민은 지금까지 코드를 봤다면 알겠지만 props 뚱땡이가 되어버렸다!

 

거기에 JsDoc로 props 주석까지 작성하는 것을 룰로 정했기 때문에 두 배로 뚱땡이,, 

이 props 뚱땡이를 어떻게 해야할까에 대해서 같이 프로젝트를 진행중인 친구와 많은 대화를 나누면서 여러 문서들도 찾아봤는데, 결론은

그냥 Atomic 패턴의 특성이니 받아들이자.

 

였다. 이유는 다음과 같은데

1. 가능하면 Page에서 모든 비즈니스 로직을 처리하고자 했고, 자연히 관리해야 할 상태가 많아질 수 밖에 없었다.

2. 애초에 해결책이라고 해봐야 Jotai에 props를 넣어서 전역으로 전달하는 것 정도인데,

3. 그런데 굳이 전역으로 상태를 관리할 필요도 없는 props를 Jotai에 넣는것은 싸피에서 지갑을 주웠는데 강남경찰서에 맡기는 바보같은 짓(근데 이 짓을 저번 프로젝트에서 했었음)이다. 

 

결국 뚱땡이 컴포넌트가 되는게 현재로써는 최선책이라는 결론을 내리고 이대로 유지하는 것으로 합의를 보았다.

 

하지만 이것과는 별개로 이 프로젝트에서도 Atomic 패턴을 잘 사용했다고는 하기에는 비즈니스 로직이 확실하게 분리되었는지도 의문이였고, 구현에 급급한 나머지 컴포넌트를 제대로 나누지 않은 상태로 넘어간 적도 잦았다.

 

어쩌다 보니 SSAFY 프로젝트들이 죄다 Atomic 패턴과의 사투가 되어가는 것 같은데, 다음 프로젝트에서는 좀 재사용성이 뛰어나고 유연한 컴포넌트를 만들어보겠다.


4. props 개선방안(추가)

SSAFY를 이수한 후 Wanted에서 진행하는 프리온보딩 챌린지에 참여중인데, 디스코드에서 우연히 props 관리에 대한 이야기가 나왔다.

내가 이해한것이 맞다면

  1. 특정 함수에서 사용될 경우 함수를 callback으로 빼준다.
  2. props drilling일 경우 children을 사용해 하위 컴포넌트를 외부로 노출
  3. 공용 컴포넌트를 여러군데에서 가져다 쓰면서 비즈니스 로직을 위한 props가 늘어났을 경우, 공용 컴포넌트를 감싼 도메인 컴포넌트를 만든다.
  4. props들을 적절하게 객체로 묶어준다.

네 가지 방법을 활용해서 props를 관리하는 것이다. 추후에 진행하는 프로젝트들에서는 위 방식들을 적절하게 사용해서 props를 개선해봐야겠다.


5. 래퍼런스

 

react-hook-form을 선택한 이유와 적용 과정

IT 채용 플랫폼 랠릿의 거대한 Form을 react-hook-form으로 마이그레이션한 이유와 과정을 공유합니다.

tech.inflab.com

 

react-hook-form을 사용하여 공통 폼 컴포넌트 만들기

리액트 프로젝트에서 react-hook-form을 사용하여 공통 컴포넌트 만들기

modac-bull.github.io

 

React Hook Form - performant, flexible and extensible form library

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com

 

forwardRef – React

The library for web and native user interfaces

react-ko.dev