핵심 개념
고급 패턴
opt-ui 컴포넌트의 고급 사용법, 커스텀 스타일링, 상태 관리 패턴을 설명합니다.
reopt design업데이트
DataTable 필터링
DataTable과 Input을 조합하여 실시간 필터링을 구현합니다. 컬럼별 커스텀 렌더링과 Badge를 활용한 상태 표시도 함께 적용합니다.
상품 목록
| 상품명 | 카테고리 | 가격 | 재고 | 상태 |
|---|---|---|---|---|
| 프리미엄 무선 키보드 | 전자기기 | ₩129,000 | 45개 | 판매중 |
| 에르고노믹 마우스 | 전자기기 | ₩89,000 | 120개 | 판매중 |
| USB-C 허브 7포트 | 액세서리 | ₩59,000 | 0개 | 보관됨 |
| 모니터 스탠드 | 가구 | ₩45,000 | 23개 | 판매중 |
| 노트북 파우치 15인치 | 액세서리 | ₩35,000 | 89개 | 준비중 |
| 블루투스 스피커 | 전자기기 | ₩79,000 | 15개 | 판매중 |
code
import { DataTable, Input, Badge } from "@reopt-ai/opt-ui";
const [search, setSearch] = useState("");
// 필터링 로직
const filtered = products.filter(
(p) => p.name.includes(search) || p.category.includes(search)
);
// 커스텀 컬럼 렌더링
const columns = [
{ id: "name", header: "상품명", accessor: "name" },
{
id: "status",
header: "상태",
accessor: (row) => (
<Badge variant={statusVariant[row.status]}>
{statusLabel[row.status]}
</Badge>
),
},
];
<Input value={search} onChange={(e) => setSearch(e.target.value)} />
<DataTable data={filtered} columns={columns} />DashboardGrid 반응형
DashboardGrid의 columns prop을 동적으로 변경하여 반응형 레이아웃을 구현합니다. 기간 필터와 함께 데이터 변경도 시뮬레이션합니다.
컬럼:
기간:
↑↓←→ 키보드로 탐색총 매출₩12.4M↑ +12.5%
주문 수1,234↑ +8.2%
신규 고객89↓ -3.1%
전환율3.2%↑ +0.4%
이탈률42%↑ -2.1%
평균 주문액₩89,000↑ +5.3%
code
import { DashboardGrid, Button } from "@reopt-ai/opt-ui";
const [columns, setColumns] = useState(3);
// 화면 크기에 따른 반응형
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 640) setColumns(1);
else if (window.innerWidth < 1024) setColumns(2);
else setColumns(3);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
<DashboardGrid stats={stats} columns={columns} />Toast 알림 시스템
ToastProvider로 앱을 감싸고, useToast hook으로 어디서든 알림을 표시합니다. 변형별 스타일과 지속 시간 커스텀이 가능합니다.
버튼을 클릭하면 화면 우측 상단에 토스트가 표시됩니다.
code
import { ToastProvider, useToast } from "@reopt-ai/opt-ui";
// 1. 앱 최상위에 Provider 배치
function App() {
return (
<ToastProvider position="top-right" defaultDuration={5000}>
<MyApp />
</ToastProvider>
);
}
// 2. 컴포넌트에서 useToast 사용
function SaveButton() {
const { addToast } = useToast();
const handleSave = async () => {
try {
await saveData();
addToast({
variant: "success",
title: "저장 완료",
message: "변경사항이 저장되었습니다.",
});
} catch (error) {
addToast({
variant: "error",
title: "오류",
message: "저장에 실패했습니다.",
duration: 8000, // 8초 (기본값 오버라이드)
});
}
};
return <Button onClick={handleSave}>저장</Button>;
}Combobox 그룹화
ComboboxGroup과 ComboboxGroupLabel로 검색 결과를 카테고리별로 그룹화합니다. 커스텀 아이템 렌더링으로 풍부한 UI를 구현합니다.
code
import {
ComboboxRoot, ComboboxInput, ComboboxPopover,
ComboboxItem, ComboboxGroup, ComboboxGroupLabel,
} from "@reopt-ai/opt-ui";
// 부서별 그룹화
const grouped = users.reduce((acc, user) => {
if (!acc[user.department]) acc[user.department] = [];
acc[user.department].push(user);
return acc;
}, {});
<ComboboxRoot setValue={setSearch} resetValueOnHide>
<ComboboxInput placeholder="팀원 검색..." autoSelect />
<ComboboxPopover>
{Object.entries(grouped).map(([dept, users]) => (
<ComboboxGroup key={dept}>
<ComboboxGroupLabel>{dept}</ComboboxGroupLabel>
{users.map((user) => (
<ComboboxItem key={user.id} value={user.name}>
<Avatar name={user.name} size="sm" />
<span>{user.name}</span>
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxPopover>
</ComboboxRoot>커스텀 스타일링
opt-ui 컴포넌트는 className prop으로 Tailwind 클래스를 추가할 수 있습니다. data-* 속성 셀렉터로 상태별 스타일도 적용 가능합니다.
code
// 1. className으로 직접 스타일 추가
<Button className="shadow-lg hover:shadow-xl">
그림자 버튼
</Button>
// 2. data-* 속성으로 상태 스타일
<ComboboxItem
className={cn(
// 기본 스타일
"px-3 py-2",
// 활성 상태 (키보드 탐색)
"data-[active-item]:bg-blue-50 data-[active-item]:text-blue-700",
// 포커스 visible (Tab 키)
"data-[focus-visible]:ring-2 data-[focus-visible]:ring-blue-600",
)}
>
// 3. 조건부 스타일
<Badge
variant={status === "active" ? "success" : "default"}
className={status === "error" ? "animate-pulse" : ""}
>
{statusLabel}
</Badge>컴포지션 패턴
여러 컴포넌트를 조합하여 복잡한 UI를 구성합니다. Primitives → Blocks → Compositions 계층을 활용합니다.
code
// 예: 검색 + 테이블 + 페이지네이션 조합
function DataExplorer<T>({ data, columns }: Props<T>) {
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const filtered = useMemo(
() => data.filter(/* 검색 로직 */),
[data, search]
);
const paginated = useMemo(
() => filtered.slice((page - 1) * 10, page * 10),
[filtered, page]
);
return (
<div className="space-y-4">
<SearchCombobox
items={data.map(extractSearchableText)}
onSelect={setSearch}
/>
<DataTable data={paginated} columns={columns} />
<Pagination
total={filtered.length}
page={page}
onChange={setPage}
/>
</div>
);
}키보드 우선 설계
opt-ui의 모든 컴포넌트는 키보드 탐색을 우선 지원합니다. 주요 패턴과 확인 사항입니다.
| 패턴 | 설명 | 컴포넌트 |
|---|---|---|
| Roving Tabindex | 그룹 내 Tab 1회, 방향키로 이동 | CompositeZone, Tabs, Menu, Toolbar |
| Focus Trap | 모달 내부에 포커스 가둠 | Dialog, CommandPalette |
| Focus Restore | 닫힐 때 이전 요소로 복귀 | Dialog, Menu, Select |
| Typeahead | 문자 입력으로 빠른 탐색 | Select, Combobox, Menu |