본문 바로가기

React/실험실

[React] Compound Component 패턴

Compound Component? 

Compound Components는 그룹 컴포넌트의 동작과 상태를 포함하고 있지만 렌더링 제어를 

외부 사용자에게 제공하는 패턴이라고 할 수 있다. 

 

여기서 핵심은 상태와 동작을 포함하고 있다는 것이다. 

 

HTML의 select 태그와 option 태그를 생각하면 이해하기 쉬울 것이다. 

<select>
  <option value="volvo">Volvo</option>
  <option value="mercedes">Mercedes</option>
  <option value="audi">Audi</option>
</select>

select 태그는 option 태그와 함께 드롭다운 기능을 수행합니다. 

UI의 상태 관리를 select가 수행하고 option 은 select가 작동하는 방식에 대해서 구성이 됩니다.

 

React의 Compound Component는 Prop Drilling을 방지하는 데 도움이 되는 선언적 UI 구성 요소를 구성하는데 사용된다. 

 

이러한 Compound Component와 JSX로 구성된 컴포넌트의 차이는 뭘까? 

<CustomSelect
  options={[
    {value: '1', display: 'Option 1'},
    {value: '2', display: 'Option 2'},
  ]}
/>

가장 큰 부분으로는 스타일 커스터마이징이다. 

또한 select에서 이미지를 넘겨야 할 경우에는? 또는 특정 엘리먼트를 넘겨야 하는 경우에는 ? 정말 끔찍해진다. 

 

Compound Component의 장단점 

아주 멋진 React 패턴이라도 장점과 단점을 이해하고 사용하는 것이 좋다. 

 

장점 

관심사 분리 

    부모 컴포넌트에 모든 UI 상태 로직이 있고 이를 내부적으로 모든 자식 컴포넌트에게 전달한다면

    책임이 명확하게 구분된다. 

복잡성 감소 

    속성을 특정 컴포넌트로 전달하는 props Drilling과 달리 Compound Component 패턴은 속성을

    바로 자식 컴포넌트에 전달한다. 

 

단점 

Copound Component의 핵심 단점으로는 부모 컴포넌트의 자식 컴포넌트만 props에 접근할 수 있다. 

export default function FlyoutMenu() {
  return (
    <FlyOut>
      {/* This breaks */}
      <div>
        <FlyOut.Toggle />
        <FlyOut.List>
          <FlyOut.Item>Edit</FlyOut.Item>
          <FlyOut.Item>Delete</FlyOut.Item>
        </FlyOut.List>
      </div>
    </FlyOut>
  );
}

다음과 같이 Flyout 컴포넌트 아래에 바로 div를 사용할 경우 props를 전달할 수 없다. 

이렇게 보면 정말 치명적인 단점으로 과연 해당 패턴을 사용할 수 있을까? 에 대한 의문이 생길 것이다. 

 

하지만 당연히 해결 방법이 있기 때문에 사용한다! 

바로 Context를 사용해서 암시적으로 상태를 공유하는 것이다. 

 

Context API를 사용하면 React에서 컴포넌트를 빌드할 때 손쉽게 props를 전달할 수 있다. 

이것을 Compound Component와 함께 사용하면 많은 유연성을 제공한다. 

 

Compound Component는 언제 사용해야 할까? 

재사용 가능한 컴포넌트를 만드는 경우 

최소한의 결합으로 응집력이 높은 컴포넌트를 만드는 경우 

컴포넌트간의 로직을 공유하는 게 더 좋은 경우 

 

한번 만들어보기 

지금까지 Compound Component에 대해서 이것저것 이야기 했다 그럼 한번 직접 만들어보자! 

 

만들 컴포넌트는 아코디언 컴포넌트이다. 

왠 악기? 라고 생각할 수 있겠지만 ( 나도 그렇게 생각함 ) 아코디언 컴포넌트란  

출처 - https://ui.alopex.io/development/component/accordion

리스트를 접기 / 펼치기가 가능한 컴포넌트를 뜻한다. 

 

스타일 구성하기 

본격적으로 컴포넌트를 만들기 전에 미리 스타일을 세팅할 계획이다. 

