핵심 개념
컴포지션 패턴
Shell이 Core를 감싸서 기능을 확장하는 방식과 커스텀 Shell을 만드는 가이드입니다.
reopt design업데이트
Shell → Core 조합
Shell은 Core를 import하여 감싸고 기능을 확장합니다. ContentTabs를 예로 살펴봅니다.
tsx
"use client";
import { type TabDef, TabsRoot, TabList, Tab, TabPanel } from "@reopt-ai/opt-ui";
export interface ContentTabsProps {
tabs: TabDef[];
defaultTabId?: string;
}
export function ContentTabs({ tabs, defaultTabId }: ContentTabsProps) {
return (
<TabsRoot defaultSelectedId={defaultTabId ?? tabs[0]?.id}>
<TabList aria-label="콘텐츠 탭">
{tabs.map((tab) => (
<Tab key={tab.id} id={tab.id}>{tab.label}</Tab>
))}
</TabList>
{tabs.map((tab) => (
<TabPanel key={tab.id} tabId={tab.id}>
{tab.content}
</TabPanel>
))}
</TabsRoot>
);
}패턴 요약: Shell은 (1) 데이터 인터페이스를 정의하고, (2) Core를 import하고, (3) 데이터를 Core에 매핑합니다. Shell 자체에는 스타일 코드가 없습니다.
데이터 주도 렌더링
Shell은 데이터 배열을 props로 받아 내부에서 Core에 매핑합니다. 이 패턴 덕분에 사용처에서는 데이터만 준비하면 됩니다.
tsx
// 사용처 (서버 컴포넌트)
const tabs: TabDef[] = [
{ id: "overview", label: "개요", content: <Overview /> },
{ id: "api", label: "API", content: <ApiDocs /> },
];
// Shell에 데이터 전달 — 렌더링은 Shell이 담당
<ContentTabs tabs={tabs} />Provider 중첩
FaqAccordion은 CompositeProvider 안에 여러 DisclosureRoot를 중첩합니다. Composite가 키보드 탐색을, 각 DisclosureRoot가 개별 열기/닫기 상태를 관리합니다.
tsx
"use client";
import {
CompositeZone,
CompositeRow,
CompositeItem,
DisclosureRoot,
DisclosureTrigger,
DisclosureContent,
type FaqItem,
} from "@reopt-ai/opt-ui";
export function FaqAccordion({ items }: { items: FaqItem[] }) {
return (
<CompositeProvider focusLoop orientation="vertical">
<Composite role="region" aria-label="자주 묻는 질문" className="...">
{items.map((faq) => (
<DisclosureRoot key={faq.id}>
<div>
<CompositeItem render={<DisclosureTrigger />}>
{faq.question}
</CompositeItem>
<DisclosureContent>
<p>{faq.answer}</p>
</DisclosureContent>
</div>
</DisclosureRoot>
))}
</Composite>
</CompositeProvider>
);
}CompositeItem의 render prop에 DisclosureTrigger를 넘기는 패턴은 render prop을 활용하여 Composite 아이템과 Disclosure 트리거를 하나의 요소로 합성합니다.
커스텀 Shell 만들기
4단계로 새로운 Shell을 만들 수 있습니다.
Step 1. Core 선택
필요한 UI 패턴에 맞는 Core를 선택합니다. 예: 알림 목록이라면 Disclosure(열기/닫기)와 Composite(키보드 탐색)가 필요합니다.
Step 2. 데이터 인터페이스 정의
tsx
// lib/types.ts에 추가
export interface NotificationItem {
id: string;
title: string;
body: string;
time: string;
}Step 3. Core 조합
tsx
// shells/notification-list.tsx
"use client";
import {
CompositeZone,
CompositeRow,
CompositeItem,
DisclosureRoot,
DisclosureTrigger,
DisclosureContent,
type NotificationItem,
} from "@reopt-ai/opt-ui";
export interface NotificationListProps {
items: NotificationItem[];
}
export function NotificationList({ items }: NotificationListProps) {
return (
<CompositeProvider focusLoop orientation="vertical">
<Composite role="region" aria-label="알림 목록" className="...">
{items.map((item) => (
<DisclosureRoot key={item.id}>
<div>
<CompositeItem render={<DisclosureTrigger />}>
<span>{item.title}</span>
<time>{item.time}</time>
</CompositeItem>
<DisclosureContent>
<p>{item.body}</p>
</DisclosureContent>
</div>
</DisclosureRoot>
))}
</Composite>
</CompositeProvider>
);
}Step 4. export 추가
shells/index.ts에 배럴 export를 추가하면 완료됩니다.
tsx
// shells/index.ts
export { NotificationList } from "./notification-list";