Badge
상태, 카테고리, 라벨을 표시하는 작은 시각적 표시기
개요
Badge 컴포넌트는 간결한 정보를 시각적으로 강조하여 표시하는 컴포넌트입니다.
주요 특징
- ✅ 다양한 Variants: default, secondary, destructive, outline
- ✅ 크기 옵션: sm, default, lg
- ✅ 의미론적 색상: 상태를 나타내는 시각적 단서
- ✅ 유연한 사용: 텍스트, 아이콘, 숫자 표시
- ✅ 접근성: ARIA 속성 지원
설치
CLI로 추가 (권장)
npx @vortex/cli add badge자동으로 설치되는 것들:
src/components/ui/badge.tsxclass-variance-authority의존성- Tailwind CSS 스타일
수동 설치
pnpm add class-variance-authority기본 사용법
Default
import { Badge } from "@/components/ui/badge";
export default function BadgeDemo() {
return <Badge>Default</Badge>;
}Secondary
<Badge variant="secondary">Secondary</Badge>Destructive
<Badge variant="destructive">Destructive</Badge>Outline
<Badge variant="outline">Outline</Badge>Variants
Default
기본 스타일의 배지
<Badge>Default</Badge>사용 사례: 일반 라벨, 기본 카테고리
Secondary
보조 정보 표시
<Badge variant="secondary">Secondary</Badge>사용 사례: 부가 정보, 태그, 메타데이터
Destructive
경고, 에러, 삭제 상태
<Badge variant="destructive">Error</Badge>
<Badge variant="destructive">Delete</Badge>사용 사례: 에러 상태, 위험 알림, 삭제 대기
Outline
테두리만 있는 스타일
<Badge variant="outline">Outline</Badge>사용 사례: 미니멀한 디자인, 중요도 낮은 정보
Sizes
Small
<Badge size="sm">Small</Badge>사용 사례: 인라인 텍스트, 컴팩트 UI
Default
<Badge>Default</Badge>사용 사례: 일반적인 경우
Large
<Badge size="lg">Large</Badge>사용 사례: 강조 필요, 터치 인터페이스
언제 사용하는가
✅ 권장 사용 사례
- 상태 표시: Active, Pending, Completed
- 카테고리 라벨: Technology, Design, Business
- 알림 카운트: 3 new messages
- 버전 표시: v2.0, Beta, New
- 태그: React, TypeScript, Tailwind
예시
// 상태 표시
<Badge variant="secondary">Active</Badge>
<Badge variant="outline">Pending</Badge>
<Badge>Completed</Badge>
// 카테고리
<Badge variant="secondary">Technology</Badge>
<Badge variant="secondary">Design</Badge>
// 알림
<Badge variant="destructive">3</Badge>
// 버전
<Badge variant="outline">v2.0</Badge>
<Badge>New</Badge>언제 사용하지 말아야 하는가
❌ 피해야 할 사용 사례
- 긴 텍스트: 한 줄 이상의 텍스트
- 클릭 가능한 액션: Button 사용 권장
- 복잡한 정보: Card 또는 Tooltip 사용
- 입력 필드: Input 컴포넌트 사용
대안
| 상황 | 대안 | 이유 |
|---|---|---|
| 긴 텍스트 | Card | 더 많은 공간과 구조 제공 |
| 클릭 액션 필요 | Button | 인터랙션 명확성 |
| 복잡한 정보 | Tooltip | 상세 설명 표시 가능 |
| 사용자 입력 | Input | 입력 전용 컴포넌트 |
| 토글 기능 | Switch | 상태 변경 시각화 |
Advanced Usage
아이콘과 함께 사용
import { Badge } from "@/components/ui/badge";
import { CheckCircle2, AlertCircle, XCircle } from "lucide-react";
export default function IconBadge() {
return (
<div className="flex gap-2">
<Badge variant="secondary">
<CheckCircle2 className="mr-1 h-3 w-3" />
Success
</Badge>
<Badge variant="outline">
<AlertCircle className="mr-1 h-3 w-3" />
Warning
</Badge>
<Badge variant="destructive">
<XCircle className="mr-1 h-3 w-3" />
Error
</Badge>
</div>
);
}숫자 카운트
<div className="flex items-center gap-2">
<span>Messages</span>
<Badge variant="destructive" size="sm">
5
</Badge>
</div>점(Dot) 표시기
<Badge variant="secondary" className="gap-1">
<span className="h-2 w-2 rounded-full bg-green-500" />
Online
</Badge>
<Badge variant="outline" className="gap-1">
<span className="h-2 w-2 rounded-full bg-gray-400" />
Offline
</Badge>제거 가능한 Badge
"use client";
import { Badge } from "@/components/ui/badge";
import { X } from "lucide-react";
import { useState } from "react";
export default function RemovableBadge() {
const [tags, setTags] = useState(["React", "TypeScript", "Tailwind"]);
return (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Badge key={tag} variant="secondary" className="gap-1">
{tag}
<button
onClick={() => setTags(tags.filter((t) => t !== tag))}
className="ml-1 rounded-full hover:bg-secondary-foreground/20"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
);
}테이블에서 상태 표시
import { Badge } from "@/components/ui/badge";
const orders = [
{ id: "1", status: "completed" },
{ id: "2", status: "pending" },
{ id: "3", status: "cancelled" },
];
export default function OrderTable() {
return (
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id}>
<td>{order.id}</td>
<td>
{order.status === "completed" && (
<Badge variant="secondary">Completed</Badge>
)}
{order.status === "pending" && (
<Badge variant="outline">Pending</Badge>
)}
{order.status === "cancelled" && (
<Badge variant="destructive">Cancelled</Badge>
)}
</td>
</tr>
))}
</tbody>
</table>
);
}카드와 함께 사용
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function CardBadge() {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Project Alpha</CardTitle>
<Badge variant="secondary">Active</Badge>
</div>
</CardHeader>
<CardContent>
<p>프로젝트 설명...</p>
<div className="flex gap-2 mt-4">
<Badge variant="outline" size="sm">
React
</Badge>
<Badge variant="outline" size="sm">
TypeScript
</Badge>
</div>
</CardContent>
</Card>
);
}접근성 (Accessibility)
ARIA Labels
<Badge aria-label="3개의 읽지 않은 메시지">3</Badge>의미론적 색상
// ✅ 좋은 예: 색상 + 텍스트
<Badge variant="destructive">Error: Failed</Badge>
// ❌ 나쁜 예: 색상만
<Badge variant="destructive" />스크린 리더
<div>
<span id="status-label">현재 상태:</span>
<Badge aria-labelledby="status-label">Active</Badge>
</div>라이브 리전
동적으로 변경되는 Badge:
"use client";
import { Badge } from "@/components/ui/badge";
import { useState } from "react";
export default function LiveBadge() {
const [count, setCount] = useState(0);
return (
<div>
<Badge variant="destructive" aria-live="polite" aria-atomic="true">
{count} 알림
</Badge>
<button onClick={() => setCount(count + 1)}>알림 추가</button>
</div>
);
}Best Practices
1. 간결한 텍스트
// ✅ 좋은 예
<Badge>New</Badge>
<Badge>3</Badge>
<Badge>Active</Badge>
// ❌ 나쁜 예
<Badge>This is a very long text that should not be in a badge</Badge>2. 의미 있는 색상 사용
// ✅ 좋은 예: 상태와 색상 일치
<Badge variant="secondary">Success</Badge>
<Badge variant="destructive">Error</Badge>
// ❌ 나쁜 예: 잘못된 색상 매핑
<Badge variant="destructive">Success</Badge>3. 일관된 크기 사용
// ✅ 좋은 예: 같은 컨텍스트에서 일관된 크기
<div className="flex gap-2">
<Badge size="sm">React</Badge>
<Badge size="sm">TypeScript</Badge>
<Badge size="sm">Tailwind</Badge>
</div>
// ❌ 나쁜 예: 혼재된 크기
<div className="flex gap-2">
<Badge size="sm">React</Badge>
<Badge>TypeScript</Badge>
<Badge size="lg">Tailwind</Badge>
</div>4. 적절한 간격
// ✅ 좋은 예
<div className="flex flex-wrap gap-2">
<Badge>Tag 1</Badge>
<Badge>Tag 2</Badge>
<Badge>Tag 3</Badge>
</div>
// ❌ 나쁜 예: 간격 없음
<div>
<Badge>Tag 1</Badge>
<Badge>Tag 2</Badge>
<Badge>Tag 3</Badge>
</div>5. 과도한 사용 피하기
// ✅ 좋은 예: 필요한 정보만
<div>
<h3>프로젝트 Alpha</h3>
<Badge variant="secondary">Active</Badge>
</div>
// ❌ 나쁜 예: 너무 많은 Badge
<div>
<h3>프로젝트 Alpha</h3>
<Badge>New</Badge>
<Badge>Hot</Badge>
<Badge>Trending</Badge>
<Badge>Popular</Badge>
<Badge>Featured</Badge>
</div>디자인 가이드라인
크기 및 간격
// Small: height 20px, padding 4px 8px, text xs
<Badge size="sm">Small</Badge>
// Default: height 24px, padding 4px 10px, text sm
<Badge>Default</Badge>
// Large: height 28px, padding 6px 12px, text base
<Badge size="lg">Large</Badge>색상 가이드
/* Default */
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
/* Secondary */
background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
/* Destructive */
background: hsl(var(--destructive));
color: hsl(var(--destructive-foreground));
/* Outline */
border: 1px solid hsl(var(--input));
color: hsl(var(--foreground));타이포그래피
/* Small */
font-size: 0.75rem; /* 12px */
line-height: 1rem;
/* Default */
font-size: 0.875rem; /* 14px */
line-height: 1.25rem;
/* Large */
font-size: 1rem; /* 16px */
line-height: 1.5rem;TypeScript
Props 타입
import type { VariantProps } from "class-variance-authority";
import { badgeVariants } from "@/components/ui/badge";
type BadgeProps = React.HTMLAttributes<HTMLDivElement> &
VariantProps<typeof badgeVariants>;
// 사용 예시
const CustomBadge = ({ variant, size, ...props }: BadgeProps) => {
return <Badge variant={variant} size={size} {...props} />;
};상태 타입 매핑
type Status = "active" | "pending" | "completed" | "cancelled";
const statusVariantMap: Record<
Status,
"secondary" | "outline" | "destructive"
> = {
active: "secondary",
pending: "outline",
completed: "secondary",
cancelled: "destructive",
};
interface StatusBadgeProps {
status: Status;
}
export function StatusBadge({ status }: StatusBadgeProps) {
return <Badge variant={statusVariantMap[status]}>{status}</Badge>;
}Badge 컬렉션
interface Tag {
id: string;
label: string;
variant?: "default" | "secondary" | "destructive" | "outline";
}
interface TagListProps {
tags: Tag[];
onRemove?: (id: string) => void;
}
export function TagList({ tags, onRemove }: TagListProps) {
return (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Badge key={tag.id} variant={tag.variant}>
{tag.label}
{onRemove && (
<button onClick={() => onRemove(tag.id)}>
<X className="h-3 w-3 ml-1" />
</button>
)}
</Badge>
))}
</div>
);
}성능 최적화
많은 Badge 렌더링
"use client";
import { Badge } from "@/components/ui/badge";
import { memo } from "react";
const BadgeItem = memo(
({ label, variant }: { label: string; variant?: string }) => {
return <Badge variant={variant as any}>{label}</Badge>;
}
);
export default function BadgeList() {
const tags = Array.from({ length: 100 }, (_, i) => ({
id: i,
label: `Tag ${i}`,
variant: "secondary",
}));
return (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<BadgeItem key={tag.id} label={tag.label} variant={tag.variant} />
))}
</div>
);
}동적 스타일
import { Badge } from "@/components/ui/badge";
import { useMemo } from "react";
export function DynamicBadge({ count }: { count: number }) {
const variant = useMemo(() => {
if (count === 0) return "outline";
if (count < 5) return "secondary";
if (count < 10) return "default";
return "destructive";
}, [count]);
return <Badge variant={variant}>{count}</Badge>;
}실전 예제
이메일 받은편지함
import { Badge } from "@/components/ui/badge";
const emails = [
{ id: 1, subject: "회의 안내", unread: true },
{ id: 2, subject: "프로젝트 업데이트", unread: true },
{ id: 3, subject: "일정 확인", unread: false },
];
export function EmailList() {
return (
<div className="space-y-2">
{emails.map((email) => (
<div
key={email.id}
className="flex items-center justify-between p-4 border rounded"
>
<span className={email.unread ? "font-semibold" : ""}>
{email.subject}
</span>
{email.unread && (
<Badge variant="destructive" size="sm">
New
</Badge>
)}
</div>
))}
</div>
);
}제품 카드
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export function ProductCard() {
return (
<Card>
<CardHeader>
<div className="flex items-start justify-between">
<CardTitle>MacBook Pro</CardTitle>
<Badge variant="destructive">Sale</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">$1,999</p>
<p className="text-sm text-muted-foreground line-through">$2,499</p>
</CardContent>
<CardFooter>
<div className="flex gap-2">
<Badge variant="outline" size="sm">
14-inch
</Badge>
<Badge variant="outline" size="sm">
M3 Pro
</Badge>
<Badge variant="outline" size="sm">
18GB RAM
</Badge>
</div>
</CardFooter>
</Card>
);
}관련 컴포넌트
참고 자료
지원 및 피드백
문제가 발생하거나 개선 제안이 있으신가요?
- 📧 이메일: dev@vortex.com
- 💬 Slack: #vortex-design-system
- 🐛 버그 리포트: GitLab Issues
Last updated on