Shadcn + Tailwind로 공통 버튼 컴포넌트 설계하기

김현준·2025년 8월 26일

html/css

목록 보기
33/33

1. 버튼은 왜 공통화해야 할까?

  • 프로젝트가 커질수록 버튼 종류(CTA, 아웃라인, 링크, 아이콘…)는 계속 늘어난다.
  • 피그마에는 다양한 스타일이 있지만, 그대로 다 만들면 관리가 지옥이 된다.
  • 그래서 의도(intent)표현(appearance), 크기(size) 를 축으로 나눠 하나의 Button 컴포넌트로 통합하는 것이 현업에서 일반적이다.

2. Variants와 CompoundVariants 개념

  • variants
    → 단일 속성으로 분기 (예: size=sm/md/lg, appearance=solid/outline/ghost/link)
  • compoundVariants
    → 여러 조건이 동시에 만족할 때만 적용되는 스타일 (예: intent=primary + appearance=solid → CTA 버튼 전용 padding)

즉, variants는 축 하나씩 관리, compoundVariants는 조합 스타일 관리라고 보면 된다.


3. 코드 예시

import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import * as React from "react";

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring",
  {
    variants: {
      // 색상 의도
      intent: {
        primary: "bg-primary",
        neutral: "bg-secondary",
        destructive: "bg-destructive",
      },
      // 표현 방식
      appearance: {
        solid: "",
        outline: "border bg-background",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "underline-offset-4 hover:underline bg-transparent px-0",
      },
      // 크기
      size: {
        sm: "h-8 px-3 text-sm rounded-md",
        md: "h-9 px-4 text-sm rounded-md",
        lg: "h-10 px-6 text-base rounded-lg",
        icon: "size-9 rounded-md",
      },
    },

    // 조합 전용 스타일
    compoundVariants: [
      {
        intent: "primary",
        appearance: "solid",
        class: "px-4 py-[11px] sm:py-[15px] rounded-lg font-semibold",
      },
    ],

    defaultVariants: {
      intent: "primary",
      appearance: "solid",
      size: "md",
    },
  }
);

type ButtonProps = React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & { asChild?: boolean };

export function Button({
  className,
  intent,
  appearance,
  size,
  asChild = false,
  ...props
}: ButtonProps) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      className={cn(buttonVariants({ intent, appearance, size, className }))}
      {...props}
    />
  );
}

4. 사용 예시

// 기본 Primary Solid (CTA)
<Button intent="primary" appearance="solid">
  정산금 조회하기
</Button>

// Neutral Solid
<Button intent="neutral" appearance="solid">
  다음
</Button>

// Outline
<Button intent="neutral" appearance="outline">
  인증하기
</Button>

// Ghost
<Button intent="neutral" appearance="ghost">
  더보기
</Button>

// Link
<Button intent="primary" appearance="link">
  자세히 보기
</Button>

// Icon Only
<Button size="icon" intent="primary" appearance="solid">
  <PlusIcon />
</Button>

5. 정리

  • intent: 버튼의 색상 의도 (primary, neutral, destructive 등)
  • appearance: 버튼의 표현 방식 (solid, outline, ghost, link)
  • size: 버튼의 크기 (sm, md, lg, icon)
  • compoundVariants: 여러 조건이 동시에 만족할 때 적용되는 조합 전용 스타일
profile
기록하자

0개의 댓글