stack-analysis
CHAPTER 65 / 90
읽기 약 2분
FUNCTION
대시보드: Recharts + 실시간 데이터
핵심 개념
Recharts·Tremor·실시간 업데이트·드릴다운 — 분석 대시보드 구축.
본문
Recharts vs 대안
| 라이브러리 | 번들 | 학습 | 적합 |
|---|---|---|---|
| Recharts | 90KB | ⭐⭐ | 일반 차트 |
| Tremor | 200KB | ⭐ | 대시보드 통합 |
| Chart.js | 80KB | ⭐⭐ | 단순 |
| Visx | 가변 | ⭐⭐⭐⭐ | 커스텀 |
| ECharts | 1MB | ⭐⭐⭐ | 복잡 비주얼 |
결론:
- 일반 대시보드 → Recharts
- 빠른 셋업 → Tremor
- 픽셀 단위 커스텀 → VisxRecharts 기본
import {
LineChart, Line, BarChart, Bar,
XAxis, YAxis, CartesianGrid, Tooltip, Legend,
ResponsiveContainer,
} from 'recharts';
function RevenueChart({ data }: { data: { date: string; revenue: number; orders: number }[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey="date" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<Tooltip
formatter={(v: number, name: string) => {
if (name === 'revenue') return [`₩${v.toLocaleString()}`, '매출'];
return [v, '주문 수'];
}}
/>
<Legend />
<Line
yAxisId="left"
type="monotone"
dataKey="revenue"
stroke="#6366f1"
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="orders"
stroke="#10b981"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
);
}Tremor — 대시보드 카드
import { Card, Metric, Text, ProgressBar, Flex } from '@tremor/react';
function StatsCard({ label, value, target, change }: any) {
const pct = (value / target) * 100;
return (
<Card>
<Text>{label}</Text>
<Metric>{value.toLocaleString()}</Metric>
<Flex className="mt-4">
<Text className="truncate">{pct.toFixed(0)}% of target</Text>
<Text>{target.toLocaleString()}</Text>
</Flex>
<ProgressBar value={pct} className="mt-2" />
<Text className={`mt-2 ${change > 0 ? 'text-green-600' : 'text-red-600'}`}>
{change > 0 ? '↑' : '↓'} {Math.abs(change)}%
</Text>
</Card>
);
}
function Dashboard() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatsCard label="MRR" value={45230} target={50000} change={12.5} />
<StatsCard label="신규 가입" value={234} target={300} change={-5.2} />
<StatsCard label="해지율" value={2.3} target={3.0} change={-0.5} />
</div>
);
}실시간 업데이트 (TanStack Query + 폴링)
import { useQuery } from '@tanstack/react-query';
function LiveDashboard() {
const { data } = useQuery({
queryKey: ['dashboard', 'today'],
queryFn: async () => {
const res = await fetch('/api/dashboard/today');
return res.json();
},
refetchInterval: 5000, // 5초마다 갱신
refetchOnWindowFocus: true,
});
return (
<>
<RevenueChart data={data?.revenue ?? []} />
<RealtimeOrders count={data?.activeOrders ?? 0} />
</>
);
}실시간 (WebSocket)
function RealtimeChart() {
const [points, setPoints] = useState<DataPoint[]>([]);
useEffect(() => {
const channel = supabase.channel('events:dashboard')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'orders',
}, (payload) => {
setPoints(prev => [...prev.slice(-49), {
time: new Date(),
value: payload.new.amount,
}]);
})
.subscribe();
return () => { supabase.removeChannel(channel); };
}, []);
return (
<LineChart data={points} width={600} height={300}>
<Line dataKey="value" stroke="#6366f1" isAnimationActive={false} />
</LineChart>
);
}드릴다운 (클릭 → 상세)
function DrillDownChart() {
const [period, setPeriod] = useState<{ year: number; month?: number; day?: number }>({
year: 2026,
});
const { data } = useQuery({
queryKey: ['revenue', period],
queryFn: () => fetch(`/api/revenue?${new URLSearchParams(period as any)}`).then(r => r.json()),
});
const handleClick = (entry: any) => {
if (!period.month) {
setPeriod({ ...period, month: entry.month });
} else if (!period.day) {
setPeriod({ ...period, day: entry.day });
}
};
return (
<>
<Breadcrumb>
<button onClick={() => setPeriod({ year: period.year })}>{period.year}</button>
{period.month && <span> / {period.month}월</span>}
{period.day && <span> / {period.day}일</span>}
</Breadcrumb>
<BarChart data={data} onClick={handleClick}>
<Bar dataKey="revenue" fill="#6366f1" cursor="pointer" />
</BarChart>
</>
);
}CSV 내보내기
function exportCSV(data: any[], filename: string) {
const headers = Object.keys(data[0]);
const rows = data.map(row =>
headers.map(h => `"${String(row[h]).replace(/"/g, '""')}"`).join(',')
);
const csv = [headers.join(','), ...rows].join('\n');
const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv' }); // BOM for Excel
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}다음 챕터
CH.66 "검색: 전문 검색(Full-text) + 필터링".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 프로젝트의 대시보드 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
대시보드 관련 실제 서비스 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스에서 대시보드 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 풀스택 시장의 대시보드 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
대시보드: Recharts + 실시간 데이터는 이 3가지만 확실히 잡으세요
1.Recharts = 일반 차트 표준 — 90KB·SVG·SSR 호환
2.Tremor로 대시보드 카드·메트릭 빠른 셋업
3.TanStack Query + refetchInterval = 폴링, WebSocket = 실시간 푸시
공유하기
진행도 65 / 90