데이터 시각화 차트 디자인 시스템: 일관된 차트 UI 구축

디자인

데이터 시각화차트 디자인디자인 시스템Recharts접근성

이 글은 누구를 위한 것인가

  • 대시보드에 일관된 차트 스타일을 적용하려는 팀
  • 접근성을 고려한 차트 색상 팔레트를 설계하는 디자이너/개발자
  • 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를 제공한다.