한 5년전 쯤 까지만 해도 회사에서 데이터 시각화 개발을 담당했었습니다. 차트를 많이 그렸는데, 그리다 보니 Web Api를 사용해서 렌더링을 하다가 그 방식을 두 번 갈아엎게 된 경험입니다. SVG → Canvas → WebGL. 각 단계에서 왜 벽을 만났는지 적어봅니다. 지금 오픈소스로 유지 중인 @keneth80/k-chart가 그 결과물이라고 보시면 됩니다. (사실 데이터 시각화를 다루는 분들이 많지 않을거라 생각합니다.)
처음엔 SVG였습니다. boxplot 차트부터 시작해서 라인 차트 같은 시계열 데이터를 그렸습니다. SVG는 다루기 편하다는 장점이 있습니다. 점 하나가 요소 하나고, 스타일도 이벤트도 붙이기 매우 쉽습니다.
문제는 데이터가 커지면서 나왔다. 백만 건짜리 데이터를 그리니까 부하가 확 걸렸다는 것이죠. SVG는 데이터 하나당 DOM 요소 하나라, 요소가 백만 개가 되면 브라우저가 그걸 다 들고 있어야 합니다. 편해서 쓴 건데, 그 편함(요소=노드)이 그대로 한계가 됐습니다.
그래서 Canvas로 바꿨습니다. (그렇다고 SVG 형 차트를 버린건 아닙니다.)
Canvas는 접근이 다릅니다. 도화지에 그림을 그리고 끝나는 방식이라, DOM을 계속 들고 있지 않아 성능 측면에서 이점이 많습니다. 점이 만 개든 백만 개든 캔버스는 하나라는 거죠. SVG에서 DOM 개수에 눌리던 문제가 이걸로 풀렸습니다.
한동안 잘 썼는데, 데이터가 또 커졌습니다. 이번엔 천만 건 이상을 출력해야 하는 상황이 왔습니다. 그러니까 Canvas도 걸리는 게 있었습니다. Canvas는 결국 점 하나하나를 루프 돌면서 그리는 거라, 천만 건이면 그 루프에 시간이 걸린다는 겁니다. 이게 좀 걸렸습니다.
아무리 고민해도 api를 호출하는 루프 시간은 줄일 수 없었거든요.
그래서 찾아봤습니다. 브라우저 내장 기능이면서 퍼포먼스가 좋은 게 없나 하고요. 그러다 WebGL을 발견했습니다.
적용해 봤더니 Canvas보다 속도가 더 빠르고 쾌적했습니다. WebGL은 좌표 데이터를 버퍼에 통째로 올려서 GPU에 넘깁니다. Canvas 2D가 CPU로 점을 하나씩 루프 돌며 그렸다면, WebGL은 버퍼에 한 번 올리고 draw call 한 번으로 끝낸다는 것이죠. 나머진 GPU가 병렬로 처리한다는 겁니다.
실제 코드는 이런 식입니다.
// Canvas 2D — 점을 하나씩 루프로 그린다
data.forEach((point) => {
context.lineTo(x(point), y(point));
});
// 천만 건이면 천만 번, 매 프레임마다
// WebGL — 좌표를 버퍼 하나로 GPU에 올린다
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.drawArrays(gl.LINE_STRIP, 0, vertices.length / 2); // draw call 한 번
물론 WebGL은 SVG의 편함을 잃는다고 할 수 있습니다. 셰이더를 다뤄야 하고 좌표도 직접 변환해야 한다. 얻는 게 있으면 잃는 게 있습니다.
세 번 겪고 나서 든 생각은, 하나만 고를 게 아니라 데이터 유형과 성격, 용량에 따라 세 개를 섞어 써야겠다는 거였습니다.
문제는 세 기술이 다르다 보니 하나의 차트로 합치기가 너무 어려웠습니다. SVG로 그린 축 위에 WebGL 라인을 얹는 걸 어떻게 한 덩어리로 만드나. 이걸 고민하다가 아키텍처를 짜게 됐습니다. (이 고민을 수년간 해옵니다.)
처음엔 TypeScript class 기반으로 하이브리드 차트를 만들었습니다. 축·scale 계산은 코어가 하고, 그리기는 SVG / Canvas / WebGL 중 골라서. 그런데 class 방식에서 메모리가 늘고 확장성이 떨어지는 문제가 생깁니다. 그래서 그걸 해결하려고 함수형으로 마이그레이션을 진행했습니다. 그 결과물이 지금의 k-chart 입니다.
돌아보면 각 전환이 다 "편함"과 "규모"를 맞바꾸는 거였습니다.
SVG는 제일 편한데 규모에서 무너지고, WebGL은 규모를 감당하는 대신 편함을 잃는 거죠. 어느 하나가 정답이 아니라 데이터가 어느 규모냐에 따라 답이 달라진다는 겁니다. 그래서 하나 고르는 대신 셋을 다 두고, 코어가 그 위에서 같은 좌표계를 공유하게 만들었습니다. class에서 함수형으로 갈아엎은 것도 확장성은 더 높게 구조를 최대한 유지하려는 거였습니다.
차트를 쓰는 입장에선 "빠른가?"만 보이는데, 만드는 입장에선 "어느 규모까지 편함을 유지할 수 있나"의 고민의 연속이었습니다. 그 경계를 세 번 넘으면서 지금 형태가 됐습니다. (뿌듯합니다.)
회사에서 데이터 시각화를 담당하며 만들기 시작해 지금은 오픈소스로 유지 중인 @keneth80/k-chart 이야기입니다. 요즘은 관심사 커뮤니티(galaxyconstellate.com)를 만들며 이런 개발 기록을 남기고 있습니다.