한국 정부 디자인 시스템을 위한 shadcn/ui 스타일 컴포넌트 라이브러리 구축
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?
- 완전한 제어 – 코드를 직접 소유합니다. 버튼 모양을 바꾸고 싶다면
button.tsx를 바로 수정하면 됩니다. - 런타임 의존성 없음 – 컴포넌트가 복사돼서
node_modules에서 가져오지 않으므로 번들에 실제 사용한 것만 포함됩니다. - Tailwind 네이티브 – 모든 것이 Tailwind 기반이어서 최신 React 프로젝트와 잘 맞습니다.
- 타입 안전성 – TypeScript +
cva덕분에 변형에 대한 자동 완성과 컴파일 시점 검사가 가능합니다. - 기본 접근성 – Radix 프리미티브와 ARIA 속성 덕분에 WCAG 준수가 기본 제공됩니다.
Trade‑offs
- 자동 업데이트 없음 – 상위 레포가 변경돼도 직접 컴포넌트를 업데이트해야 합니다.
- 초기 설정이 다소 큼 – 프로젝트에 파일이 많이 추가됩니다.
- 학습 곡선 – 컴포넌트 구조와 CLI 사용법을 익히는 데 시간이 필요합니다.
KRDS 프로젝트에서 커스터마이징이 불가피할 경우, 이러한 트레이드오프는 충분히 감수할 만합니다.
Get Started
npx hanui init
npx hanui add button input modal select tabs
GitHub:
Documentation: