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 고려까지 더하면 프로덕션에서도 안정적인 동적 차트를 제공할 수 있습니다.
'React' 카테고리의 다른 글
| React에서 스크롤 위치 저장 및 복원 기능 구현하기 (1) | 2026.06.10 |
|---|---|
| React 앱에 서버리스 함수(Function) 연동하기 (0) | 2026.06.05 |
| React 컴포넌트에서 메모리 누수 디버깅하기 (0) | 2026.06.05 |
| React에서 스타일링을 위한 CSS Modules 활용하기 (0) | 2026.06.04 |
| React 앱에서 Web Share API 사용하여 콘텐츠 공유하기 (0) | 2026.06.04 |