React Compound Components: ‘Build Your Own Adventure’ 패턴

발행: (2026년 2월 16일 오후 08:07 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

위 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 코드 블록, URL 및 기술 용어는 그대로 유지하면서 번역해 드릴 수 있습니다. 감사합니다.

🎨 그래픽 설명

“메가‑프롭” 방식 (구식) 🦖

거대한 설정 객체를 전달합니다. 컴포넌트가 레이아웃을 결정합니다. 당신은 전혀 제어할 수 없습니다. Tab ListPanels 아래로 옮기고 싶다면 라이브러리를 다시 작성해야 합니다.

+-----------------------------+
|       |
+-----------------------------+
| [Tab 1] [Tab 2] [Tab 3]     |
   {/* You put this where YOU want */}
    One
    Two


    Content 1
    Content 2

부모(Tabs)가 Context를 사용해 비밀스럽게 상태를 자식에게 전달합니다. 그들은 텔레파시처럼 소통합니다. 🧠✨

💻 코드: “Prop Hell”에서 “Component Heaven”으로

1️⃣ 설정 – Context 만들기

먼저, 컴포넌트들이 통신할 비밀 채널이 필요합니다.

import React, { createContext, useContext, useState } from 'react';

// 1. Create the Context
const AccordionContext = createContext();

// 2. Create the Parent Component
const Accordion = ({ children }) => {
  const [openIndex, setOpenIndex] = useState(0);

  // The toggle function logic
  const toggleIndex = (index) => {
    setOpenIndex(prev => (prev === index ? null : index));
  };

  return (
    {/* Provide context to children */}
    {children}
  );
};

2️⃣ 자식 – Context 사용

이제 하위 컴포넌트를 생성합니다. 사용자로부터 isOpen이나 onClick을 prop으로 받지 않고, 컨텍스트에서 가져오는 것을 확인하세요.

// 3. The Item Component (Just a wrapper, usually)
const AccordionItem = ({ children }) => {
  return {children};
};

// 4. The Trigger (The clickable part)
const AccordionHeader = ({ children, index }) => {
  const { toggleIndex, openIndex } = useContext(AccordionContext);
  const isActive = openIndex === index;

  return (
    <div onClick={() => toggleIndex(index)}>
      {children} {isActive ? '🔽' : '▶️'}
    </div>
  );
};

// 5. The Content (The hidden part)
const AccordionPanel = ({ children, index }) => {
  const { openIndex } = useContext(AccordionContext);

  if (openIndex !== index) return null;

  return <div>{children}</div>;
};

// Attach sub‑components to the Parent for cleaner imports (optional but cool)
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

export default Accordion;

3️⃣ 사용 – 매직 순간 🪄

이 API가 얼마나 깔끔한지 보세요. 복잡한 설정 객체가 없습니다. JSX만 있으면 됩니다.

import Accordion from './Accordion';

const FAQ = () => (
  <Accordion>
    <Accordion.Item>
      <Accordion.Header index={0}>Is React hard?</Accordion.Header>
      <Accordion.Panel index={0}>
        Only until you understand `useEffect`. Then it's just chaotic.
      </Accordion.Panel>
    </Accordion.Item>

    <Accordion.Item>
      <Accordion.Header index={1}>Why use Compound Components?</Accordion.Header>
      <Accordion.Panel index={1}>
        Because passing 50 props is bad for your blood pressure.
      </Accordion.Panel>
    </Accordion.Item>
  </Accordion>
);

🏆 왜 이걸 해야 할까요? (사용 사례)

  1. UI 라이브러리 (탭, 셀렉트, 메뉴)
    디자인 시스템을 구축한다면 이것은 필수입니다. 사용자는 탭 안에 아이콘을 넣거나 입력 필드 아래에 라벨을 배치하고 싶어합니다. 복합 컴포넌트를 사용하면 renderLabelBottom 같은 prop을 추가하지 않아도 이를 구현할 수 있습니다.

  2. 암시적 상태 공유
    Accordion.Header가 패널을 토글하는 방법을 자동으로 알고 있다는 점을 보세요. 사용자는 onClick={() => setIndex(1)} 같은 코드를 직접 연결할 필요가 없습니다. 그대로 작동합니다.

  3. 시맨틱 구조
    HTML처럼 읽힙니다: “. 선언적이고 아름답습니다.

🕳️ 함정 (The “Gotchas”)

  1. “단일 자식” 제한
    자식을 “ 로 감싸서 컨텍스트를 차단하면(컨텍스트 API에서는 드물지만 오래된 React.Children.map 방식에서는 흔함) 문제가 발생합니다.
    해결: 위에서 보여준 Context API 패턴을 그대로 사용하세요. div를 X‑rays처럼 투과합니다.

  2. 과도한 설계
    간단한 버튼에 이 방법을 쓰지 마세요. 컴포넌트에 여러 개의 서로 다른 자식 간에 공유해야 할 내부 상태가 없다면 일반 props만 사용하면 됩니다.

  3. 네임 오염
    Accordion, AccordionItem, AccordionHeader, AccordionPanel을 모두 내보내면 import가 복잡해질 수 있습니다.
    해결: 메인 컴포넌트에 붙여서 사용하세요(Accordion.Item). 네임스페이스가 깔끔해지고 전문가처럼 보입니다.

🏁 결론

Compound Component Pattern은 누군가에게 물고기를 주는 것(경직된 컴포넌트)과 물고기 잡는 법을 가르치는 것(유연한 도구 세트) 사이의 차이입니다.

여러분(라이브러리 작성자)에게는 약간 더 많은 설정 코드가 필요하지만, 사용자에게는 훨씬 더 인체공학적이고 확장 가능한 API를 제공합니다.

It's a delightful experience for the developer using your component.  

And isn't that what we all want? To be loved by other developers? (Please validate me). 🥺🤝
0 조회
Back to Blog

관련 글

더 보기 »

미친 React key

tsx에서 map을 통한 렌더링 export function Parent { const array, setArray = useState(1, 2, 3, 4, 5); useEffect(() => { setTimeout(() => { setArray(prev => [6, 7, 8, 9, 10, ...prev]); ... }); }); }