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

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

Source: Dev.to

아키텍처

┌─────────────────────────────────────────────┐
│  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 (config)
# - variables.css (디자인 토큰) 복사
# - tailwind.config.ts 업데이트 (preset 추가)
npx hanui add button modal input
# 복사:
# - components/ui/button.tsx
# - components/ui/modal.tsx
# - components/ui/input.tsx
# - 의존성(예: cn 유틸리티) 등

컴포넌트 구조

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

// 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 (
    
      {loading && }
      {children}
    
  );
}

핵심 패턴

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

디자인 토큰 시스템

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

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

CSS 변수 (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 프리셋 (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 통합

복잡한 컴포넌트(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 (
    
      
        
        
          {children}
        
      
    
  );
}

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

결과적인 기능:

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

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

왜 이 접근법인가?

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

트레이드‑오프

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

맞춤화가 불가피한 KRDS 프로젝트에서는 이러한 트레이드‑오프가 충분히 가치가 있습니다.

시작하기

npx hanui init
npx hanui add button input modal select tabs

GitHub:
문서:

Back to Blog

관련 글

더 보기 »

플랫폼 요소 소개

새 제품의 일부로, 이제 사전 구축된 UI 블록 및 액션 세트를 사용하여 애플리케이션에 직접 기능을 추가할 수 있습니다. Vercel for Platforms of pr...