합성 컴포넌트로 재사용 가능한 컴포넌트 만들기

D uuu·2024년 1월 2일

React

목록 보기
7/10

변경에 유연한 컴포넌트

개발할때 마다 컴포넌트를 어떻게 나눌 것인가는 큰 고민거리이다. 단일 책임 원칙에 따라 컴포넌트를 나누려고 노력하지만, 오히려 너무 세분화 할수록 복잡도가 더 올라가는 경우도 있었다. 어느 수준에서 컴포넌트를 적절하게 나누는 것이 적합한걸까? 매번 고민하지만 해결책을 찾지 못했었다. 그러다 좋은 글을 읽게 되었다.

블로그 https://blog.jbee.io/web/%EB%B3%80%EA%B2%BD%EC%97%90+%EC%9C%A0%EC%97%B0%ED%95%9C+%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8

내가 고민하던 부분이 글에 담겨 있어서 읽으면서 공감을 많이 했다. 그리고 아래 부분에 대해 새로운 영감(insight) 을 얻었다.

1. 결합되어 있는 의존성

컴포넌트를 생성할때 보통 만들고자 하는 화면의 이름을 가져다 쓴다. 예를들어 마사지 아이템을 보여주는 화면이라면 <MassageItem> , 마사지 아이템의 디테일을 보여주는 화면이라면 <MassageItemDetail> 이렇게 컴포넌트명을 짓다보니 이 컴포넌트는 해당 페이지에만 사용할 수 있다고 단정짓게 되어 비슷한 구조임에도 불구하고 중복성을 인식하지 못했다.

블로그에서는 이러한 특정 도메인과 강하게 결합돼있는 점이 문제라고 지적했다.
<MassageItem><MassageItemDetail> 의 화면은 거의 유사하며 비슷한 코드임에도 불구하고 도메인에 갇혀 문제점을 파악하지 못했다.

2. 일어나지 않을 일..?

현재는 마사지 아이템과 마사지의 디테일한 부분만 고르면 되는 구조이다. 그런데 만약에 아래와 같은 요구사항이 생기면 어떻게 될까?

  • 마사지를 받고 싶은 선생님을 고를 수 있도록 해주세요
  • 마사지 옵션이 하나 더 늘어났어요, 옵션을 선택하도록 해주세요.

서비스를 운영하다 보면 충분히 있을 수 있는 일이다. 그런데 나는 이런 상황을 전혀 고려하지 않고 현재 화면를 그리는데에만 몰두했다. 만약 실제 서비스라면 나는 <MassageTutor>, <MassageOption> 컴포넌트를 또 만들거나 아니면 그제서야 문제점을 인식하고 많은 부분을 고치려고 할지 모른다.

그래서..?

블로그 글을 읽고 내 컴포넌트를 다시 보니 몇가지 문제점을 고쳐야겠다는 생각이 들었고, 결합되어 있는 의존성을 제거하고 앞으로 일어날 수 있는 상황들을 고려하면서 리팩토링 하기로 했다.



합성 컴포넌트란 ?

합성 컴포넌트 패턴은 하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤, 분리된 각 컴포넌트를 사용하는 쪽에서 조합해 사용하는 컴포넌트 패턴을 의미한다.

간단한 예시로 html의 select를 볼 수 있는데, select는 <select><option> 태그의 조합으로 이루어져있다. <select><option> 은 각각 독립적으로는 큰 의미가 없지만 사용하는 곳에서 이를 조합해 사용함으로써 화면에 의미 있는 요소가 된다.

이처럼 사용하는 곳에서 컴포넌트의 조합을 활용할 수 있다면 높은 재사용성을 만족하면서 다양한 상황에 사용할 수 있다는 생각이 들어 기존에 만들어놓은 컴포넌트를 리팩토링 해보기로 했다.


합성 컴포넌트로 재사용 가능한 컴포넌트 만들기

아래 사진처럼 구현하는데 현재는 하나의 컴포넌트로 이루어져 있다.

<기존코드>

<ItemBoxStyle>
  <ImgBoxStyle>
      <img
          src={massage.image}
          alt={massage.displayItem}
          width="100%"
          height="100%"
      />
  </ImgBoxStyle>
  <ContentBoxStyle>
    <TopBoxStyle>
      <span>{massage.displayItem}</span>
      <span>({detail.time}분)</span>
    </TopBoxStyle>
    <MiddleBoxStyle>{addComma(detail.price)}</MiddleBoxStyle>
      <ButtonStyle onClick={() => setAvailableTime(detail.time)}>
        예약하기
      </ButtonStyle>
  </ContentBoxStyle>
 </ItemBoxStyle>

