<aside> ➡️

</aside>

공통 컴포넌트에 대한 고민

<aside> ❓

공통 컴포넌트를 만들 때마다 항상 고민하는 부분들이 있습니다.

어디까지 생각하고 props를 전달해야 하는지, 나중에 디자이너에 의해 스타일이 변경되진 않을지, boolean 혹은 값의 유무에 따라 보여지는 뷰를 항상 조건부 렌더링으로 표현해야 하는지 등등…

항상 공통 컴포넌트를 만들어 놓고, 추후에 추가 기능이 생기거나 디자인이 변경되거나 코드적으로 수정사항이 발생하면 새로운 props를 추가하고, 코드 리팩토링 하고, 공통 컴포넌트를 리팩토링 하다보니 이 컴포넌트를 사용하는 모든 곳에서 수정이 필요할 때도 있고,,,

물론 제가 아직 공통 컴포넌트를 효율적으로 구현하지 못하는 실력의 한계가 있긴 하겠지만 그럼에도 불구하고 지금까지 만들었던 공통 컴포넌트는 재사용성이 떨어지고 협업에서 사람들과 쓰기에 올바른 패턴은 아니었다고 생각합니다.

</aside>

import { useState } from 'react';

import { IcClipboardCopy } from '@svg';
import useToast from 'src/hooks/useToast';

import { accountNumberStyle, buttonWrapperStyle, iconStyle } from './ClipboardCopyButton.style';
import Toast from '../../Toast/Toast';

const ClipboardCopyButton = () => {
  const { showToast, isToastVisible } = useToast();
  const [toastMessage, setToastMessage] = useState('');
  const accountNumber = '12345678';
  const handleCopyClick = async () => { ~~~ };
  return (
    <button css={buttonWrapperStyle} onClick={handleCopyClick}>
      <span css={accountNumberStyle}>{accountNumber}</span>
      <span css={iconStyle}>
        <IcClipboardCopy />
        <Toast isVisible={isToastVisible} toastBottom={3}>
          {toastMessage}
        </Toast>
      </span>
    </button>
  );
};

export default ClipboardCopyButton;

이를 해결할 방법 - Compound Component Pattern

위와 같은 고민을 해결하기 위한 방법으로 Compound Component Pattern이라는 방식이 있습니다. - (이하 CCP라고 칭함)

<aside> 📍

이 패턴의 주요 특징

  1. 상위 컴포넌트가 여러 하위 컴포넌트를 포함
  2. 하위 컴포넌트들이 상위 컴포넌트의 상태와 메서드를 공유하여 통합된 UI 구성 가능
  3. 이를 통해 사용자에게 단일 API를 제공하면서도 유연한 구성 가능 </aside>

바로 코드로 알아보자!

import React from "react";
import styles from "./BottomSheetItem.module.css";

const BottomSheetItem = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.cardWrapper}>{children}</div>;
};

const ItemTitle = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.title}>{children}</div>;
};

const ItemCategory = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.category}>{children}</div>;
};

const ItemDescription = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.itemDescription}>{children}</div>;
};
const ItemRating = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.itemRating}>{children}</div>;
};

const ItemReview = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.itemReview}>후기 {children}</div>;
};

const ItemDistance = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.itemDistance}>{children}</div>;
};

const ItemAddress = ({ children }: { children: React.ReactNode }) => {
  return <div className={styles.itemAddress}>{children}</div>;
};

const Image = ({ src, alt }: { src: string; alt: string }) => {
  return <img src={src} alt={alt} className={styles.image} />;
};

const Badge = () => {
  return <div>🍎</div>;
};

BottomSheetItem.ItemTitle = ItemTitle;
BottomSheetItem.ItemCategory = ItemCategory;
BottomSheetItem.ItemDescription = ItemDescription;
BottomSheetItem.ItemRating = ItemRating;
BottomSheetItem.ItemReview = ItemReview;
BottomSheetItem.ItemDistance = ItemDistance;
BottomSheetItem.ItemAddress = ItemAddress;
BottomSheetItem.Image = Image;
BottomSheetItem.Badge = Badge;

export { BottomSheetItem };
import { BottomSheetItem } from "../../../../components/common/BottomSheetItem/BottomSheetItem";
import image1 from "../../../../assets/images/액정 키스ㅋㅋ.jpeg";
import styles from "./Home.module.css";

const Home = () => {
  return (
    <>
      <BottomSheetItem>
        <section className={styles.section}>
          <div className={styles.sectionHeader}>
            <BottomSheetItem.ItemTitle>카페 나나빈</BottomSheetItem.ItemTitle>
            <BottomSheetItem.ItemCategory>카페</BottomSheetItem.ItemCategory>
            <BottomSheetItem.Badge />
          </div>
          <BottomSheetItem.ItemDescription>
            그린바나나가루를 사용하여 만든 디저트카페 나나빈입니다! 시그니처
            음료까지 맛보러오세요^^
          </BottomSheetItem.ItemDescription>
          <div className={styles.ratingAndReview}>
            <BottomSheetItem.ItemRating>⭐️4.5</BottomSheetItem.ItemRating>
            <BottomSheetItem.ItemReview>19</BottomSheetItem.ItemReview>
          </div>
          <div className={styles.distanceAndAddress}>
            <BottomSheetItem.ItemDistance>420m</BottomSheetItem.ItemDistance>
            <BottomSheetItem.ItemAddress>
              영등포동7가
            </BottomSheetItem.ItemAddress>
          </div>
        </section>
        <BottomSheetItem.Image src={image1} alt="Image1" />
      </BottomSheetItem>
    </>
  );
};

export default Home;

image.png

CCP가 필요한 이유