Design & Rendering Patterns/React) Design Patterns

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

머지?는 병합입니다 2024. 11. 25. 12:02

 

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

이들은 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