HTML5 캔버스는 브라우저에서 2D 도형, 이미지, 텍스트를 직접 그릴 수 있는 강력한 도구입니다. AG Grid의 자바스크립트 차트 라이브러리인 AG Charts는 대규모 데이터(10만 건 이상)를 효율적으로 처리하기 위해 캔버스를 선택하였으며, 다양한 최적화 기법을 적용하여 성능을 극대화했습니다. 이번 분석에서는 AG Charts가 캔버스 렌더링 성능을 최적화하기 위해 사용한 주요 기법들을 자세히 살펴보겠습니다.
HTML5 캔버스의 이해
HTML5 캔버스는 `<canvas>` 요소를 통해 다양한 그래픽을 그릴 수 있으며, 주로 2D 렌더링 컨텍스트(`getContext(“2d”)`)를 사용합니다. AG Charts는 3D 렌더링을 사용하지 않고 2D 렌더링에 집중하여 성능을 최적화했습니다. 캔버스는 도형, 선, 호, 이미지, 텍스트 등을 그리기 위한 다양한 메서드를 제공하며, 이를 통해 복잡한 그래픽을 효율적으로 렌더링할 수 있습니다.
기본 캔버스 예제
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// 현재 상태 저장
ctx.save();
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100);
// 가장 최근에 save() 메서드로 저장한 상태로 되돌리기
ctx.restore();
ctx.fillRect(150, 40, 100, 100);
위 예제는 캔버스에 두 개의 녹색 사각형을 그리는 간단한 코드로, `save()`와 `restore()` 메서드를 사용하여 상태를 관리하는 방법을 보여줍니다.
캔버스 렌더링의 주요 과제
캔버스 렌더링 시 `beginPath`, `arc`, `fill`, `stroke` 등의 메서드가 반복적으로 호출되면 성능 저하가 발생할 수 있습니다. 특히 대규모 데이터셋을 처리할 때 메서드 호출이 프레임당 수천 번 또는 수십만 번 일어나면, 메인 스레드의 부하가 커져 사용자가 체감할 수 있는 성능 저하가 발생합니다.
예를 들어, 10만 개의 데이터 포인트를 렌더링할 때 각 메서드를 개별적으로 호출하면 선형 시간 복잡도(O(n))로 인해 렌더링 속도가 느려집니다. 실제로 크롬에서 10만 개의 데이터 포인트를 렌더링하는 데 약 287.1ms가 소요되었습니다.
성능 최적화 기법
1. 배치 렌더링 (Batch Rendering)
배치 렌더링은 여러 그리기 호출을 하나의 그룹으로 묶어 한 번에 실행하는 최적화 기법입니다. 이를 통해 개별 호출의 오버헤드를 줄이고 렌더링 시간을 단축할 수 있습니다.
export const drawBatched = ({ ctx, data, size, fill, stroke }) => {
ctx.save();
const r = size / 2;
ctx.beginPath(); // 한 번 호출
data.forEach(({ x, y }) => {
ctx.moveTo(x + r, y);
ctx.arc(x, y, r, 0, Math.PI * 2);
});
// 스타일 상태 변경 사항 적용
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 2;
// 경로를 한 번에 일괄 처리하여 그리기
ctx.fill();
ctx.stroke();
ctx.restore();
};
배치 렌더링을 적용하면 10만 개의 데이터 포인트 렌더링 시간을 287.1ms에서 15.4ms로 크게 줄일 수 있습니다. 단점으로는 여러 도형이 겹칠 경우 “뭉개짐” 효과가 발생할 수 있습니다.
2. 오프스크린 캔버스 API를 활용한 스프라이트 렌더링
오프스크린 캔버스는 메모리 내에서만 렌더링되는 캔버스를 제공하며, DOM에 종속되지 않고 독립적으로 동작할 수 있습니다. 이를 통해 렌더링 작업을 워커에서 처리하여 메인 스레드의 부하를 줄일 수 있습니다.
export const drawOffscreen = ({ canvasCtx, offscreenCanvasCtx, data }) => {
canvasCtx.save();
offscreenCanvasCtx.fillStyle = fill;
offscreenCanvasCtx.strokeStyle = stroke;
offscreenCanvasCtx.lineWidth = strokeWidth;
const center = (size + strokeWidth) / 2;
const r = size / 2;
const sSize = size + strokeWidth;
// 한 번 호출
offscreenCanvasCtx.beginPath();
offscreenCanvasCtx.arc(center, center, r, 0, Math.PI * 2);
offscreenCanvasCtx.fill();
offscreenCanvasCtx.stroke();
// 각 데이터 포인트에 대해 오프스크린 캔버스의 내용을 메인 캔버스에 그리기
data.forEach(({ x, y }) => {
canvasCtx.drawImage(
offscreenCanvasCtx,
x - center,
y - center,
sSize,
sSize
);
});
canvasCtx.restore();
};
이 방법을 적용하면 10만 개의 데이터 포인트 렌더링 시간을 65.7ms로 줄일 수 있습니다. 배치 렌더링보다 약간의 성능 비용이 발생하지만, 결과물의 품질에는 영향을 미치지 않습니다.
3. 변경 감지 (Change Detection)
AG Charts는 트리 기반 장면(scene) 그래프를 사용하여 변경된 노드만 다시 그리는 방식으로 성능을 최적화했습니다. `dirty` 플래그를 통해 변경된 속성이 있는 노드와 관련된 부모 그룹을 식별하고, 해당 부분만 재렌더링함으로써 불필요한 그리기 작업을 최소화했습니다.
이 접근 방식은 전체 렌더링 성능을 크게 향상시키며, 필요한 부분만 효율적으로 다시 그릴 수 있게 합니다.
성능 비교 및 요약
아래 표는 10만 개의 데이터 포인트를 캔버스에 그릴 때 각 최적화 방식의 성능 차이를 보여줍니다:
렌더링 방식 | 렌더링 시간 (ms) |
---|---|
개별 렌더링
|
287.1
|
배치 렌더링
|
15.4
|
오프스크린 렌더링
|
65.7
|
배치 렌더링이 가장 빠르지만, 스프라이트 렌더링과 변경 감지를 결합하면 성능과 품질의 균형을 맞출 수 있습니다.
결론
HTML5 캔버스의 성능 최적화는 AG Charts와 같은 고성능 차트 라이브러리에서 필수적입니다. 배치 렌더링, 오프스크린 캔버스, 변경 감지 등의 기법을 통해 대규모 데이터를 효율적으로 처리할 수 있으며, 상황에 따라 적절한 최적화 방식을 선택하는 것이 중요합니다. 이러한 최적화 과정을 통해 AG Charts는 높은 성능과 품질을 유지하며 사용자에게 우수한 시각화 경험을 제공합니다.
참고 자료: AG Grid, “Optimising HTML5 Canvas Rendering: Best Practices and Techniques”