스타일을 구성하기 위해서 styled-components를 사용할 계획이다.

import styled from "styled-components";

export const Container = styled.div`
  display: flex;
  border-bottom: 8px solid #222;
`;

export const Frame = styled.div`
  margin-bottom: 40px;
`;
export const Inner = styled.div`
  display: flex;
  padding: 70px 45px;
  flex-direction: column;
  max-width: 815px;
  margin: auto;
`;
export const Title = styled.h1`
  font-size: 40px;
  line-height: 1.1;
  margin-top: 0;
  margin-bottom: 8px;
  color: black;
  text-align: center;
`;
export const Item = styled.div`
  color: white;
  margin: auto;
  margin-bottom: 10px;
  max-width: 728px;
  width: 100%;
  &:first-of-type {
    margin-top: 3em;
  }
  &:last-of-type {
    margin-bottom: 0;
  }
`;
export const Header = styled.div`
  display: flex;
  flex-direction: space-between;
  cursor: pointer;
  margin-bottom: 1px;
  font-size: 26px;
  font-weight: normal;
  background: #303030;
  padding: 0.8em 1.2em 0.8em 1.2em;
  user-select: none;
  align-items: center;
  img {
    filter: brightness(0) invert(1);
    width: 24px;
    user-select: none;
    @media (max-width: 600px) {
      width: 16px;
    }
  }
`;
export const Body = styled.div`
  font-size: 26px;
  font-weight: normal;
  line-height: normal;
  background: #303030;
  white-space: pre-wrap;
  user-select: none;
  overflow: hidden;
  &.closed {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.25ms cubic-bezier(0.5, 0, 0.1, 1);
  }
  &.open {
    max-height: 0px;
    transition: max-height 0.25ms cubic-bezier(0.5, 0, 0.1, 1);
  }
  span {
    display: block;
    padding: 0.8em 2.2em 0.8em 1.2em;
  }
`;

다음과 같이 스타일을 설정했다. 

 

이제 본격적으로 컴포넌트를 만들어보자! 

import { useState, useContext, createContext } from "react";
import {
  Container,
  Inner,
  Item,
  Body,
  Frame,
  Title,
  Header,
} from "./accordion.style";

먼저 Compound Component를 만드는데 필요한 것을 먼저 불러왔다. 

 

const ToggleContext = createContext();

const Accordion = ({ children, ...restProps }) => {
  return (
    <Container {...restProps}>
      <Inner>{children}</Inner>
    </Container>
  );
};

export default Accordion;

createContext를 사용해서 Context 객체를 생성했다. 

추후 props를 전달할 때 Context를 통해 유연하기 전달할 수 있다. 

 

또한 자식 컴포넌트를 렌더링하기 위해서 children과 나중에 style 혹은 필요한 속성을 전달할 때 사용할 수 있게

restProps를 받아와서 사용한다. 

 

const AccordionTitle = ({ children, ...restProps }) => {
  return <Title {...restProps}>{children}</Title>;
};

const AccordionFrame = ({ children, ...restProps }) => {
  return <Frame {...restProps}>{children}</Frame>;
};

이어서 Title과 Frame을 만들어주었다. 

각각 특별하게 하는 것은 없고 정해진 스타일을 토대로 자식 컴포넌트를 렌더링해주는 역할이다. 

 

const AccordionItem = ({ children, ...restProps }) => {
  const [toggleShow, setToggleShow] = useState(true);

  return (
    <ToggleContext.Provider value={{ toggleShow, setToggleShow }}>
      <Item>{children}</Item>
    </ToggleContext.Provider>
  );
};

ToggleContext를 사용하는 부분이다. 

Item 컴포넌트에서 useState의 상태를 초기화 해주고 자식 컴포넌트에게 Context를 사용해서 상태를 전달해준다.

 

앞서 말한 것처럼 Context를 사용하므로 중간에 Accordion 중간에 div 등 예상치 못한 태그가 들어가더라도

props를 전달하는데 문제가 없다. 

그러므로 레이아웃을 구성하는데 훨씬 자유로워졌다. 

 

