Skip to Content

Badge

상태, 카테고리, 라벨을 표시하는 작은 시각적 표시기


개요

Badge 컴포넌트는 간결한 정보를 시각적으로 강조하여 표시하는 컴포넌트입니다.

주요 특징

  • 다양한 Variants: default, secondary, destructive, outline
  • 크기 옵션: sm, default, lg
  • 의미론적 색상: 상태를 나타내는 시각적 단서
  • 유연한 사용: 텍스트, 아이콘, 숫자 표시
  • 접근성: ARIA 속성 지원

설치

CLI로 추가 (권장)

npx @vortex/cli add badge

자동으로 설치되는 것들:

  • src/components/ui/badge.tsx
  • class-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> ); }

관련 컴포넌트

  • Button: 클릭 가능한 액션
  • Card: 복잡한 정보 표시
  • Tooltip: 추가 설명 제공

참고 자료


지원 및 피드백

문제가 발생하거나 개선 제안이 있으신가요?

Last updated on