shadcn/ui + Tailwind: 거래 대시보드 디자인 시스템
빠른 탐색: 이 글은 shadcn/ui와 Tailwind CSS가 거래 대시보드에 적용되는 방식을 심층적으로 다루며, 디자인 시스템 구축부터 컴포넌트 커스터마이징까지 전문급 금융 인터페이스 개발의 완벽한 가이드를 제공합니다. 예상 읽기 시간 13분.
왜 shadcn/ui + Tailwind를 선택할까요?
금융 거래 인터페이스 개발에서 디자인 시스템의 선택은 개발 효율성과 사용자 경험에 직접적인 영향을 미칩니다. 기존의 UI 컴포넌트 라이브러리(예: Material-UI, Ant Design)는 기능이 풍부하지만 커스터마이징이 어렵고 번들 크기가 큽니다.
shadcn/ui는 독특한 "Copy-Paste 컴포넌트" 모드를 채택하여 컴포넌트 소스코드를 프로젝트에 직접 넣어 무한한 커스터마이징 유연성을 제공합니다. Tailwind CSS의 원자적 스타일과 함께 현대 금융 인터페이스 개발의 최선의 조합이 됩니다.
기존 컴포넌트 라이브러리와 비교
| 특성 | Material-UI | Ant Design | shadcn/ui + Tailwind |
|:---|:---|:---|:---|
| 커스터마이징 유연성 | 중간 (스타일 오버라이드 필요) | 중간 (테마 시스템) | 매우 높음 (소스코드 직접 수정) |
| Bundle 크기 | 큼 (200KB+) | 큼 (300KB+) | 작음 (사용한 컴포넌트만) |
| 학습 곡선 | 중간 | 중간 | 낮음 (Tailwind 직관적) |
| 디자인 일관성 | Material Design | Ant Design | 완전 커스터마이징 |
| TypeScript | 지원 | 지원 | 네이티브 우선 |
| 다크 모드 | 설정 필요 | 설정 필요 | 내장 지원 |
핵심 통찰: shadcn/ui는 "컴포넌트 라이브러리"가 아니라 "컴포넌트 모음"입니다. 각 컴포넌트의 완전한 제어권을 가지며, 이는 높은 브랜드 아이덴티티가 필요한 금융 제품에 필수적입니다.
디자인 시스템 기반: Tailwind 구성
색상 시스템 디자인
거래 인터페이스의 색상은 미학을 넘어 정보 전달의 기능을 담당합니다. 상승/하락, 수익/손실, 경고/안전 —— 각 색상은 명확한 의미를 가집니다.
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: ['class'],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
// 주 색상 - 브랜드 아이덴티티
primary: {
DEFAULT: '#0ea5e9', // Sky 500
foreground: '#ffffff',
50: '#f0f9ff',
100: '#e0f2fe',
500: '#0ea5e9',
600: '#0284c7',
900: '#0c4a6e',
},
// 거래 의미 색상
trade: {
up: '#22c55e', // Green 500 - 상승/수익
down: '#ef4444', // Red 500 - 하락/손실
neutral: '#6b7280', // Gray 500 - 보합
warning: '#f59e0b', // Amber 500 - 경고
info: '#3b82f6', // Blue 500 - 정보
},
// 배경 레벨
background: {
DEFAULT: '#0f172a', // Slate 900 - 메인 배경
secondary: '#1e293b', // Slate 800 - 카드
tertiary: '#334155', // Slate 700 - 보더/구분
},
// 텍스트 레벨
foreground: {
DEFAULT: '#f8fafc', // Slate 50 - 주요 텍스트
secondary: '#cbd5e1', // Slate 300 - 보조 텍스트
muted: '#64748b', // Slate 500 - 보조 텍스트
},
// 보더와 구분
border: '#334155',
input: '#475569',
ring: '#0ea5e9',
},
// 간격 시스템 - 8px 기준
spacing: {
'4.5': '1.125rem', // 18px
'13': '3.25rem', // 52px
},
// 폰트 시스템
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'], // 숫자 표시
},
// 애니메이션
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'flash-green': 'flashGreen 0.5s ease-out',
'flash-red': 'flashRed 0.5s ease-out',
},
keyframes: {
flashGreen: {
'0%, 100%': { backgroundColor: 'transparent' },
'50%': { backgroundColor: 'rgba(34, 197, 94, 0.2)' },
},
flashRed: {
'0%, 100%': { backgroundColor: 'transparent' },
'50%': { backgroundColor: 'rgba(239, 68, 68, 0.2)' },
},
},
},
},
plugins: [require('tailwindcss-animate')],
};
export default config;
CSS 변수와 다크 모드
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--card: 217 33% 17%;
--card-foreground: 210 40% 98%;
--popover: 222 47% 11%;
--popover-foreground: 210 40% 98%;
--primary: 199 89% 48%;
--primary-foreground: 0 0% 100%;
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--accent: 217 33% 17%;
--accent-foreground: 210 40% 98%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--border: 217 33% 25%;
--input: 217 33% 25%;
--ring: 199 89% 48%;
--radius: 0.5rem;
/* 거래 의미 */
--trade-up: 142 71% 45%;
--trade-down: 0 84% 60%;
--trade-warning: 38 92% 50%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
shadcn/ui 컴포넌트 커스터마이징
기본 컴포넌트 설치
# shadcn/ui 초기화
npx shadcn-ui@latest init
# 거래 인터페이스에 자주 사용하는 컴포넌트 설치
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add table
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add select
npx shadcn-ui@latest add input
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add skeleton
npx shadcn-ui@latest add tooltip
npx shadcn-ui@latest add toast
거래 전용 컴포넌트 커스터마이징
#### 가격 변동 표시 컴포넌트
// components/ui/price-change.tsx
import { cn } from '@/lib/utils';
import { ArrowUp, ArrowDown, Minus } from 'lucide-react';
interface PriceChangeProps {
value: number; // 변동값
percentage?: number; // 변동 퍼센트
showIcon?: boolean;
className?: string;
decimalPlaces?: number;
}
export function PriceChange({
value,
percentage,
showIcon = true,
className,
decimalPlaces = 2,
}: PriceChangeProps) {
const isPositive = value > 0;
const isNeutral = value === 0;
const colorClass = isPositive
? 'text-trade-up'
: isNeutral
? 'text-trade-neutral'
: 'text-trade-down';
const Icon = isPositive ? ArrowUp : isNeutral ? Minus : ArrowDown;
return (
<div className={cn('flex items-center gap-1 font-mono', colorClass, className)}>
{showIcon && <Icon className="h-4 w-4" />}
<span>
{isPositive ? '+' : ''}
{value.toFixed(decimalPlaces)}
{percentage !== undefined && (
<span className="ml-1 text-xs">
({isPositive ? '+' : ''}
{percentage.toFixed(decimalPlaces)}%)
</span>
)}
</span>
</div>
);
}
#### 거래 쌍 카드 컴포넌트
// components/features/trading-pair-card.tsx
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { PriceChange } from '@/components/ui/price-change';
import { Badge } from '@/components/ui/badge';
interface TradingPairCardProps {
symbol: string;
baseAsset: string;
quoteAsset: string;
price: number;
change24h: number;
change24hPercent: number;
volume24h: number;
high24h: number;
low24h: number;
isFavorite?: boolean;
onClick?: () => void;
}
export function TradingPairCard({
symbol,
baseAsset,
quoteAsset,
price,
change24h,
change24hPercent,
volume24h,
high24h,
low24h,
isFavorite,
onClick,
}: TradingPairCardProps) {
return (
<Card
className="cursor-pointer transition-all hover:border-primary/50 hover:shadow-lg"
onClick={onClick}
>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
<span className="text-xs font-bold text-primary">{baseAsset[0]}</span>
</div>
<div>
<h4 className="font-semibold">{baseAsset}/{quoteAsset}</h4>
<p className="text-xs text-muted-foreground">{symbol}</p>
</div>
</div>
{isFavorite && (
<Badge variant="secondary" className="h-6 w-6 p-0">
★
</Badge>
)}
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-baseline justify-between">
<span className="text-2xl font-bold font-mono">
{price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 8,
})}
</span>
<PriceChange
value={change24h}
percentage={change24hPercent}
/>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<div>
<span>24h 고: </span>
<span className="font-mono text-foreground">{high24h.toLocaleString()}</span>
</div>
<div>
<span>24h 저: </span>
<span className="font-mono text-foreground">{low24h.toLocaleString()}</span>
</div>
<div className="col-span-2">
<span>24h 거래량: </span>
<span className="font-mono text-foreground">
{(volume24h / 1e6).toFixed(2)}M
</span>
</div>
</div>
</CardContent>
</Card>
);
}
거래 대시보드 레이아웃 시스템
그리드 레이아웃 디자인
// components/layout/dashboard-grid.tsx
import { cn } from '@/lib/utils';
import { ReactNode } from 'react';
interface DashboardGridProps {
children: ReactNode;
className?: string;
}
export function DashboardGrid({ children, className }: DashboardGridProps) {
return (
<div className={cn(
'grid gap-4 p-4',
'grid-cols-1',
'md:grid-cols-2',
'lg:grid-cols-3',
'xl:grid-cols-4',
className
)}>
{children}
</div>
);
}
// 크기 조절 가능한 대시보드 그리드
interface ResizableGridProps {
layouts: Layout[];
children: ReactNode[];
}
export function ResizableDashboardGrid({ layouts, children }: ResizableGridProps) {
// react-grid-layout 사용
return (
<ResponsiveGridLayout
className="layout"
layouts={layouts}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
rowHeight={60}
draggableHandle=".drag-handle"
>
{children}
</ResponsiveGridLayout>
);
}
사이드바 네비게이션 디자인
// components/layout/sidebar.tsx
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
LayoutDashboard,
Bot,
TrendingUp,
BarChart3,
Settings,
Wallet
} from 'lucide-react';
const navItems = [
{ icon: LayoutDashboard, label: '대시보드', href: '/dashboard' },
{ icon: Bot, label: '봇', href: '/bots' },
{ icon: TrendingUp, label: '전략 마켓', href: '/strategies' },
{ icon: BarChart3, label: '분석', href: '/analysis' },
{ icon: Wallet, label: '자산', href: '/wallet' },
{ icon: Settings, label: '설정', href: '/settings' },
];
export function Sidebar() {
return (
<div className="flex h-full w-64 flex-col border-r bg-card">
<div className="flex h-16 items-center border-b px-6">
<Logo className="h-8 w-8" />
<span className="ml-3 text-lg font-bold">Sentinel</span>
</div>
<ScrollArea className="flex-1 py-4">
<nav className="space-y-1 px-3">
{navItems.map((item) => (
<NavLink
key={item.href}
to={item.href}
className={({ isActive }) =>
cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
)
}
>
<item.icon className="h-5 w-5" />
{item.label}
</NavLink>
))}
</nav>
</ScrollArea>
<div className="border-t p-4">
<UserProfile />
</div>
</div>
);
}
반응형 디자인 전략
브레이크포인트 시스템
// 디자인 시스템 브레이크포인트
const breakpoints = {
sm: '640px', // 휴대폰 가로
md: '768px', // 태블릿
lg: '1024px', // 소형 데스크톱
xl: '1280px', // 데스크톱
'2xl': '1536px', // 대형 화면
};
// 거래 인터페이스 전용 반응형 규칙
/*
- < 768px: 단일 열 레이아웃, 단순화된 차트
- 768px - 1024px: 이중 열 레이아웃
- 1024px - 1280px: 삼중 열 레이아웃
- > 1280px: 완전한 다중 열 레이아웃
*/
반응형 테이블
// components/ui/responsive-table.tsx
import { cn } from '@/lib/utils';
interface ResponsiveTableProps {
children: React.ReactNode;
className?: string;
}
export function ResponsiveTable({ children, className }: ResponsiveTableProps) {
return (
<div className={cn('w-full overflow-auto', className)}>
<table className="w-full caption-bottom text-sm">
{children}
</table>
</div>
);
}
// 모바일 카드 뷰
export function MobileCardView({ data, renderCard }: MobileCardViewProps) {
return (
<div className="grid gap-4 md:hidden">
{data.map((item, index) => (
<div key={index}>{renderCard(item)}</div>
))}
</div>
);
}
애니메이션과 미세 상호작용
가격 플래시 애니메이션
// hooks/usePriceFlash.ts
import { useState, useEffect } from 'react';
export function usePriceFlash(price: number) {
const [flash, setFlash] = useState<'up' | 'down' | null>(null);
const [prevPrice, setPrevPrice] = useState(price);
useEffect(() => {
if (price > prevPrice) {
setFlash('up');
} else if (price < prevPrice) {
setFlash('down');
}
setPrevPrice(price);
const timer = setTimeout(() => setFlash(null), 500);
return () => clearTimeout(timer);
}, [price]);
return flash;
}
// 사용
function PriceDisplay({ price }: { price: number }) {
const flash = usePriceFlash(price);
return (
<span
className={cn(
'font-mono text-2xl font-bold transition-colors',
flash === 'up' && 'animate-flash-green',
flash === 'down' && 'animate-flash-red'
)}
>
{price.toFixed(2)}
</span>
);
}
로딩 상태 스켈레톤
// components/ui/skeleton.tsx (shadcn/ui 내장)
import { Skeleton } from '@/components/ui/skeleton';
export function TradingPairCardSkeleton() {
return (
<Card>
<CardHeader className="flex flex-row items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[150px]" />
<Skeleton className="h-4 w-[100px]" />
</div>
</CardHeader>
<CardContent className="space-y-4">
<Skeleton className="h-8 w-[200px]" />
<div className="grid grid-cols-2 gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</CardContent>
</Card>
);
}
실전 사례: Sentinel Bot 디자인 시스템
디자인 원칙
Sentinel Design Principles:
├── 명확성 우선 (Clarity First)
│ └── 가격, 손익 등 핵심 데이터가 한눈에 보임
├── 효율성 지향 (Efficiency Oriented)
│ └── 작업 단계 감소, 일반 기능은 원클릭 완료
├── 전문적 질감 (Professional Aesthetic)
│ └── 다크 테마, 금융급 시각 품질
└── 즉각적인 피드백 (Instant Feedback)
└── 가격 변동, 작업 결과 즉시 시각적 피드백
컴포넌트 사용 통계
| 컴포넌트 유형 | 사용 횟수 | 커스터마이징 정도 |
|:---|:---:|:---:|
| Button | 120+ | 중간 (거래 의미 색상) |
| Card | 80+ | 높음 (다양한 변형) |
| Table | 25+ | 높음 (반응형, 정렬) |
| Dialog | 30+ | 중간 |
| Badge | 200+ | 높음 (상태 태그) |
| Chart | 15+ | 높음 (Recharts 래핑) |
자주 묻는 질문 FAQ
Q1: shadcn/ui와 Radix UI의 관계는 무엇인가요?
A: shadcn/ui는 Radix UI의 헤드리스 컴포넌트(headless)를 기반으로 하여 Tailwind 스타일을 추가합니다. 사용자는 다음을 얻습니다:
- Radix의 접근성과 키보드 네비게이션
- Tailwind의 유연한 스타일
- 완전히 커스터마이징 가능한 소스코드
Q2: shadcn/ui 컴포넌트는 어떻게 업데이트하나요?
A: 컴포넌트가 프로젝트에 있으므로 수동으로 업데이트해야 합니다:
# 특정 컴포넌트 재설치 (덮어쓰기)
npx shadcn-ui@latest add button -o
# 또는 공식 업데이트 수동 비교
# https://ui.shadcn.com/docs/components/button
Q3: Tailwind의 bundle이 크지 않나요?
A: 아닙니다. Tailwind는 PurgeCSS를 사용하여 실제로 사용된 클래스만 번들합니다. 프로덕션 환경에서 보통 < 10KB입니다.
Q4: 테마 전환은 어떻게 구현하나요?
A: shadcn/ui는 내장 지원:
// providers/theme-provider.tsx
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'dark' | 'light' | 'system';
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState<Theme>('dark');
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Q5: 금융 데이터의 특수 폰트 요구사항은 무엇인가요?
A: 숫자는 고정폭 폰트(monospace) 사용을 권장하여 정렬을 보장:
/* 가격 표시 */
.font-mono-tabular {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum';
}
Q6: 대량 데이터의 성능은 어떻게 처리하나요?
A: 조합 전략:
- 가상 스크롤링 (react-window)
- 페이지네이션 또는 무한 스크롤
- 데이터 사전 로딩과 캐싱
- 스켈레톤으로 지연 인지 감소
Q7: shadcn/ui가 대형 팀에 적합한가요?
A: 적합하지만 규칙을 세워야 합니다:
- 컴포넌트 커스터마이징 가이드
- 디자인 Token 문서
- Code Review 체크리스트
Q8: Figma 디자인과 어떻게 협업하나요?
A: 권장 프로세스:
- Figma 디자인 → 2. Tailwind 구성 동기화 → 3. shadcn/ui 컴포넌트 구현 → 4. 디자인 시스템 문서
결론 및 실행 권장 사항
shadcn/ui + Tailwind CSS는 현대 거래 인터페이스 개발의 최선의 조합입니다:
- ✅ 완전한 커스터마이징 제어
- ✅ 우수한 개발 경험
- ✅ 전문적인 시각 품질
- ✅ 가벼운 bundle 크기
즉시 실행
- [ ] Tailwind + shadcn/ui 초기화
- [ ] 디자인 Token 정의 (색상, 폰트, 간격)
- [ ] 기본 컴포넌트 설치 및 커스터마이징
- [ ] 거래 전용 컴포넌트 라이브러리 구축
- [ ] 반응형 레이아웃 시스템 구현
추가 읽기:
작성자: Sentinel Team
마지막 업데이트: 2026-03-04
디자인 검증: Sentinel Bot 실제 디자인 시스템 경험 기반
거래 인터페이스 디자인 시스템을 구축하고 있나요? Sentinel Bot의 shadcn/ui 기반 인터페이스를 지금 경험하거나, 디자인 시스템 템플릿을 다운로드하여 빠르게 시작하세요.
Sentinel Bot 무료 체험 | 디자인 시스템 템플릿 다운로드 | 디자인 컨설팅
관련 문서
동일 시리즈 추가 읽기
- React 18 거래 인터페이스 - React 생태계 통합
- TypeScript 5 타입 안전성 - 타입 안전 개발
교차 시리즈 추천
- 거래 인터페이스 심리 - UI/UX와 거래 심리