또한 props Drilling으로부터 자유롭다. 

const AccordionHeader = ({ children, ...restProps }) => {
  const { toggleShow, setToggleShow } = useContext(ToggleContext);

  return (
    <Header onClick={() => setToggleShow(!toggleShow)} {...restProps}>
      {children}
    </Header>
  );
};

const AccordionBody = ({ children, ...restProps }) => {
  const { toggleShow } = useContext(ToggleContext);
  return (
    <Body className={toggleShow ? "open" : "close"}>
      <span>{children}</span>
    </Body>
  );
};

이어서 Item에서 사용할 Header와 Body를 만들었다. 

두 컴포넌트는 ToggleContext에서 props를 받아서 작업을 수행한다. 

 

Accordion.Title = AccordionTitle;
Accordion.Frame = AccordionFrame;
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Body = AccordionBody;

이제 앞서 만든 컴포넌트들을 모두 Accordion 컴포넌트에 포함시킨다. 

이렇게 깔끔한 Accordion 컴포넌트를 만들 수 있게 되었다. 

 

const faqList = [
  {
    id: 1,
    header: "What is Netflix?",
    body: "Netflix is a streaming service that offers a wide variety of award-winning TV programs, films, anime, documentaries and more – on thousands of internet-connected devices.\n\nYou can watch as much as you want, whenever you want, without a single advert – all for one low monthly price. There’s always something new to discover, and new TV programs and films are added every week!",
  },
  {
    id: 2,
    header: "How much does Netflix cost?",
    body: "Watch Netflix on your smartphone, tablet, smart TV, laptop or streaming device, all for one low fixed monthly fee. Plans start from £5.99 a month. No extra costs or contracts.",
  },
  {
    id: 3,
    header: "Where can I watch?",
    body: "Watch anywhere, anytime, on an unlimited number of devices. Sign in with your Netflix account to watch instantly on the web at netflix.com from your personal computer or on any internet-connected device that offers the Netflix app, including smart TVs, smartphones, tablets, streaming media players and game consoles.\n\nYou can also download your favorite programs with the iOS, Android, or Windows 10 app. Use downloads to watch while you’re on the go and without an internet connection. Take Netflix with you anywhere.",
  },
  {
    id: 4,
    header: "How do I cancel?",
    body: "Netflix is flexible. There are no annoying contracts and no commitments. You can easily cancel your account online with two clicks. There are no cancellation fees – start or stop your account at any time.",
  },
  {
    id: 5,
    header: "What can I watch on Netflix?",
    body: "Netflix has an extensive library of feature films, documentaries, TV programs, anime, award-winning Netflix originals, and more. Watch as much as you want, any time you want.",
  },
];

Accordion 컴포넌트를 테스트하기 위해서 간단한 faq 데이터를 가지고 왔다. 

 

function App() {
  return (
    <Accordion>
      <Accordion.Title>Frequently Asked Questions</Accordion.Title>
      <Accordion.Frame>
        {faqList.map((item) => (
          <Accordion.Item key={item.id}>
            <Accordion.Header>{item.header}</Accordion.Header>
            <Accordion.Body>{item.body}</Accordion.Body>
          </Accordion.Item>
        ))}
      </Accordion.Frame>
    </Accordion>
  );
}

그리고 다음과 같이 Accordion 컴포넌트를 사용하면 된다. 

사용자는 API 명세만 알면 내부를 몰라도 쉽게 사용할 수 있다. 

 

출처 

https://www.smashingmagazine.com/2021/08/compound-components-react/

 

Compound Components In React — Smashing Magazine

Compound components help developers build more expressive and flexible APIs to share state and logic within components. This tutorial explains how this can be achieved with the help of using the Context API and React to build components by using this advan

www.smashingmagazine.com

 

 

반응형

'React > 실험실' 카테고리의 다른 글

[React] useForm  (0) 2023.03.05
[React] redux-persist  (1) 2023.02.11
[React] Context Module Function 패턴  (0) 2023.02.02
[React] Context API 언제 사용해야할까?  (0) 2023.01.31
[React] 독립된 React 컴포넌트의 이점  (0) 2023.01.29