/**
* Badge 컴포넌트
*
* 상태, 카테고리 등을 표시하는 배지 컴포넌트입니다.
*/
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/cn';
export type BadgeVariant = 'default' 'success' 'warning' 'error' 'info';
export type BadgeSize = 'sm' 'md' 'lg';
export interface BadgeProps extends HTMLAttributesHTMLSpanElement {
variant?: BadgeVariant;
size?: BadgeSize;
}
const variantStyles: RecordBadgeVariant, string = {
default: 'bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100',
success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
error: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
info: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
};
const sizeStyles: RecordBadgeSize, string = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2.5 py-1 text-sm',
lg: 'px-3 py-1.5 text-base',
};
export const Badge = forwardRefHTMLSpanElement, BadgeProps(
({ className, variant = 'default', size = 'md', children, ...props }, ref) = {
return (
span
ref={ref}
className={cn(
'inline-flex items-center rounded-full font-medium',
variantStyles[variant],
sizeStyles[size],
className
)}
{...props}
{children}
/span
);
}
);
Badge.displayName = 'Badge';
1. 왜 Badge를 따로 만들었는지
프로젝트 곳곳에서 상태, 카테고리, 라벨을 보여줄 일이 많았는데, 매번 span에 클래스를 직접 넣다 보니 색과 크기가 제각각이었고 다크모드도 페이지마다 다르게 넣고 있었습니다. 그래서 의미별로 색이 정해진, 크기만 고르면 되는 공용 Badge 하나를 두기로 했습니다. 한 컴포넌트에서 variant와 size만 바꿔 쓰면 되고, 특정 페이지만 예외 스타일이 필요할 때는 className으로 덮어쓸 수 있게 만들었습니다.
2. 컴포넌트 구성과 설계 방향
Badge는 의미 전달이 목적이므로 시맨틱하게는 문장 안의 짧은 라벨에 가깝다고 보고 span을 선택했습니다. 버튼이 아니므로 button은 쓰지 않았고, 클릭 가능한 Badge가 필요해지면 이 컴포넌트를 감싸는 방식으로 두기로 했습니다.
스타일은 Tailwind만 사용하고, variantsize별 클래스를 Record 타입의 상수로 두어서 허용된 값만 쓰게 하고, cn()으로 기본 + variant + size + 사용처 className을 합치도록 했습니다. 그래서 한 곳만 수정해도 프로젝트 전체 Badge 룩이 맞춰지고, 필요할 때만 className으로 예외를 줄 수 있습니다.
3. 프롭스가 필요한 이유
variant
상태카테고리마다 색을 고정하고 싶어서 넣었습니다. 성공/경고/에러/정보/기본 다섯 가지로 제한해 두면, 팀원도 이건 success, 이건 error처럼 의미로만 선택하게 되고, 다크모드용 클래스도 variant마다 한 세트만 관리하면 됩니다. 그래서 BadgeVariant 타입과 variantStyles 맵으로 허용 값과 스타일을 1:1로 묶었습니다.
size
리스트 안의 작은 라벨, 카드 제목 옆, 대시보드 헤더 등 쓰이는 자리가 달라서 크기를 세 단계(sm, md, lg)로 나눴습니다. padding과 text-*만 바꾸는 sizeStyles로 처리해서, 같은 variant인데 크기만 다르게 쓰는 경우를 쉽게 만들었습니다.
className
디자인 시스템 기본만으로는 부족한 페이지(예: 블로그 카드에서만 보라 계열로 쓰고 싶을 때)를 위해 넣었습니다. cn()에서 맨 뒤에 두어 기본variantsize 다음에 적용되도록 했고, 그래서 대부분은 variant/size만 쓰고, 예외만 className으로 덮어쓴다는 규칙을 유지할 수 있습니다.
children, ref, ...propsBadge는 라벨용이라 내용은 전부 children으로 받기로 했고, HTMLAttributesHTMLSpanElement를 상속해 data-*, aria-*, id 등은 그대로 넘길 수 있게 했습니다. ref는 레이아웃 측정이나 포커스 제어가 필요해질 때를 대비해 forwardRef로 전달되도록 해 두었습니다.
4. 사용 용도와 쓰는 방식
관리자: 글 발행/대기, 미디어 사용 여부, 연락처네비게이션 설정 상태 등을
variant="success",variant="warning"등으로 표시할 때 사용합니다. 리스트가 많아서 작은 Badge가 필요하면size="sm"을 씁니다.블로그: 카테고리명은
variant="info"로 통일하고, 추천 글은variant="success"로 두었습니다. 카드 디자인에 맞추고 싶을 때만className으로 배경/글자색을 덮어씁니다.공통: 새 Badge 의미가 생기면
BadgeVariant와variantStyles에 한 줄씩만 추가하면 되고, 특정 페이지만 다른 색이 필요하면className으로 처리하는 식으로 씁니다.
5. 정리
Badge는 상태카테고리 같은 짧은 라벨을 의미에 맞는 색과 크기로 통일해서 쓰고, 예외는 className으로만 처리한다는 목적으로 만들었습니다. variant와 size 프롭은 그 목적을 위해 꼭 필요했고, className은 확장과 예외 처리용으로 두었습니다.