본문 바로가기

Design & Rendering Patterns/React) Design Patterns

[React Design] Compound Pattern (복합 컴포넌트 패턴)

 

애플리케이션에는 상호 의존적인 컴포넌트가 있는 경우가 많습니다. 

이들은 state와 로직을 함께 공유하며 서로 의존하고 있습니다.

select, 드롭다운 컴포넌트 또는 메뉴 항목과 같은 컴포넌트에서 이러한 패턴을 자주 볼 수 있습니다. 

 

복합 컴포넌트 패턴을 사용하면 하나의 작업을 수행하기 위해 모두 함께 작동하는 컴포넌트를 만들 수 있습니다.

 

Chakra UI 에서 볼 수 있는

<Tabs>
  <Tabs.TabList>
    <Tabs.Tab>탭1</Tabs.Tab>
    <Tabs.Tab>탭2</Tabs.Tab>
  </Tabs.TabList>
  <Tabs.TabPanels>
    <Tabs.TabPanel>내용1</Tabs.TabPanel>
    <Tabs.TabPanel>내용2</Tabs.TabPanel>
  </Tabs.TabPanels>
</Tabs>

 

<상위 컴포넌트. 하위 컴포넌트 명> 으로 컴포넌트 이름을 명명하는 게 특징 입니다.

 

 

Context API

React.Children.map 이나 React .cloneElement 를 사용할 경우

1. React.Children.map의 경우 Wrapper로 감싸면 props 전달이 안되어 컴포넌트 중첩이 제한되고

2.React .cloneElement 는 props 의 얕은 병합이 수행되어, 이미 존재하는 props는  새 props와 병합 혹은 덮어써지는 문제가 발생하게 됩니다.

 

그래서 context api로 훨씬 깔끔히 구현이 가능합니다.

 

  • 중첩 깊이에 상관없이 데이터 접근 가능
  • Props 충돌 문제 없음
  • 더 명확한 컴포넌트 관계
  • TypeScript와의 더 나은 통합

하나를 누르면 클릭한 목록은 열리고, 다른 목록은 닫히는 아코디언 메뉴를 만든다고 가정합시다

 

 

 

// App.jsx

import Accordion from "./components/Accordion/Accordion";

function App() {
  return (
    <section>
      <h2>너 내 동료가 되어라?</h2>
      <Accordion className="accordion">
        <Accordion.Item id='experience' className="accordion-item" title="우린 20년의 역사가 있다">
          <article>
            <p>배멀미는 없는가?</p>
            <p>대한민국 국적의 어선의 경우 보통 참치배라 부르는 다랑어 선망어선이나 다랑어 연승어선이 널리 알려져 있고, 트롤어선,메로라 불리는 이빨고기 저연승어선, 꽁치를 잡는 봉수망어선과 오징어채낚이선 등이 흔한 축에 속한다.</p>
          </article>
        </Accordion.Item>
        <Accordion.Item id='recruit' className="accordion-item" title="어서 입사해라">
          <article>
            <p>새우는 좋아하는가?</p>
            <p>다만 현재 원양어선에 초임 승조원으로 승선하기 위해서는 해기사 면허가 필수이며 면허가 없다면 승선이 힘들다</p>
          </article>
        </Accordion.Item>
      </Accordion>
    </section>
  )
}

export default App;

 

 

이와 같은 구조가 있다면 현재 App 컴포넌트에서는 Accordion 만 임포트하고 있는걸 확인 할 수 있습니다.

 

 

 

// Accordion.jsx

import { createContext, useContext, useState } from "react";
import AccordionItem from "./AccordionItem.jsx";

const AccordionContext = createContext();

export function useAccordionContext() {
  const ctx = useContext(AccordionContext);
  if (!ctx)     throw new Error('Accordion components must be used within an Accordion')
  return ctx;
}

export default function Accordion({ children, className }) { 
  const [openItemId, setOpenItemId] = useState(null);

  function toggleItem(id) {
    setOpenItemId(prevId => prevId === id ? null : id);
  }

  const contextValue = {
    openItemId,
    toggleItem,
  }

  return (
    <AccordionContext.Provider value={contextValue}>
      <ul className={className}>
        {children}
      </ul>
    </AccordionContext.Provider>
  )
}

Accordion.Item = AccordionItem

 

상위 컴포넌트인 Accordion.jsx 에서 createContext 로 context api를 구현 후, 하위 컴포넌트를 감싸고

openItemId state 와 openItemId setter 함수인 toggleItem 함수를 공유해주는 역할을 하고 있습니다.

 

AccordionItem을 import 하여 Accordion.Item = AccordionItem 로 변환해주는 것도 이 컴포넌트의 역할 입니다.

 

 

// AccordionItem.jsx

import { useAccordionContext } from "./Accordion";

export default function AccordionItem({ id, className, title, children }) {
  const { openItemId, toggleItem } = useAccordionContext();

  const isOpen = openItemId === id;

  function handleClick() {
    toggleItem(id);
  }

  return (
    <li className={className}>
      <h3 onClick={handleClick}>{title}</h3>
      <div className={isOpen ? 'accordion-item-content open' : 'accordion-item-content' }>{children}</div>
    </li>
  )
}

 

하위 컴포넌트에서는 현재 클릭한 id 가 내 id 라면 display: block 으로 보여주고

아닐 경우는 display: none 으로 AccordionItem 을 안보여주고 있습니다

 

// index.css
<style>
.accordion-item-content {
  display: none;
  padding: 1rem;
  background-color: #2c344a;
}

.open {
  display: block;
}
</style>

 

이 처럼 context api 를 사용하여 상호 공유된  state와 로직을 통해 상호 동작을 하는 모습을 구현할 수 있습니다

 

참고 : https://www.patterns.dev/react/compound-pattern

 

Compound Pattern

Create multiple components that work together to perform a single task

www.patterns.dev