본문 바로가기

React

React에서 Chart.js를 사용한 동적 차트 생성

React에서 Chart.js를 활용하면 실시간 데이터 스트림, 사용자 상호작용, 반응형 레이아웃까지 손쉽게 구현할 수 있습니다. 이 글은 react-chartjs-2를 기반으로 동적(실시간) 차트를 만드는 실무형 예제와 성능/UX 최적화 팁을 제공합니다.

1. 설치와 기본 셋업

Chart.js와 React 바인딩 라이브러리인 react-chartjs-2를 설치합니다.

// 설치
npm i chart.js react-chartjs-2
// (옵션) 시간 축, 줌/팬이 필요하다면
npm i chartjs-adapter-date-fns chartjs-plugin-zoom

필요한 스케일/플러그인을 등록합니다. 프로젝트 초기에 한 번만 등록하면 됩니다.

// chartjs-setup.js
import { Chart as ChartJS,
  CategoryScale, LinearScale, PointElement, LineElement, BarElement,
  TimeScale, Tooltip, Legend, Title, Filler, Decimation } from 'chart.js';
import 'chartjs-adapter-date-fns';

ChartJS.register(
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  TimeScale, // 시간축 사용 시
  Tooltip,
  Legend,
  Title,
  Filler, // 그라디언트/영역 채움
  Decimation // 대용량 데이터 다운샘플링
);

export default ChartJS;

2. 최소 동작 LineChart 컴포넌트

메모이제이션된 data/options로 불필요한 리렌더를 줄입니다.

// LineChart.jsx
import React, { useMemo, forwardRef } from 'react';
import { Line } from 'react-chartjs-2';
import './chartjs-setup';

const LineChart = forwardRef(function LineChart({ labels, points }, ref) {
  const data = useMemo(() => ({
    labels,
    datasets: [
      {
        label: '실시간 값',
        data: points,
        borderColor: 'rgb(75, 192, 192)',
        pointRadius: 0,
        tension: 0.3,
        fill: true,
        backgroundColor: (ctx) => {
          const { chart } = ctx;
          const { ctx: c, chartArea } = chart || {};
          if (!chartArea) return 'rgba(75, 192, 192, 0.2)';
          const g = c.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
          g.addColorStop(0, 'rgba(75, 192, 192, 0)');
          g.addColorStop(1, 'rgba(75, 192, 192, 0.35)');
          return g;
        }
      }
    ]
  }), [labels, points]);

  const options = useMemo(() => ({
    responsive: true,
    animation: false,
    maintainAspectRatio: false,
    plugins: {
      legend: { display: true },
      tooltip: { intersect: false, mode: 'index' },
      title: { display: false }
    },
    scales: {
      x: { display: true },
      y: { display: true, beginAtZero: true }
    }
  }), []);

  return (
    <div style={{ height: 280 }}>
      <Line ref={ref} data={data} options={options} />
    </div>
  );
});

export default LineChart;

3. 상태 기반 동적 업데이트

setInterval로 매초 데이터가 들어오는 상황을 시뮬레이션합니다. 최대 길이를 제한해 메모리 증가를 방지합니다.

// DynamicLineDemo.jsx
import React, { useEffect, useState } from 'react';
import LineChart from './LineChart';

