Canvas API 기본 이해
HTML5 Canvas는 JavaScript를 통해 2D 그래픽을 직접 그릴 수 있는 래스터 기반 요소입니다. Chart.js나 D3 같은 라이브러리 없이도 Canvas API만으로 충분히 인터랙티브한 차트를 만들 수 있습니다. Canvas의 좌표 체계와 렌더링 사이클을 이해하면 자유도 높은 시각화를 구현할 수 있습니다.
Canvas 초기 설정
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
// 고해상도 디스플레이 대응
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
ctx.scale(dpr, dpr);
// 차트 영역 정의
const padding = { top: 40, right: 30, bottom: 60, left: 70 };
const chartWidth = canvas.clientWidth - padding.left - padding.right;
const chartHeight = canvas.clientHeight - padding.top - padding.bottom;
바 차트 그리기
바 차트는 카테고리별 값을 비교하는 데 효과적입니다. X축에 카테고리, Y축에 값을 매핑하고 requestAnimationFrame을 활용하면 애니메이션까지 구현할 수 있습니다.
function drawBarChart(data) {
const maxVal = Math.max(...data.map(d => d.value));
const barWidth = chartWidth / data.length * 0.7;
const gap = chartWidth / data.length * 0.3;
// Y축 그리드 라인
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 0.5;
for (let i = 0; i <= 5; i++) {
const y = padding.top + (chartHeight / 5) * i;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(padding.left + chartWidth, y);
ctx.stroke();
}
// 바 그리기
data.forEach((item, i) => {
const x = padding.left + i * (barWidth + gap) + gap / 2;
const barHeight = (item.value / maxVal) * chartHeight;
const y = padding.top + chartHeight - barHeight;
// 그라디언트 적용
const gradient = ctx.createLinearGradient(x, y, x, y + barHeight);
gradient.addColorStop(0, '#4F46E5');
gradient.addColorStop(1, '#7C3AED');
ctx.fillStyle = gradient;
ctx.fillRect(x, y, barWidth, barHeight);
// 라벨
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(item.label, x + barWidth / 2, padding.top + chartHeight + 20);
});
}
마우스 인터랙션 추가
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 어떤 바 위에 있는지 확인
const hoveredBar = data.findIndex((item, i) => {
const x = padding.left + i * (barWidth + gap) + gap / 2;
const barHeight = (item.value / maxVal) * chartHeight;
const y = padding.top + chartHeight - barHeight;
return mouseX >= x && mouseX <= x + barWidth
&& mouseY >= y && mouseY <= y + chartHeight;
});
if (hoveredBar !== -1) {
canvas.style.cursor = 'pointer';
showTooltip(mouseX, mouseY, data[hoveredBar]);
} else {
canvas.style.cursor = 'default';
hideTooltip();
}
});
라인 차트와 애니메이션
라인 차트에서는 점 사이를 곡선으로 잇는 Bezier 커브와 requestAnimationFrame 기반 애니메이션이 핵심입니다.
function animateLineChart(data, progress = 0) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawAxes();
const visiblePoints = Math.floor(data.length * progress);
ctx.beginPath();
ctx.strokeStyle = '#10B981';
ctx.lineWidth = 2;
for (let i = 0; i < visiblePoints; i++) {
const x = padding.left + (i / (data.length - 1)) * chartWidth;
const y = padding.top + chartHeight - (data[i] / maxVal) * chartHeight;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
if (progress < 1) {
requestAnimationFrame(() => animateLineChart(data, progress + 0.02));
}
}
성능 최적화 팁
| 기법 | 설명 | 효과 |
|---|---|---|
| Off-screen Canvas | 정적 요소를 별도 Canvas에 미리 렌더링 | CPU 부하 50% 감소 |
| Dirty Region | 변경된 영역만 다시 그리기 | 대규모 차트에서 필수 |
| Path2D 캐싱 | 반복 그려지는 도형을 Path2D 객체로 캐싱 | hitTest 성능 향상 |
| requestAnimationFrame | setInterval 대신 rAF 사용 | 프레임 드롭 방지 |
반응형 리사이즈 처리
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
redrawChart();
}
});
resizeObserver.observe(canvas.parentElement);
Canvas API를 직접 다루면 번들 사이즈를 크게 줄이면서도 완전한 커스터마이징이 가능합니다. 프로덕션에서는 접근성을 위해 ARIA 속성과 대체 데이터 테이블을 함께 제공하는 것을 권장합니다.
댓글 0