InputBox 컴포넌트 만들기

jh·2024년 4월 1일
post-thumbnail

프로젝트를 진행하면서, 이런 모양의 컴포넌트가 비슷하게 쓰이는 것 같은데 쓰는 사람들마다 다 다른 방식으로 사용을 하고 있다는 걸 알게 되었다.

물론 비슷하다고 다 공통으로 만들 필요는 없다는 건 알지만

  • 엄청난 로직이 들어가는 게 아닌데 따로 쓰는 것 자체가 조금 비효율적이다
  • 각자마다 이를 그리기 위한 코드가 너무 다르다 보니 이게 비슷한 컴포넌트겠구나 하는 게 잘 안그려진다는 게 가장 큰 문제점이었다.

input 예시

const ProjectInputBox = ({
  name,
  label,
  footer,
  children,
  width,
}: ProjectInputBoxProps) => {
  const {
    register,
    formState: { errors },
  } = useFormContext<ProjectFormValues>()

  return (
      <label htmlFor={name}>
        <Text
          fontSize="md"
          as="b">
          {label}
        </Text>
      </label>
      <ErrorMessage
        name={name}
        errors={errors}
        render={({ message }) => <ErrorText message={message} />}
      />
      {isValidElement(children) &&
        cloneElement(children as ReactElement<InputElementProps>, {
          id: name,
          ...register(name, projectInputRegister[name]),
        })}

      {footer && (
        <Text
          fontSize="sm"
          color="grey">
          {footer}
        </Text>
      )}
  )
}
  • react-hook-form의 로직과 UI 관련 로직이 혼재되게 되면 처음 봤을 때 의미를 이해하기가 좀 어렵다
  • UI가 각자의 책임만을 담당하면서 분리되어 있었으면 좋았을 것 같다는 생각이 들었다

컴포넌트 분리해보기

컴포넌트를 분리해서 보면 3개로 나눌 수 있다

  • 가장 위에 위치하는 label 태그
  • 중간에 위치하는 input
  • 하단에 위치하는 text

그래서 합성 컴포넌트 방식으로 컴포넌트를 3개의 컴포넌트로 나눈 다음, 각 컴포넌트가 독립적인 역할을 하도록 나누어 본다면 좋을 것 같다는 생각이 들었다

  • 스타일 관련한 부분은 나중에 고려
<InputBox>
	<InputBox.Header/> - 최상단 담당
    <InputBox.Input/> - 중간 담당
</InputBox> 

그런데 뭔가 이것만으로는 좀 부족하다는 생각이 들었다.

  • 단순히 3가지의 컴포넌트로 나누어서 만들게 되면 책임 분리 면에서야 좋겠지만, 코드 길이가 기존보다 더 늘어난다는 단점이 존재한다
  • 주석 같은걸로 명시해주면 코드를 바꾸는 노력보다 더 쉽게 해결할 수 있지 않을까? 라는 생각이 좀 들었다.

그래서 조금 더 기능을 추가해보기로 했다

각 컴포넌트가 공유해야 하는 값이 존재하는가?

일단 제일 먼저 생각난 것은

  • label 태그의 htmlForinputid 값이 동일해야 한다
    - 무조건 동일해야 하는건 아니지만, 값이 동일할 경우 얻을 수 있는 장점이 많이 존재한다

  • 물론 label과 input에 따로 props로 전달하는 것도 그리 어렵지 않은 방법이지만, 하나의 값으로 관리하게 된다면 human error를 방지할 수 있지 않을까? 라는 생각이 들었다

InputBox.Header

  • header는 semantic tag로 label 태그를 사용하는 게 좋다
  • 그 안에 들어갈 text 는 외부에서 자유롭게 커스텀이 가능해야 한다

단순히 text만 넣을 수 있게끔 string 타입으로 제한해도 편할 것 같지만, 확장성을 생각하여 children 의 타입을 ReactNode로 했다

const InputHeader = ({ children }: InputHeaderProps) => {
  const { id } = useInputBoxContext()
  return (
    
      <label htmlFor={id}>{children}</label>
    
  )
}

InputBox.Content

이 부분에서 고민한 점이

  • 해당 부분을 Input 태그로 고정
  • children을 통해 자유롭게 설정

일단 컴포넌트명이 Input인 만큼, 뭔가 Input에 대해서만 다룰 수 있으면 좋을 것 같다는 생각이 들었다.

  • 그럼 props를 통해 textarea 정도까지만 다룰까?
  • input / textarea를 하게 되면 props 받는 게 좀 까다로워 질 것 같다는 생각이 들었다. 물론 attriubte를 상속받는 방법도 있지만, 그럴거면 아예 children을 사용해서 받는 편이 더 낫지 않을까? 라는 생각이 들었다.
const InputContent = forwardRef<HTMLInputElement, InputContentProps>(
  ({ children}, ref) => {
    const { id } = useInputBoxContext()
    return (
      <div ref={ref}>
        {cloneElement(children, {
          id,
        })}
      </div>
    )
  },
)
  • 어떤 children이 들어올지는 모르지만, cloneElement 를 통해 props에 id를 추가함으로써 label과 연결
  • ref forwarding을 통해 react-hook-form 사용하게 될 시에 생길 수 있는 ref 관련 에러를 신경쓰지 않게 함

문제점 - id 라는 props가 오버라이딩 되지 않을까? 했지만, 결론은
useInputBoxContext() 의 id값으로 적용되어서 문제 없다

cloneElement 함수에 전달된 id 값이 최종적으로 children에 설정됩니다.

Footer 와 사용하면서 생긴 수정사항은 다음으로..

0개의 댓글