9ui: React Base UI + Tailwind CSS Copy-Paste 컴포넌트 라이브러리 완벽 가이드
⏱️ 예상 읽기 시간: 12분
서론
React 생태계에서 UI 컴포넌트 라이브러리를 선택할 때 항상 고민이 됩니다. Material-UI는 무겁고, Ant Design은 디자인이 제한적이고, 직접 만들기엔 시간이 부족하죠. 이런 딜레마를 해결하는 새로운 접근법이 바로 9ui입니다.
9ui는 Base UI + Tailwind CSS를 기반으로 한 copy-paste 방식의 컴포넌트 라이브러리로, npm 패키지가 아닌 코드를 직접 복사해서 사용하는 혁신적인 방식을 제공합니다. 이를 통해 완전한 커스터마이징과 최적화된 성능을 동시에 얻을 수 있습니다.
9ui란 무엇인가?
핵심 특징
9ui는 shadcn/ui의 철학을 계승하면서도 Base UI를 기반으로 한 차세대 컴포넌트 라이브러리입니다.
특징 | 설명 | 장점 |
---|---|---|
Copy-Paste 방식 | npm 설치가 아닌 코드 직접 복사 | 완전한 커스터마이징 가능 |
Base UI 기반 | 접근성 우수한 unstyled 컴포넌트 | WAI-ARIA 표준 준수 |
Tailwind CSS | 유틸리티 퍼스트 CSS 프레임워크 | 빠른 스타일링, 작은 번들 크기 |
TypeScript 지원 | 완전한 타입 안전성 | 개발자 경험 향상 |
shadcn CLI 호환 | 기존 도구 체인 활용 | 간편한 설치 및 관리 |
기존 라이브러리와의 차이점
graph LR
A[전통적인 UI 라이브러리] --> B[큰 번들 크기]
A --> C[제한된 커스터마이징]
A --> D[복잡한 오버라이드]
E[9ui Copy-Paste 방식] --> F[필요한 컴포넌트만]
E --> G[완전한 커스터마이징]
E --> H[코드 직접 제어]
환경 설정 및 설치
사전 요구사항
- Node.js: 18.0.0 이상
- React: 18.0.0 이상
- Next.js: 13.0.0 이상 (권장)
- Tailwind CSS: 3.0.0 이상
개발 환경 확인
# 현재 버전 확인
node --version
npm --version
테스트 환경:
- macOS 15.0.0 (Sequoia)
- Node.js v20.10.0
- npm 10.2.3
1. Next.js 프로젝트 생성
# 새 Next.js 프로젝트 생성
npx create-next-app@latest my-9ui-project \
--typescript \
--tailwind \
--eslint \
--app \
--no-src-dir \
--no-import-alias
cd my-9ui-project
2. shadcn/ui CLI 초기화
# shadcn CLI 초기화
npx shadcn@latest init
초기화 옵션 선택:
- Framework: Next.js ✅
- Color: Neutral (권장)
- CSS Variables: Yes ✅
3. 9ui 테마 설치
# 9ui 공식 테마 추가
npx shadcn@latest add "https://9ui.dev/r/theme.json"
✅ 설치 확인: app/globals.css
에 9ui CSS 변수가 추가됩니다.
주요 컴포넌트 실습
Button 컴포넌트
가장 기본적인 컴포넌트부터 시작해보겠습니다.
# Button 컴포넌트 설치
npx shadcn@latest add "https://9ui.dev/r/button.json"
기본 사용법
import { Button } from "@/components/ui/button"
export default function ButtonDemo() {
return (
<div className="flex gap-4 flex-wrap">
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
)
}
사이즈 및 로딩 상태
import { Button } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
export default function AdvancedButton() {
return (
<div className="space-y-4">
{/* 다양한 크기 */}
<div className="flex gap-2 items-center">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
</div>
{/* 로딩 상태 */}
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</Button>
</div>
)
}
Card 컴포넌트
정보를 구조화하여 표시하는 핵심 컴포넌트입니다.
# Card 컴포넌트 설치
npx shadcn@latest add "https://9ui.dev/r/card.json"
기본 카드 구조
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export default function CardDemo() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 기본 카드 */}
<Card>
<CardHeader>
<CardTitle>프로젝트 A</CardTitle>
<CardDescription>
새로운 웹 애플리케이션 개발 프로젝트
</CardDescription>
</CardHeader>
<CardContent>
<p>React와 TypeScript를 활용한 현대적인 웹 애플리케이션을
개발하고 있습니다.</p>
</CardContent>
<CardFooter>
<Button>자세히 보기</Button>
</CardFooter>
</Card>
{/* 통계 카드 */}
<Card>
<CardHeader>
<CardTitle>월간 사용자</CardTitle>
<CardDescription>이번 달 활성 사용자 수</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">15,234</div>
<p className="text-xs text-muted-foreground">
+12.5% from last month
</p>
</CardContent>
</Card>
{/* 진행률 카드 */}
<Card>
<CardHeader>
<CardTitle>개발 진행률</CardTitle>
<CardDescription>현재 스프린트 진행 상황</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>완료된 작업</span>
<span>8/12</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{width: '67%'}}
/>
</div>
</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">
세부 사항 보기
</Button>
</CardFooter>
</Card>
</div>
)
}
Command 컴포넌트
검색과 실행을 위한 강력한 커맨드 팔레트입니다.
# Command 컴포넌트 설치 (Dialog도 함께 설치됨)
npx shadcn@latest add "https://9ui.dev/r/command.json"
기본 커맨드 팔레트
"use client"
import { useState } from "react"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@/components/ui/command"
import { Button } from "@/components/ui/button"
export default function CommandDemo() {
const [open, setOpen] = useState(false)
return (
<div className="space-y-4">
<Button onClick={() => setOpen(!open)}>
Command 팔레트 {open ? '닫기' : '열기'}
</Button>
{open && (
<div className="border rounded-lg max-w-md">
<Command>
<CommandInput placeholder="명령어 또는 검색어를 입력하세요..." />
<CommandList>
<CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
<CommandGroup heading="빠른 작업">
<CommandItem onSelect={() => console.log('새 파일')}>
📄 새 파일 생성
</CommandItem>
<CommandItem onSelect={() => console.log('프로젝트 열기')}>
📁 프로젝트 열기
</CommandItem>
<CommandItem onSelect={() => console.log('설정')}>
⚙️ 설정
</CommandItem>
</CommandGroup>
<CommandGroup heading="최근 명령어">
<CommandItem>git status</CommandItem>
<CommandItem>npm run dev</CommandItem>
<CommandItem>npm run build</CommandItem>
<CommandItem>code .</CommandItem>
</CommandGroup>
<CommandGroup heading="도구">
<CommandItem>🎨 색상 팔레트</CommandItem>
<CommandItem>📊 대시보드</CommandItem>
<CommandItem>🔍 검색</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</div>
)}
</div>
)
}
키바인딩과 함께 사용
"use client"
import { useEffect, useState } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Command, CommandInput, CommandList, CommandItem } from "@/components/ui/command"
export default function GlobalCommand() {
const [open, setOpen] = useState(false)
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
return (
<>
<p className="text-sm text-muted-foreground">
Press <kbd className="px-2 py-1 bg-muted rounded text-xs">⌘</kbd>
<kbd className="px-2 py-1 bg-muted rounded text-xs">K</kbd> to open
</p>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="overflow-hidden p-0">
<DialogHeader className="px-4 pb-4">
<DialogTitle>명령어 팔레트</DialogTitle>
<DialogDescription>
원하는 작업을 빠르게 실행하세요
</DialogDescription>
</DialogHeader>
<Command>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandItem onSelect={() => setOpen(false)}>
Calendar
</CommandItem>
<CommandItem onSelect={() => setOpen(false)}>
Search Emoji
</CommandItem>
<CommandItem onSelect={() => setOpen(false)}>
Calculator
</CommandItem>
</CommandList>
</Command>
</DialogContent>
</Dialog>
</>
)
}
고급 활용 패턴
1. 테마 커스터마이징
9ui의 가장 큰 장점은 완전한 커스터마이징입니다.
CSS 변수 수정
/* app/globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* 커스텀 색상 추가 */
--brand: 210 100% 50%;
--brand-foreground: 0 0% 100%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
/* 다크 모드 브랜드 색상 */
--brand: 210 100% 60%;
}
커스텀 버튼 Variant
// components/ui/button.tsx 수정
const buttonVariants = cva(
// ... 기존 base 클래스들
{
variants: {
variant: {
// ... 기존 variants
brand: "bg-brand text-brand-foreground hover:bg-brand/90",
gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600",
},
// ... 기존 size variants
}
}
)
// 사용 예시
<Button variant="brand">브랜드 버튼</Button>
<Button variant="gradient">그라디언트 버튼</Button>
2. 폼 통합 (React Hook Form + Zod)
# 필요한 패키지 설치
npm install react-hook-form @hookform/resolvers zod
# Form 컴포넌트 설치
npx shadcn@latest add "https://9ui.dev/r/form.json"
npx shadcn@latest add "https://9ui.dev/r/input.json"
npx shadcn@latest add "https://9ui.dev/r/label.json"
완성된 폼 예제
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
const formSchema = z.object({
username: z.string().min(2, "사용자명은 최소 2자 이상이어야 합니다."),
email: z.string().email("올바른 이메일 주소를 입력해주세요."),
password: z.string().min(8, "비밀번호는 최소 8자 이상이어야 합니다."),
})
export default function RegistrationForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
password: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
// 여기서 API 호출 등 처리
}
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>회원가입</CardTitle>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>사용자명</FormLabel>
<FormControl>
<Input placeholder="홍길동" {...field} />
</FormControl>
<FormDescription>
다른 사용자들에게 표시될 이름입니다.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>이메일</FormLabel>
<FormControl>
<Input type="email" placeholder="hong@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>비밀번호</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
가입하기
</Button>
</form>
</Form>
</CardContent>
</Card>
)
}
3. 데이터 테이블 구현
# Table 컴포넌트 설치
npx shadcn@latest add "https://9ui.dev/r/table.json"
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
const projectData = [
{
id: "PROJ-001",
name: "E-commerce Platform",
status: "active",
progress: 75,
team: "Frontend Team",
deadline: "2025-08-15"
},
{
id: "PROJ-002",
name: "Mobile App Redesign",
status: "review",
progress: 90,
team: "Design Team",
deadline: "2025-07-30"
},
{
id: "PROJ-003",
name: "API Migration",
status: "planning",
progress: 25,
team: "Backend Team",
deadline: "2025-09-10"
}
]
export default function ProjectTable() {
return (
<div className="border rounded-lg">
<Table>
<TableCaption>현재 진행 중인 프로젝트 목록</TableCaption>
<TableHeader>
<TableRow>
<TableHead>프로젝트 ID</TableHead>
<TableHead>프로젝트명</TableHead>
<TableHead>상태</TableHead>
<TableHead>진행률</TableHead>
<TableHead>담당팀</TableHead>
<TableHead>마감일</TableHead>
<TableHead className="text-right">작업</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projectData.map((project) => (
<TableRow key={project.id}>
<TableCell className="font-medium">{project.id}</TableCell>
<TableCell>{project.name}</TableCell>
<TableCell>
<Badge
variant={
project.status === "active" ? "default" :
project.status === "review" ? "secondary" :
"outline"
}
>
{project.status === "active" ? "진행중" :
project.status === "review" ? "검토중" : "계획중"}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{width: `${project.progress}%`}}
/>
</div>
<span className="text-sm">{project.progress}%</span>
</div>
</TableCell>
<TableCell>{project.team}</TableCell>
<TableCell>{project.deadline}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">편집</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
실제 프로젝트 적용 사례
1. 관리자 대시보드
// app/dashboard/page.tsx
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import {
Users,
DollarSign,
Activity,
TrendingUp,
Plus
} from "lucide-react"
export default function Dashboard() {
return (
<div className="space-y-6">
{/* 헤더 */}
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">대시보드</h1>
<Button>
<Plus className="mr-2 h-4 w-4" />
새 프로젝트
</Button>
</div>
{/* 통계 카드들 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">총 사용자</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2,350</div>
<p className="text-xs text-muted-foreground">
+180.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">수익</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+$12,234</div>
<p className="text-xs text-muted-foreground">
+19% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">활성 세션</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<p className="text-xs text-muted-foreground">
+201 from last hour
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">성장률</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12.5%</div>
<p className="text-xs text-muted-foreground">
+2% from last month
</p>
</CardContent>
</Card>
</div>
{/* 추가 콘텐츠 영역 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>최근 활동</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[1,2,3].map((item) => (
<div key={item} className="flex items-center space-x-4">
<div className="w-2 h-2 bg-blue-600 rounded-full" />
<div className="flex-1 space-y-1">
<p className="text-sm font-medium">새 사용자 등록</p>
<p className="text-xs text-muted-foreground">2분 전</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>빠른 액션</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<Button variant="outline" className="h-20 flex-col">
<Users className="h-6 w-6 mb-2" />
사용자 관리
</Button>
<Button variant="outline" className="h-20 flex-col">
<Activity className="h-6 w-6 mb-2" />
분석 보기
</Button>
<Button variant="outline" className="h-20 flex-col">
<DollarSign className="h-6 w-6 mb-2" />
결제 관리
</Button>
<Button variant="outline" className="h-20 flex-col">
<TrendingUp className="h-6 w-6 mb-2" />
리포트
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
2. 개발 도구 UI
// components/DevToolsPanel.tsx
"use client"
import { useState } from "react"
import {
Command,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem
} from "@/components/ui/command"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
Terminal,
FileText,
Settings,
Search,
GitBranch,
Play
} from "lucide-react"
const tools = [
{ name: "터미널", icon: Terminal, command: "terminal", category: "개발" },
{ name: "파일 검색", icon: Search, command: "search", category: "개발" },
{ name: "Git 상태", icon: GitBranch, command: "git", category: "버전관리" },
{ name: "빌드 실행", icon: Play, command: "build", category: "빌드" },
{ name: "설정", icon: Settings, command: "settings", category: "환경설정" },
{ name: "로그 보기", icon: FileText, command: "logs", category: "디버깅" },
]
export default function DevToolsPanel() {
const [selectedTool, setSelectedTool] = useState<string | null>(null)
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 커맨드 팔레트 */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
개발 도구 팔레트
</CardTitle>
</CardHeader>
<CardContent>
<Command>
<CommandInput placeholder="도구 또는 명령어 검색..." />
<CommandList>
<CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
{["개발", "버전관리", "빌드", "환경설정", "디버깅"].map(category => (
<CommandGroup key={category} heading={category}>
{tools
.filter(tool => tool.category === category)
.map(tool => (
<CommandItem
key={tool.command}
onSelect={() => setSelectedTool(tool.command)}
>
<tool.icon className="mr-2 h-4 w-4" />
{tool.name}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</CardContent>
</Card>
{/* 도구 상태 */}
<Card>
<CardHeader>
<CardTitle>시스템 상태</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm">서버 상태</span>
<Badge variant="default">실행중</Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-sm">빌드 상태</span>
<Badge variant="secondary">성공</Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-sm">테스트</span>
<Badge variant="outline">통과</Badge>
</div>
<div className="pt-4">
<Button className="w-full" variant="outline">
전체 상태 보기
</Button>
</div>
</CardContent>
</Card>
{/* 선택된 도구 상세 */}
{selectedTool && (
<Card className="lg:col-span-3">
<CardHeader>
<CardTitle>
{tools.find(t => t.command === selectedTool)?.name} 실행 결과
</CardTitle>
</CardHeader>
<CardContent>
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm">
<div>$ {selectedTool}</div>
<div className="mt-2">
{selectedTool === "git" && "On branch main\nYour branch is up to date with 'origin/main'."}
{selectedTool === "build" && "✓ Build completed successfully\n✓ Generated static files\n✓ Ready for deployment"}
{selectedTool === "terminal" && "터미널이 준비되었습니다. 명령어를 입력하세요."}
{!["git", "build", "terminal"].includes(selectedTool) && `${selectedTool} 명령어가 실행되었습니다.`}
</div>
</div>
</CardContent>
</Card>
)}
</div>
)
}
성능 최적화 및 베스트 프랙티스
1. Tree Shaking 최적화
9ui의 copy-paste 방식은 자동으로 tree shaking을 지원합니다:
// ❌ 전체 라이브러리 import (불필요한 코드 포함)
import * as UIComponents from 'some-ui-library'
// ✅ 필요한 컴포넌트만 import (9ui 방식)
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
2. 컴포넌트 lazy loading
import { lazy, Suspense } from 'react'
import { Card, CardContent } from "@/components/ui/card"
const HeavyChart = lazy(() => import('./HeavyChart'))
export default function Dashboard() {
return (
<Card>
<CardContent>
<Suspense fallback={<div>차트 로딩 중...</div>}>
<HeavyChart />
</Suspense>
</CardContent>
</Card>
)
}
3. 메모이제이션 활용
"use client"
import { memo, useMemo } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
interface StatsCardProps {
title: string
value: number
change: number
}
const StatsCard = memo(({ title, value, change }: StatsCardProps) => {
const changeColor = useMemo(() => {
return change > 0 ? 'text-green-600' : 'text-red-600'
}, [change])
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value.toLocaleString()}</div>
<p className={`text-sm ${changeColor}`}>
{change > 0 ? '+' : ''}{change}%
</p>
</CardContent>
</Card>
)
})
StatsCard.displayName = 'StatsCard'
4. 접근성 개선
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
export default function AccessibleUI() {
return (
<Card>
<CardContent>
{/* 스크린 리더를 위한 라벨 */}
<Button
aria-label="프로젝트 삭제"
aria-describedby="delete-description"
>
삭제
</Button>
<div id="delete-description" className="sr-only">
이 작업은 되돌릴 수 없습니다. 신중히 확인하세요.
</div>
{/* 키보드 네비게이션 지원 */}
<div role="tablist" className="flex">
<Button
role="tab"
aria-selected="true"
aria-controls="panel-1"
tabIndex={0}
>
탭 1
</Button>
<Button
role="tab"
aria-selected="false"
aria-controls="panel-2"
tabIndex={-1}
>
탭 2
</Button>
</div>
</CardContent>
</Card>
)
}
테스트 및 디버깅
개발 서버 실행
# 개발 서버 시작
npm run dev
# 브라우저에서 확인
open http://localhost:3000
빌드 테스트
# 프로덕션 빌드
npm run build
# 빌드 결과 확인
npm start
Storybook 통합 (선택사항)
# Storybook 설치
npx storybook@latest init
# 9ui 컴포넌트 스토리 생성
mkdir -p stories/ui
// stories/ui/Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from '@/components/ui/button'
const meta: Meta<typeof Button> = {
title: '9ui/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
},
},
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: 'Button',
},
}
export const Destructive: Story = {
args: {
variant: 'destructive',
children: 'Delete',
},
}
문제 해결 및 FAQ
자주 발생하는 문제들
1. Tailwind CSS 클래스가 적용되지 않는 경우
// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
// 9ui 컴포넌트 경로 추가
'./components/ui/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}
2. Base UI 의존성 오류
# Base UI 패키지 수동 설치
npm install @base_ui/react
3. TypeScript 타입 오류
// types/globals.d.ts
declare module '@base_ui/react' {
// Base UI 타입 선언
}
유용한 개발 도구
VS Code 확장
// .vscode/extensions.json
{
"recommendations": [
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-next"
]
}
prettier 설정
// .prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": ["prettier-plugin-tailwindcss"]
}
zshrc Aliases 설정
작업 효율성을 위한 유용한 aliases:
# ~/.zshrc에 추가
# 9ui 관련 aliases
alias 9ui-install="npx shadcn@latest add"
alias 9ui-theme="npx shadcn@latest add 'https://9ui.dev/r/theme.json'"
alias 9ui-button="npx shadcn@latest add 'https://9ui.dev/r/button.json'"
alias 9ui-card="npx shadcn@latest add 'https://9ui.dev/r/card.json'"
alias 9ui-command="npx shadcn@latest add 'https://9ui.dev/r/command.json'"
alias 9ui-form="npx shadcn@latest add 'https://9ui.dev/r/form.json'"
# 개발 워크플로우
alias ndev="npm run dev"
alias nbuild="npm run build"
alias nstart="npm start"
alias nlint="npm run lint"
# Git 워크플로우
alias gst="git status"
alias gaa="git add ."
alias gcm="git commit -m"
alias gps="git push"
alias gpl="git pull"
# 프로젝트 생성
alias create-9ui="npx create-next-app@latest --typescript --tailwind --eslint --app"
# 버전 확인
alias check-env="node --version && npm --version && git --version"
적용하기:
source ~/.zshrc
결론
9ui는 Base UI + Tailwind CSS의 강력한 조합으로 현대적인 React 애플리케이션 개발에 새로운 패러다임을 제시합니다.
핵심 장점 요약
장점 | 설명 | 효과 |
---|---|---|
완전한 제어권 | copy-paste 방식으로 코드 직접 수정 | 100% 커스터마이징 |
우수한 성능 | 필요한 컴포넌트만 포함 | 작은 번들 크기 |
뛰어난 접근성 | Base UI의 WAI-ARIA 표준 | 포용적 사용자 경험 |
개발자 경험 | TypeScript + shadcn CLI | 빠른 개발 속도 |
미래 보장 | 표준 기술 기반 | 장기적 안정성 |
권장 사용 시나리오
- ✅ 커스텀 디자인 시스템 구축이 필요한 경우
- ✅ 성능이 중요한 웹 애플리케이션
- ✅ 접근성이 중요한 프로덕트
- ✅ 완전한 제어권이 필요한 프로젝트
- ✅ 빠른 프로토타이핑이 필요한 상황
다음 단계
- 공식 문서 탐색: 9ui.dev 에서 더 많은 컴포넌트 확인
- 커뮤니티 참여: GitHub에서 이슈 리포팅 및 기여
- 심화 학습: Base UI와 Tailwind CSS 고급 기능 마스터
- 실제 프로젝트 적용: 사이드 프로젝트나 회사 프로젝트에 도입
9ui는 단순한 컴포넌트 라이브러리를 넘어, 개발자가 진정으로 원하는 UI를 구현할 수 있는 자유로움을 제공합니다. 이제 여러분의 차례입니다. 9ui로 멋진 애플리케이션을 만들어보세요! 🚀
실제 테스트 환경:
- macOS 15.0.0 (Sequoia)
- Node.js v20.10.0
- Next.js 15.0.3
- React 19.0.0
- Tailwind CSS 4.0.0
테스트 완료된 컴포넌트:
- ✅ Button (모든 variant 테스트 완료)
- ✅ Card (기본, 통계, 진행률 카드)
- ✅ Command (검색, 그룹핑, 키바인딩)
- ✅ Form (React Hook Form + Zod 통합)
- ✅ Table (데이터 테이블 구현)