그런데 이 컴포넌트를 아래와 같이 4개로 쪼개어 조합해서 사용하는 방법으로 바꾸어 볼 것 이다.

  • layout : 전체적인 레이아웃을 담당한다.
  • image : 이미지 부분을 담당한다.
  • content : 내용 부분을 담당한다.
  • button : 버튼 부분을 담당한다.


Card 컴포넌트 만들기(layout)

Card 컴포넌트는 레이아웃을 담당한다. image, content, button 등을 이 Card 컴포넌트로 감싸서 사용할 것이다.

type TProps = {
  children: React.ReactNode;
};

const Card = ({ children }: TProps) => {
  return <CardLayoutStyle>{children}</CardLayoutStyle>;
};

export default Card;


CardImage 컴포넌트 만들기(이미지)

CardImage 컴포넌트는 image 와 alt 를 props 로 받는다.

type TProps = {
  image: string;
  alt: string;
};

const CardImage = ({ image, alt }: TProps) => {
  return (
    <ImgBoxStyle>
      <img src={image} alt={alt} width="100%" height="100%" />
    </ImgBoxStyle>
  );
};

export default CardImage;


CardContent 컴포넌트 만들기(내용)

CardContent 컴포넌트는 안에 들어가는 내용과 style 이 조금씩 다르므로 props 를 여러개 받는다.

type TProps = {
  title: string;
  content: string | number;
  subTitle?: string | number;
  fontSize?: string;
  textAlign?: string;
};

const CardContent = ({
  title,
  content,
  subTitle,
  fontSize,
  textAlign,
}: TProps) => {
  return (
    <ContentBoxStyle $textAlign={textAlign} $fontSize={fontSize}>
      <TitleStyle>
        {title}
        {subTitle}
      </TitleStyle>
      <ContentStyle>{content}</ContentStyle>
    </ContentBoxStyle>
  );
};

export default CardContent;


컴포넌트 가져다 쓰기

버튼은 미리 만들어둔 CommonButton 을 가져다 썼다.
이렇게 기능별로 컴포넌트를 쪼개면 독립적으로는 큰 의미가 없지만 사용하는 곳에서 이를 조합해 사용함으로써 화면에 의미 있는 요소가 된다.

기존 컴포넌트👇

<ItemBoxStyle>
      <ImgBoxStyle>
        <img
          src={massage.image}
          alt={massage.displayItem}
          width="100%"
          height="100%"
        />
      </ImgBoxStyle>
      <ContentBoxStyle>
        <TopBoxStyle>
          <span>{massage.displayItem}</span>
          <span>({detail.time}분)</span>
        </TopBoxStyle>
        <MiddleBoxStyle>{addComma(detail.price)}</MiddleBoxStyle>
        <ButtonStyle onClick={() => setAvailableTime(detail.time)}>
          예약하기
        </ButtonStyle>
      </ContentBoxStyle>
    </ItemBoxStyle>

합성 컴포넌트로 조합해서 만든 컴포넌트👇

(... 생략)

    <>
      <CardImage image={massage.image} alt={massage.displayItem} />
      <CardContent title={massage.displayItem} content={massage.content} />
      <CommonButton
        type="round"
        onClickButton={() => selectMassageItem(massage.item)}
        $padding="0.7rem"
        $border={`2px solid ${palette.grey}`}
      >
        선택하기
      </CommonButton>
    </>
  • 이렇게 카드를 조합해서 사용하면 어떤 화면에서는 버튼이 필요없다면 버튼을 빼고 구현할 수 있고, 추가로 기능을 구현해야할때 카드를 조합해서 새로운 화면을 그릴 수 있어서 재사용이 높아진다.

  • 기존 컴포넌트와도 비교했을때 가독성도 훨씬 좋아졌다.


마무리

주어진 것만 처리하는 개발자가 아니라 생각하는 개발자가 되어야 한다고 무수히 들었었는데 이번 리팩토링을 통해 그 의미를 실감하게 되었다.
단순히 화면을 구성하는데에만 몰두하다 보니 여러 측면에서 놓치고 있던 부분이 많다는 것을 느꼈다😭

개발은 단순히 주어진 요구사항을 해결하는 것 이상의 의미가 있다. 이번 경험을 통해 유연한 컴포넌트 만드는 방법, 코드를 작성하는 방법에 대해서 생각해볼 수 있어서 좋았다!!!

profile
배우고 느낀 걸 기록하는 공간

0개의 댓글