이 글은 누구를 위한 것인가
- 대시보드에 일관된 차트 스타일을 적용하려는 팀
- 접근성을 고려한 차트 색상 팔레트를 설계하는 디자이너/개발자
- Recharts 기반 커스텀 차트 컴포넌트를 만들려는 팀
들어가며
차트마다 색상이 다르고, 로딩 상태가 없고, 빈 데이터 처리가 없으면 대시보드가 조각보처럼 보인다. 차트 디자인 시스템은 색상, 타입, 인터랙션을 표준화해서 일관된 시각화를 제공한다.
이 글은 bluefoxdev.kr의 데이터 시각화 디자인 시스템 가이드 를 참고하여 작성했습니다.
1. 차트 디자인 원칙
[차트 타입 선택 가이드]
시간 흐름 추세: Line Chart
카테고리 비교: Bar Chart (수평: 긴 레이블)
부분/전체 비율: Pie/Donut (5개 이하)
두 변수 상관관계: Scatter Plot
단일 지표 강조: KPI Card + Sparkline
지리 분포: Map Chart
[접근성 색상 팔레트]
색맹 친화 팔레트 (8색):
#1f77b4 파랑 (기본)
#ff7f0e 주황
#2ca02c 초록
#d62728 빨강
#9467bd 보라
#8c564b 갈색
#e377c2 분홍
#7f7f7f 회색
대비 확인: 배경 대비 3:1 이상 (그래픽 요소)
패턴/형태 병용: 색상만으로 구분 금지
[반응형 전략]
모바일: 간소화 (레이블 축소, 범례 하단)
태블릿: 중간 밀도
데스크탑: 전체 인터랙션
ResizeObserver로 컨테이너 크기 감지
Recharts: <ResponsiveContainer width="100%" />
2. 차트 디자인 시스템 구현
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
// 디자인 시스템 토큰
const chartTokens = {
colors: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'],
fontFamily: 'Inter, sans-serif',
fontSize: { axis: 12, label: 11, tooltip: 13 },
grid: { stroke: '#e2e8f0', strokeDasharray: '3 3' },
tooltip: { borderRadius: 8, border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' },
};
// 커스텀 툴팁
function CustomTooltip({ active, payload, label, formatter }: any) {
if (!active || !payload?.length) return null;
return (
<div style={{ background: 'white', padding: '12px 16px', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}>
<p style={{ fontWeight: 600, marginBottom: 8, color: '#374151' }}>{label}</p>
{payload.map((entry: any, i: number) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, color: entry.color }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: entry.color, display: 'inline-block' }} />
<span style={{ color: '#6b7280', fontSize: 13 }}>{entry.name}:</span>
<span style={{ fontWeight: 600, color: '#111827' }}>
{formatter ? formatter(entry.value) : entry.value}
</span>
</div>
))}
</div>
);
}
// 기본 라인 차트 컴포넌트
interface LineChartProps {
data: any[];
lines: { dataKey: string; name: string }[];
xAxisKey: string;
formatter?: (value: number) => string;
height?: number;
isLoading?: boolean;
}
function SystemLineChart({ data, lines, xAxisKey, formatter, height = 300, isLoading }: LineChartProps) {
if (isLoading) return <ChartSkeleton height={height} />;
if (!data.length) return <ChartEmpty height={height} />;
return (
<ResponsiveContainer width="100%" height={height}>
<LineChart data={data} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
<CartesianGrid {...chartTokens.grid} />
<XAxis
dataKey={xAxisKey}
tick={{ fontSize: chartTokens.fontSize.axis, fontFamily: chartTokens.fontFamily }}
tickLine={false}
/>
<YAxis
tick={{ fontSize: chartTokens.fontSize.axis, fontFamily: chartTokens.fontFamily }}
axisLine={false}
tickLine={false}
tickFormatter={formatter}
/>
<Tooltip content={<CustomTooltip formatter={formatter} />} />
<Legend wrapperStyle={{ fontSize: 13, fontFamily: chartTokens.fontFamily }} />
{lines.map((line, i) => (
<Line
key={line.dataKey}
type="monotone"
dataKey={line.dataKey}
name={line.name}
stroke={chartTokens.colors[i % chartTokens.colors.length]}
strokeWidth={2}
dot={false}
activeDot={{ r: 4 }}
/>
))}
</LineChart>
</ResponsiveContainer>
);
}
function ChartSkeleton({ height }: { height: number }) {
return (
<div style={{ height, background: '#f1f5f9', borderRadius: 8, animation: 'pulse 1.5s infinite' }}
role="status" aria-label="차트 로딩 중" />
);
}
function ChartEmpty({ height }: { height: number }) {
return (
<div style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8' }}>
<div style={{ textAlign: 'center' }}>
<svg width={48} height={48} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<p style={{ marginTop: 8, fontSize: 14 }}>데이터가 없습니다</p>
</div>
</div>
);
}
마무리
차트 디자인 시스템의 핵심은 토큰화다. 색상, 폰트, 그리드 스타일을 chartTokens 객체로 중앙화하면 전체 대시보드가 한 번에 업데이트된다. 색맹 친화 팔레트 + 패턴 병용으로 접근성을 확보하고, 로딩/빈 상태 컴포넌트를 표준화해서 일관된 UX를 제공한다.