export default function DynamicLineDemo() {
  const [labels, setLabels] = useState([]);
  const [points, setPoints] = useState([]);

  useEffect(() => {
    const id = setInterval(() => {
      const now = new Date().toLocaleTimeString();
      const value = Math.round(Math.random() * 100);
      setLabels((prev) => [...prev.slice(-19), now]); // 최대 20개 유지
      setPoints((prev) => [...prev.slice(-19), value]);
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <LineChart labels={labels} points={points} />;
}

4. 실시간 스트림(WebSocket) 연동

서버에서 오는 이벤트를 그대로 차트에 반영합니다. 컴포넌트 언마운트 시 소켓을 닫아 리소스를 정리합니다.

// LiveSocketChart.jsx
import React, { useEffect, useRef, useState } from 'react';
import LineChart from './LineChart';

export default function LiveSocketChart() {
  const socketRef = useRef(null);
  const [labels, setLabels] = useState([]);
  const [points, setPoints] = useState([]);

  useEffect(() => {
    socketRef.current = new WebSocket('wss://example.com/stream');
    socketRef.current.onmessage = (e) => {
      const { t, v } = JSON.parse(e.data); // t: 타임스탬프, v: 값
      setLabels((prev) => [...prev.slice(-99), t]);
      setPoints((prev) => [...prev.slice(-99), v]);
    };
    socketRef.current.onerror = console.error;
    return () => socketRef.current?.close();
  }, []);

  return <LineChart labels={labels} points={points} />;
}

5. 성능 최적화 팁(대용량/고빈도)

렌더 횟수와 데이터 크기를 관리하면 프레임 드랍을 줄일 수 있습니다.

// 차트 인스턴스에 직접 접근해 배치 업데이트
import React, { useRef, useCallback } from 'react';
import LineChart from './LineChart';

export default function ImperativeUpdate() {
  const chartRef = useRef(null);

  const addPoint = useCallback((x, y) => {
    const chart = chartRef.current;
    if (!chart) return;
    chart.data.labels.push(x);
    chart.data.datasets[0].data.push(y);
    if (chart.data.labels.length > 1000) {
      chart.data.labels.shift();
      chart.data.datasets[0].data.shift();
    }
    chart.update('none'); // 애니메이션 없이 빠른 갱신
  }, []);

  // 예: 외부에서 addPoint를 호출하는 로직 연결
  return <LineChart ref={chartRef} labels={[]} points={[]} />;
}

추가 팁:

  • Decimation 플러그인 활성화: 수천~수만 포인트에서 CPU 사용량 감소
  • animation: false, pointRadius: 0로 렌더 비용 절감
  • 업데이트 주기 제한(throttle/debounce)로 불필요한 갱신 억제
  • React Strict Mode에서 개발 시 이펙트가 두 번 실행됨을 고려

6. 시간 축과 반응형 설정

시계열 데이터는 TimeScale을 사용하면 확대/가독성이 좋아집니다.

const options = {
  responsive: true,
  maintainAspectRatio: false,
  scales: {
    x: {
      type: 'time',
      time: { unit: 'minute' }
    },
    y: { beginAtZero: true }
  }
};

컨테이너 높이를 고정하거나 CSS로 부모 크기에 맞춰 반응형으로 동작하게 합니다.

// 예: CSS
.chart-wrap { height: 300px; }
// JSX
<div className="chart-wrap"><Line ... /></div>

7. 상호작용(툴팁/범례/줌)

툴팁/범례는 기본 플러그인으로 충분합니다. 추가로 줌/팬은 chartjs-plugin-zoom을 사용합니다.

import zoomPlugin from 'chartjs-plugin-zoom';
import { Chart as ChartJS } from 'chart.js';
ChartJS.register(zoomPlugin);

const options = {
  plugins: {
    zoom: {
      pan: { enabled: true, mode: 'x' },
      zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: 'x' }
    }
  }
};

8. Next.js(SSR)에서의 주의점

Canvas 기반이라 SSR과 맞지 않습니다. 클라이언트 컴포넌트로 분리하세요.

// app/컴포넌트/ChartClient.jsx
'use client';
import LineChart from './LineChart';
export default function ChartClient(props) { return <LineChart {...props} />; }

// 또는 동적 임포트
import dynamic from 'next/dynamic';
const Line = dynamic(() => import('react-chartjs-2').then(m => m.Line), { ssr: false });

9. 접근성/SEO 체크리스트

  • 캔버스는 시맨틱 정보가 부족합니다. figure/figcaption으로 맥락 제공
  • aria-label로 차트 의미를 전달
  • 차트 데이터 요약 텍스트를 함께 제공하여 AEO 개선
<figure aria-label="실시간 지표 라인 차트">
  <Line ... />
  <figcaption>최근 20초 동안의 값 추이입니다.</figcaption>
</figure>

10. 마무리

react-chartjs-2와 Chart.js를 결합하면 실시간 데이터 반영, 반응형, 상호작용까지 짧은 코드로 구현 가능합니다. 데이터 길이 제한, 배치 업데이트, 플러그인 활용으로 성능을 챙기고, 접근성/SSR 고려까지 더하면 프로덕션에서도 안정적인 동적 차트를 제공할 수 있습니다.