한국 정부 디자인 시스템을 위한 shadcn/ui 스타일 컴포넌트 라이브러리 구축

발행: (2025년 12월 10일 오전 10:16 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Architecture

┌─────────────────────────────────────────────┐
│  CLI (npx hanui add button)                 │
└─────────────────┬───────────────────────────┘


┌─────────────────────────────────────────────┐
│  Component Registry (components.json)       │
│  - Component definitions                    │
│  - Dependencies                             │
│  - File paths                               │
└─────────────────┬───────────────────────────┘


┌─────────────────────────────────────────────┐
│  Your Project                               │
│  components/ui/button.tsx  ← copied here    │
└─────────────────────────────────────────────┘

CLI

CLI는 진입점입니다. 다음을 담당합니다:

  • init – 프로젝트를 초기화합니다
  • add – 컴포넌트를 프로젝트에 복사합니다
  • diff – 변경 사항을 보여줍니다 (곧 제공)
npx hanui init
# 생성 내용:
# - components.json (설정)
# - variables.css (디자인 토큰) 복사
# - tailwind.config.ts 업데이트 (프리셋 추가)
npx hanui add button modal input
# 복사 내용:
# - components/ui/button.tsx
# - components/ui/modal.tsx
# - components/ui/input.tsx
# - 의존성(예: cn 유틸리티) 등

Component Structure

각 컴포넌트는 일관된 패턴을 따릅니다:

// button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  // Base styles
  'inline-flex items-center justify-center rounded font-medium transition-colors',
  {
    variants: {
      variant: {
        primary: 'bg-krds-primary-base text-white hover:bg-krds-primary-60',
        secondary: 'bg-krds-gray-10 text-krds-gray-90 hover:bg-krds-gray-20',
        tertiary: 'bg-transparent text-krds-primary-base hover:bg-krds-primary-5',
        danger: 'bg-krds-danger-base text-white hover:bg-krds-danger-60',
      },
      size: {
        sm: 'h-8 px-3 text-krds-label-sm',
        md: 'h-10 px-4 text-krds-label-md',
        lg: 'h-12 px-6 text-krds-label-lg',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

interface ButtonProps
  extends React.ButtonHTMLAttributes,
    VariantProps {
  loading?: boolean;
}

export function Button({
  className,
  variant,
  size,
  loading,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      aria-busy={loading}
      {...props}
    >
      {loading && /* loading indicator placeholder */}
      {children}
    </button>
  );
}

핵심 패턴

  • 타입‑안전한 변형을 위한 cva
  • 클래스 이름 병합을 위한 cn 유틸리티
  • Tailwind 클래스에 사용된 KRDS 토큰 (krds-primary-base, krds-label-md)
  • 내장 접근성 (aria-busy)

Design Token System

KRDS는 색상, 타이포그래피, 간격 값을 정의합니다. HANUI는 이를 CSS 변수와 Tailwind 프리셋으로 구현합니다.

CSS Variables (variables.css)

:root {
  /* Colors */
  --krds-color-light-primary-50: #256ef4;
  --krds-color-light-primary-60: #0b50d0;

  /* Semantic tokens */
  --krds-primary-base: var(--krds-color-light-primary-50);
  --krds-primary-text: var(--krds-color-light-primary-60);

  /* Typography */
  --krds-fs-body-md: 17px;
  --krds-fs-heading-lg: 24px;
}

.dark {
  /* Dark mode overrides */
  --krds-primary-base: var(--krds-color-light-primary-50);
  --krds-primary-text: var(--krds-color-light-primary-40);
}

Tailwind Preset (tailwind.preset.ts)

const hanuiPreset = {
  theme: {
    extend: {
      colors: {
        'krds-primary': {
          DEFAULT: 'var(--krds-primary-base)',
          base: 'var(--krds-primary-base)',
          text: 'var(--krds-primary-text)',
          50: 'var(--krds-color-light-primary-50)',
          60: 'var(--krds-color-light-primary-60)',
        },
      },
      fontSize: {
        'krds-body-md': ['17px', { lineHeight: '150%' }],
        'krds-heading-lg': [
          'var(--krds-fs-heading-lg)',
          { lineHeight: '150%', fontWeight: '700' },
        ],
      },
    },
  },
};

이제 KRDS 값에 직접 매핑되는 Tailwind 클래스를 사용할 수 있습니다:

// Example usage
<div className="text-krds-primary-base">
  KRDS‑compliant text
</div>

Radix UI Integration

복잡한 컴포넌트(Modal, Select, Tabs)는 Radix UI 프리미티브 위에 구축됩니다.

왜 Radix인가?

  • 접근성 – 기본적으로 WAI‑ARIA를 준수
  • 스타일이 없음 – KRDS 스타일을 자유롭게 적용 가능
  • 조합 가능 – 유연한 복합 컴포넌트 API 제공
// modal.tsx
import * as Dialog from '@radix-ui/react-dialog';

export function Modal({ children, ...props }) {
  return (
    <Dialog.Root {...props}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50" />
        <Dialog.Content className="bg-white p-4 rounded">
          {children}
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

export const ModalTitle = Dialog.Title;
export const ModalDescription = Dialog.Description;
export const ModalClose = Dialog.Close;

제공되는 기능:

  • 포커스 트랩
  • Escape 키 처리
  • 스크린 리더 알림
  • 스크롤 잠금

모두 무료로 제공되며, 우리는 KRDS 스타일만 추가합니다.

Why This Approach?

  1. 완전한 제어 – 코드를 직접 소유합니다. 버튼 모양을 바꾸고 싶다면 button.tsx를 바로 수정하면 됩니다.
  2. 런타임 의존성 없음 – 컴포넌트가 복사돼서 node_modules에서 가져오지 않으므로 번들에 실제 사용한 것만 포함됩니다.
  3. Tailwind 네이티브 – 모든 것이 Tailwind 기반이어서 최신 React 프로젝트와 잘 맞습니다.
  4. 타입 안전성 – TypeScript + cva 덕분에 변형에 대한 자동 완성과 컴파일 시점 검사가 가능합니다.
  5. 기본 접근성 – Radix 프리미티브와 ARIA 속성 덕분에 WCAG 준수가 기본 제공됩니다.

Trade‑offs

  • 자동 업데이트 없음 – 상위 레포가 변경돼도 직접 컴포넌트를 업데이트해야 합니다.
  • 초기 설정이 다소 큼 – 프로젝트에 파일이 많이 추가됩니다.
  • 학습 곡선 – 컴포넌트 구조와 CLI 사용법을 익히는 데 시간이 필요합니다.

KRDS 프로젝트에서 커스터마이징이 불가피할 경우, 이러한 트레이드오프는 충분히 감수할 만합니다.

Get Started

npx hanui init
npx hanui add button input modal select tabs

GitHub:
Documentation:

Back to Blog

관련 글

더 보기 »