React-Hook-Form과 Zod를 사용한 Input 컴포넌트 만들기 (1)

김아현·2023년 8월 9일
0

프로젝트

목록 보기
1/4

👍 쉽지않았던 Input 공통 컴포넌트 개발 회고

Input 공통 컴포넌트 개발을 맡다

가장 먼저, 이번 먹팟 프로젝트엔 프론트엔드로 3명이 참여했기 때문에 각자 어디까지 담당할 지를 나누어야 했다.

그래서 회의를 통해 사용할 기술 스택을 선정한 이후엔 다른 프론트엔드 파트 팀원들이 셋업을 준비해주었고 나는 글로벌 스타일 파일을 만들었다.

어느 정도 프로젝트 셋업이 끝난 후에 공통 인풋 개발을 위해 역할을 분배했다.

여기서 어려웠던 점은 어디까지가 공통 컴포넌트의 영역인지 구분하는 것이었는데, 그래서 공통 컴포넌트인 Input은 가장 먼저 Figma 디자인 시스템 파일에 주어진 status별 Input 디스플레이를 구현하는 것을 목표로 했다.

Input의 요구 사항은 다음과 같았다.

  • Title이 존재할 것
  • Title의 HelperText를 입력할 수 있을 것
  • placeholder를 지정할 수 있을 것
  • submit에서 validation을 통과하지 못하면 border color를 error/red500으로 변화시킬 것
  • 2가지 사이즈에 대응할 것
  • 505px width의 인풋은 타이틀이 왼쪽으로 정렬된 형태. label이 아니다

디자인 시안을 보고 가장 먼저 하나의 인풋 컴포넌트에서 모든 사이즈의 인풋에 대응하려 했다.

그 목표를 달성하기 위해 3가지 사이즈로 대응하는 방법을 적용했다. 그런데 개발 도중에 vanilla-extract에 미숙한 탓에 난관에 부딪혔다.

  • email 입력 폼의 small 사이즈 적용 시, children flex를 정리하기 위한 추가 css 코드가 필요하다
  • medium 사이즈와 large 사이즈 인풋의 label typhograpy와 div flex 구조를 각 타입에 알맞게 변경시키려면 조건부로 css를 변경할 코드가 필요하다.

이 두 문제는, vanilla-extract의 recipe variants를 사용해 해결할 수 있었다. variants를 설정하면, 컴포넌트의 Props로 size, color등을 넘겨주어 조건부 css를 사용할 수 있다.

하지만 이후에 Input 컴포넌트가 Calendar나 Map등 다른 컴포넌트들과 복합적으로 쓰이게 되면서 size를 결정 짓는 width는 상속으로 처리했다.

대신, Input이 사용되는 case별로 type variants를 나누어 아래처럼 코드를 작성했다.

export const inputBase = recipe({
  base: { 
    minWidth: '100%',
    height: '56px',
    fontSize: fontSize.md,
    padding: space['lg'],
    paddingRight: space['4xl'],
    backgroundColor: color.grey50,
    color: color.hint,
    borderRadius: borderRadius.md,
    border: `1px solid ${color.grey100}`,
    selectors: {
      '&::-webkit-search-cancel-button': {
        display: 'none',
      },
      '&:not(:focus)': {
        color: color.primary,
      },
      '&:not(:disabled):focus': {
        color: color.primary,
        border: `1px solid ${color.primary500}`,
      },
    },
  },
  variants: {
    type: {
      textArea: {
        alignItems: 'flex-start',
        padding: '16px',
        gap: '8px',
        height: '299px',
      },
      title: {
        fontSize: fontSize.xl,
        fontWeight: fontWeight.semibold,
      },
      search: {
        paddingLeft: space['5xl'],
      },
      password: {},
    },
  },
});

✅ 이외의 문제들

  • inner clear button을 어떻게 만들까?
    - watch API, setValue API를 활용해 각각의 인풋의 값에 대해 clear function이 작동하도록 해야한다.
    → ⚠️ 이 경우, onBlur에 clearButton이 보이지 않게 처리하려면 결국 useState로 Input의 state를 관리해야 하기 때문에 리렌더링이 일어나게 된다.
    → 🆗 따라서 디자이너분과 협의하에 onBlur에 clearButton이 사라지지 않고 그대로 있도록 했다.
  • error status field가 존재할 경우, 버튼 하단에 error label 추가하긴 했다.
    → ⚠️ Object.keys(errors).length 조건으로 렌더링하는게 맞나? errors에 좀 더 똑똑하게 접근하고파 ..
    → 🆗 위 조건을 없애고useFormContext를 사용해서 onSubmit시 필드 에러를 체크해 사용자가 변화를 인지할 수 있게끔 만들었다.
const InputErrorMessage = ({ name, showError = true }: errorProps) => {
  const { formState } = useFormContext();
  const errorMessage = showError && (formState.errors[name]?.message as string);

  return (
      <Typography color="red500" variant="label5" as="p">
        {errorMessage}
      </Typography>
  );
};

며칠 동안 컴포넌트를 작성하며 이런 저런 문제들을 발견하고, 초기 설계 접근 방향이 잘못되었다 생각해 다른 방식의 인풋 컴포넌트를 만들 방법을 구상했다.

그 중, 눈에 들어온 블로그 아티클.

드롭 다운은 팀원이 직접 구현한 공통 컴포넌트를 쓰겠지만 캘린더는 외부 라이브러리 사용이 예정되어있으므로 controller를 이용한 type별 인풋 컴포넌트 분리가 적절할거라 생각했다.

그래서 위 아티클을 참고해, 한 일주일~이주일 정도는 컴포넌트를 수정했다.

결국 나중 가선, 공통 ControllerInput 컴포넌트가 아니라 각각의 캘린더, 드롭다운을 Controller에 말아서 사용하는 페이지 단일 컴포넌트로 쓰긴 했지만 위에서 삽질하며 많은 도움을 얻을 수 있었다.

다음 시리즈에 이어서, 어떻게 최종까지 Input 컴포넌트를 개선시켜나갔는지 계속 적어보려 한다.


Controller를 도입한 인풋 공통 컴포넌트 개발 참고 자료

공부 | React Hook Form 정리

재렌더링 비용이 적은 폼 컴포넌트 만들기 | Blog by pumpkiinbell

react-hook-form과 MUI를 사용한 재사용성 있는 Input 공통 컴포넌트 만들기(TypeScript)


🛠 기술 스택
FrameworkNextjs React
Stylingvanilla-extract
State Managementtanstack/React Query Zustand
Testingvitest @testing-library
+react-hook-form Prettier typescript yarn berry
profile
멘티를 넘어 멘토가 되는 그날까지 파이팅

0개의 댓글