React Compound Components: ‘Build Your Own Adventure’ 패턴
Source: Dev.to
위 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 코드 블록, URL 및 기술 용어는 그대로 유지하면서 번역해 드릴 수 있습니다. 감사합니다.
🎨 그래픽 설명
“메가‑프롭” 방식 (구식) 🦖
거대한 설정 객체를 전달합니다. 컴포넌트가 레이아웃을 결정합니다. 당신은 전혀 제어할 수 없습니다. Tab List를 Panels 아래로 옮기고 싶다면 라이브러리를 다시 작성해야 합니다.
+-----------------------------+
| |
+-----------------------------+
| [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>
);
🏆 왜 이걸 해야 할까요? (사용 사례)
-
UI 라이브러리 (탭, 셀렉트, 메뉴)
디자인 시스템을 구축한다면 이것은 필수입니다. 사용자는 탭 안에 아이콘을 넣거나 입력 필드 아래에 라벨을 배치하고 싶어합니다. 복합 컴포넌트를 사용하면renderLabelBottom같은 prop을 추가하지 않아도 이를 구현할 수 있습니다. -
암시적 상태 공유
Accordion.Header가 패널을 토글하는 방법을 자동으로 알고 있다는 점을 보세요. 사용자는onClick={() => setIndex(1)}같은 코드를 직접 연결할 필요가 없습니다. 그대로 작동합니다. -
시맨틱 구조
HTML처럼 읽힙니다: “. 선언적이고 아름답습니다.
🕳️ 함정 (The “Gotchas”)
-
“단일 자식” 제한
자식을 “ 로 감싸서 컨텍스트를 차단하면(컨텍스트 API에서는 드물지만 오래된React.Children.map방식에서는 흔함) 문제가 발생합니다.
해결: 위에서 보여준 Context API 패턴을 그대로 사용하세요. div를 X‑rays처럼 투과합니다. -
과도한 설계
간단한 버튼에 이 방법을 쓰지 마세요. 컴포넌트에 여러 개의 서로 다른 자식 간에 공유해야 할 내부 상태가 없다면 일반 props만 사용하면 됩니다. -
네임 오염
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). 🥺🤝