테마
Tailwind 공유 컴포넌트 - Button, Table, Modal 구현
Tailwind CSS로 스타일링한 Button, Table, Modal 공유 컴포넌트를 구현하고 합성 패턴을 학습한다
학습 목표
- Tailwind CSS 기반의 재사용 가능한 공유 컴포넌트를 설계할 수 있다
- Button, Table, Modal 공통 컴포넌트의 Props 인터페이스를 정의할 수 있다
- 컴포넌트 합성(composition) 패턴을 이해하고 적용할 수 있다
- ANTD의 API 설계를 참조하여 일관된 컴포넌트 인터페이스를 만들 수 있다
본문
1. 공유 컴포넌트 설계 원칙
web 프로젝트에서는 ANTD 같은 UI 프레임워크 대신 Tailwind CSS로 직접 공유 컴포넌트를 구현한다. ANTD의 API 설계를 참조하되, 경량화된 형태로 만든다.
2. Button 컴포넌트
Props 인터페이스
typescript
type ButtonType = "confirm" | "cancel";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
buttonType?: ButtonType;
}Tailwind 스타일 정의
typescript
const buttonColors: Record<ButtonType, { base: string; hover: string }> = {
confirm: {
base: "bg-blue-500 text-white",
hover: "hover:bg-blue-700",
},
cancel: {
base: "bg-red-500 text-white",
hover: "hover:bg-red-700",
},
};컴포넌트 구현
tsx
export default function Button({
buttonType = "confirm",
className = "",
children,
...props
}: ButtonProps) {
const colors = buttonColors[buttonType];
return (
<button
className={`
px-4 py-2 rounded cursor-pointer
${colors.base} ${colors.hover}
${className}
`}
{...props}
>
{children}
</button>
);
}3. Table 컴포넌트
ANTD Table의 columns/dataSource 패턴을 참조하여, Tailwind로 스타일링한 경량 테이블을 구현한다.
Props 인터페이스
typescript
interface Column<T = any> {
dataIndex: string; // 데이터 키
title: string; // 헤더 제목
render?: (value: any, record: T) => React.ReactNode;
}
interface TableProps<T = any> {
columns: Column<T>[];
datas: T[];
rowKey: string;
onRowClick?: (record: T) => void;
}컴포넌트 구현
tsx
export default function Table<T extends Record<string, any>>({
columns, datas, rowKey, onRowClick,
}: TableProps<T>) {
return (
<table className="w-full">
<thead className="bg-gray-200">
<tr>
{columns.map((col) => (
<th key={col.dataIndex} className="px-4 py-2">{col.title}</th>
))}
</tr>
</thead>
<tbody className="border">
{datas.map((data) => (
<tr key={String(data[rowKey])}
className="hover:bg-gray-50 cursor-pointer border-b"
onClick={() => onRowClick?.(data)}>
{columns.map((col) => {
const value = data[col.dataIndex];
return (
<td key={col.dataIndex} className="px-4 py-2 text-center">
{col.render ? col.render(value, data) : String(value)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
}4. Modal 컴포넌트
컴포넌트 구현
tsx
interface ModalProps {
open: boolean;
title?: React.ReactNode;
children?: React.ReactNode;
onClose: () => void;
onConfirm?: () => void;
}
export default function Modal({
open,
title,
children,
onClose,
onConfirm,
}: ModalProps) {
if (!open) return null;
return (
<>
{/* Dim 배경 */}
<div
className="fixed inset-0 bg-black/50 z-40"
onClick={onClose}
/>
{/* 콘텐츠 */}
<div className="fixed inset-0 flex items-center justify-center z-50">
<div className="bg-white rounded min-w-[400px] min-h-[300px] p-6">
{/* Header */}
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold">{title}</h3>
<span className="cursor-pointer text-xl" onClick={onClose}>
x
</span>
</div>
{/* Body */}
<div className="border-y py-4 overflow-auto max-h-[400px]">
{children}
</div>
{/* Footer */}
<div className="flex justify-end gap-2 mt-4">
{onConfirm && (
<Button buttonType="confirm" onClick={onConfirm}>
확인
</Button>
)}
<Button buttonType="cancel" onClick={onClose}>
닫기
</Button>
</div>
</div>
</div>
</>
);
}5. 컴포넌트 합성 패턴
ShoppingList에서 세 컴포넌트를 조합하여 완전한 기능을 구현한다.
tsx
function ShoppingList() {
const { items, deleteItem } = useShoppingStore();
const [selectedItem, setSelectedItem] = useState<Item | undefined>();
const columns = [
{ dataIndex: "name", title: "상품명" },
{
dataIndex: "price",
title: "가격",
render: (v: number) => `${v.toLocaleString()}원`,
},
{ dataIndex: "count", title: "수량" },
{
dataIndex: "id",
title: "처리",
render: (id: number) => (
<Button
buttonType="cancel"
onClick={(e) => {
e.stopPropagation();
deleteItem(id);
}}
>
삭제
</Button>
),
},
];
return (
<>
<Table
columns={columns}
datas={items}
rowKey="id"
onRowClick={(item) => setSelectedItem(item)}
/>
<Modal
open={!!selectedItem}
title="상품 상세"
onClose={() => setSelectedItem(undefined)}
>
<p>상품명: {selectedItem?.name}</p>
<p>가격: {selectedItem?.price.toLocaleString()}원</p>
<p>수량: {selectedItem?.count}개</p>
</Modal>
</>
);
}6. 컴포넌트 합성 흐름
7. ANTD vs Tailwind 공유 컴포넌트 비교
| 비교 항목 | ANTD (Docs 프로젝트) | Tailwind (Web 프로젝트) |
|---|---|---|
| 번들 크기 | 크다 (트리셰이킹 필요) | 작다 (사용한 유틸만 포함) |
| 커스터마이징 | 테마 토큰 기반 | 유틸리티 클래스 조합 |
| Shadow DOM 대응 | StyleProvider 필요 | toString-loader 사용 |
| 컴포넌트 완성도 | 높음 (즉시 사용 가능) | 직접 구현 필요 |
| 디자인 일관성 | ANTD 디자인 시스템 | 자체 디자인 자유도 |
Tailwind 설정은 tailwind.config.js에서 content 경로를 src/**/*.{js,jsx,ts,tsx}로 지정하고, index.css에 @tailwind base/components/utilities 디렉티브를 선언한다. toString-loader가 이 CSS를 문자열로 변환하여 Shadow DOM에 주입한다.
핵심 정리
- 공유 컴포넌트 설계: ANTD의
columns/dataSource같은 검증된 API 패턴을 참조하되, Tailwind로 경량화하여 구현한다 - 컴포넌트 합성: Button(원자) -> Table/Modal(분자) -> ShoppingList(조합) 순서로 계층적으로 합성한다
- Tailwind + Shadow DOM: toString-loader로 Tailwind CSS를 문자열로 변환한 후 Shadow Root에 주입하면 스타일 격리가 가능하다
- 기술 선택의 다양성: 같은 마이크로 프론트엔드 시스템 내에서 ANTD 프로젝트와 Tailwind 프로젝트가 공존할 수 있다