이럴 땐 합성 컴포넌트 사용해보시죠

지인혁·2024년 3월 27일
1

🎈 들어가며

공통 컴포넌트 작업을 하다가 하나의 컴포넌트에서 많은 역할과 유연하지 못한 컴포넌트를 보며 어떻게 해결할 수 있을지 많은 고민과 자료를 찾아 보았으며 합성 컴포넌트 패턴을 알게 되었습니다.

만약 하나의 컴포넌트에서 역할을 분리하거나, 유연하게 설계하여 재사용을 높이고 싶다면 컴포넌트 내부를 독립적인 조각으로 나누어 조합하는 합성 컴포넌트 패턴을 사용해보시기 바랍니다.


첫 구현

예를 들어 radio 컴포넌트를 구현한다면 정말 간단합니다. label과 input의 라디오, 그리고 감싸는 div 이렇게 하나의 컴포넌트화를 한다면 간단하게 구현할 수 있고 당장 사용하는데는 큰 문제가 없습니다.

const Radio = ({ id, labelValue, name, inputValue, wrapStyle, labelStyle, inputStyle }) => {

  return (
    <div className={wrapStyle}>
      <label
        htmlFor={id}
        className={labelStyle}
      >
        {labeValue}
      </label>
      <input
        type="radio"
        id={id}
        name={name}
        value={inputValue}
        className={inputStyle}
      />
    </div>
  );
};

<Radio
  id={}
  labelValue={}
  name={}
  ipnutValue={}
  wrapStyle={}
  labelStyle={}
/>

id 값을 받고 연결하며, 스타일의 유연성을 고려해서 각 태그의 스타일까지 props로 전달받고 있습니다. 이렇게 사용해도 어느정도 radio 컴포넌트 모양을 갖추게 되었습니다.

하지만 좀 더 깊게 생각해볼까요? 만약 radio 컴포넌트를 변경해야하는 상황을 생각해보며 문제점을 추려내봅시다.

문제점

단일 책임 원칙

현재 컴포넌트는 label, button의 2가지 책임을 처리를 하고 있습니다. 겨우 2개의 처리지만 2개에 필요한 props는 상당히 많다고 판단됩니다.

만약 추가적인 요구사항을 통해 더 많은 태그의 기능이 추가된다면 props는 끝도 보이지 않을 겁니다. 과연 요구사항이 얼마나 추가되거나 변경될지는 아무도 모릅니다.

이렇게 될 경우 이 Radio 컴포넌트는 많은 역할을 하는 거대한 컴포넌트가 되어 가독성과 재사용성에 영향을 줄 수 있습니다.

부족한 유연성

현재 설계된 방법은 label이 button 태그보다 앞에 있습니다. 현재 프로젝트에서 UI의 형태가 만족한다면 당장은 문제가 없습니다.

하지만 UI가 변경되거나 다양한 형태의 UI가 프로젝트에서 요구된다면 이 또한 재사용성에 실패한 유연성 없는 컴포넌트가 될 것 입니다.

합성 컴포넌트로 변경

Radio 컴포넌트를 여러가지 컴포넌트 조각으로 분리한 뒤 사용하는 쪽에서 자유롭게 사용하는 컴포넌트 패턴입니다.

음 자세히 보면 이렇게 분리할 수 있을 것 같습니다.

  • radio를 감싸는 최상위 wrap 컴포넌트
  • label 컴포넌트
  • button 컴포넌트

wrap을 메인 컴포넌트로 label, button를 서브 컴포넌트로 만들어 볼 수 있겠습니다.

radio 메인 컴포넌트

최상위 wrap을 담당하는 컴포넌트는 본인의 자식 요소로 어떤 데이터가 렌더링 될 지 모릅니다. 사용하는 곳에서 children으로 주입을 하고 radio 컴포넌트는 자식 요소를 렌더링을 하는 역할과 전체적인 스타일을 담당합니다.

const Radio = ({ children, ...rest }: IRadioProps) => {
  const style = twMerge(`flex gap-2 ${rest.className}`);

  return <div className={style}>{children}</div>;
};

label 서브 컴포넌트

label 서브 컴포넌트는 button 태그에 연결되는 id 값과 어떤 콘텐츠가 자식으로 들어오는지에 관심이 있습니다.

또한 rest props도 각각 담당하는 컴포넌트에서 독립적으로 받고 있어 가독성도 매우 좋습니다.

const RadioButtonLabel = ({ id, children, ...rest }: IRadioButtonLabelProps) => {
  return (
    <label
      htmlFor={id}
      {...rest}
    >
      {children}
    </label>
  );
};

button 서브 컴포넌트

button 서브 컴포넌트 또한 자기 자신에 필요한 props만 주입을 받고 있어 가독성에서 더 좋고 독립적입니다.

const RadioButton = ({ id, name, value, ...rest }: IRadioButtonProps) => {
  return (
    <input
      type="radio"
      id={id}
      name={name}
      value={value}
      {...rest}
    />
  );
};

메인과 서브 컴포넌트 연결하기

이거는 필수가 아닌 선택인 것 같습니다. 하지만 가독성에 도움을 줄 수 있는 방법이므로 서브 컴포넌트가 메인의 요소 컴포넌트라고 명시해주는 것이 좋다고 생각합니다.

radio 컴포넌트에서 2개의 서브 컴포넌트를 import 하여 확실하게 관계를 표시해줍니다.

Radio.Label = RadioButtonLabel;
Radio.Button = RadioButton;

사용하기

이전 첫 구현과 합성 컴포넌트로 구현했을 때 사용하는 쪽을 비교해 봅시다.

// 첫 구현
<Radio
  id={}
  labelValue={}
  name={}
  ipnutValue={}
  wrapStyle={}
  labelStyle={}
/>
// 합성 컴포넌트 구현
<Radio>
  <Radio.Button
    id={}
   	name={}
    value={}
    onChange={}
  />
 <Radio.Label id={}>{children}</Radio.Label>
</Radio>

첫 구현 컴포넌트는 하나의 컴포넌트에 모두 밀집되어 있는 느낌인 반면 합성 컴포넌트 구현은 정말로 필요한 컴포넌트에 props를 주입하여 각각 독립되어 있는 느낌입니다.

또한 합성 컴포넌트를 사용하면서 고정되어 있던 label과 button의 위치를 사용하는 쪽에서는 자유자제로 배치할 수 있는 최대의 장점을 가지게 됩니다.

아래와 같이 label과 button의 위치를 변경해도 컴포넌트를 직접 수정하는 일이 발생하지 않습니다.

<Radio>
  <Radio.Label id={}>{children}</Radio.Label>
  <Radio.Button
    id={}
   	name={}
    value={}
    onChange={}
  />
</Radio>

🎊 마치며

컴포넌트 작업을 하실 때 다양한 패턴으로 좋은 컴포넌트 방향을 잡으실 수 있습니다. 만약 컴포넌트 내부가 복잡하여 분리가 필요하고 유연한 UI 설계를 고려하신다면 합성 컴포넌트를 적용해보는 방법을 추천드립니다!

profile
대구 사나이

0개의 댓글