KChart 개발 (Codex)

차트 그리느라 화면이 멈추길래, 그리기를 다른 스레드로 옮긴 이야기

Kenneth Kang · 공유 · 23시간 전 · 수정됨

차트 그리느라 화면이 멈추길래, 그리기를 다른 스레드로 옮긴 이야기

지난번에 대용량 데이터를 다운샘플링으로 줄인 이야기를 썼는데, 그 후속입니다. 데이터를 줄여도 결국 그리는 작업 자체가 무거우면 화면이 버벅이더라고요. 그래서 그리기를 아예 다른 스레드로 옮겼습니다. 지금 오픈소스로 유지 중인 @keneth80/k-chart 얘기입니다.

문제: 그리기가 UI를 붙잡는다

브라우저의 메인 스레드는 화면의 JavaScript 실행과 사용자 상호작용을 다 처리합니다. 클릭, 스크롤, 입력 같은 게 전부 여기서 돌죠. 그런데 여기에 무거운 차트 그리기까지 얹히면, 그리는 동안 다른 UI 처리가 지연됩니다. 차트가 그려지는 몇백 밀리초 동안 화면이 굳는 거죠.

데이터를 다운샘플링으로 줄여도 이건 완전히 안 사라졌습니다. 줄인 데이터라도 그리는 작업 자체가 메인 스레드를 점유하니까요. 이때, 구조적인 문제때문에 Codex도 Claude Code도 어찌하질 못하더라구요.

해결: 그리기를 워커로 옮긴다

그래서 Web Worker + OffscreenCanvas를 썼습니다. 그리기 작업을 메인 스레드가 아니라 별도 스레드에서 하게 한 겁니다. 원래는 대용량 데이터 처리를 워커에서 처리하게 하려고 했는데, 오히려 반대로 적용해보리고 한거죠.

역할을 이렇게 나눴습니다.

  • 메인 스레드: 축·범례·툴팁처럼 상호작용이 중요한 것 + 데이터 준비(다운샘플링, 좌표 변환, 버퍼 생성)
  • 워커: Canvas/WebGL로 선을 실제로 그리는 것

흐름으로 보면 이렇습니다.

메인 스레드
  원본 데이터
      ↓
  축·scale 등 차트 구성
      ↓
  LTTB / Min-Max 다운샘플링 (활성화한 경우)
      ↓
  데이터 값을 화면 좌표로 변환
      ↓
  Float32Array 생성
      ↓
  워커에 좌표 버퍼 전달
      │
      ▼
웹워커 + OffscreenCanvas
  Canvas 2D: 선 경로를 만들고 stroke()
  WebGL: 버퍼 업로드 및 drawArrays()
      ↓
  완료 메시지를 메인 스레드로 반환

메인은 "무엇을 그릴지"까지 계산해서 좌표 버퍼를 넘기고, 워커는 그 버퍼로 "그리기"만 합니다. 그리는 동안 메인 스레드는 자유로우니, 사용자가 스크롤하든 클릭하든 화면이 안 멈춥니다.

핵심: "빨라진다"가 아니라 "안 멈춘다"

여기서 오해하기 쉬운 게 있습니다. 워커를 쓰면 무조건 빨라진다고 생각하는데, 그게 아닙니다. (체감속도는 어느정도 개선이 되긴 했습니다.)

목적은 **"총 처리 시간을 무조건 줄인다"가 아니라 "무거운 그리기가 UI 스레드를 오래 점유하지 않게 한다"**입니다. 오히려 메인과 워커 사이에 데이터를 주고받는 비용(전송·전처리)이 있어서, 데이터가 작으면 전체 시간은 비슷하거나 살짝 늘 수도 있습니다.

그래도 쓰는 이유는, 사용자 체감이 다르기 때문입니다. 총 500ms가 걸리더라도 그게 메인 스레드를 다 막으면 화면이 500ms 굳지만, 워커로 돌리면 그 500ms 동안 화면은 멀쩡히 반응합니다. 전자는 "멈췄다", 후자는 "그려지고 있다"로 느껴지죠.

쓰는 쪽에서는

k-chart에서는 이걸 옵션으로 뒀습니다. downsample(데이터 축약)과 asyncRender(워커 그리기)는 서로 독립적인 설정입니다.

const series = createWebglLineSeries<Point>({
  selector: 'signal',
  xField: 'x',
  yField: 'signal',

  // 메인 스레드에서 렌더링할 데이터 수를 줄임
  downsample: true,

  // 실제 선 그리기는 워커에 위임
  asyncRender: {
    enabled: true,
    workerFactory: () =>
      new Worker(
        new URL('./kchart-render.worker.ts', import.meta.url),
        { type: 'module' },
      ),
  },
});

워커 진입 파일은 짧습니다.

// kchart-render.worker.ts
import { startKChartRenderWorker } from '@keneth80/k-chart';

startKChartRenderWorker();

정리하면

지난 글의 다운샘플링과 이번 워커는 목적이 다릅니다.

  • 다운샘플링 = 그릴 점을 줄이는 것 (양을 줄임)
  • 웹 워커 = 그리기 작업을 UI 스레드 밖으로 옮기는 것 (위치를 옮김)

둘은 독립적이라 따로 켜고 끌 수 있습니다. 데이터가 많으면 다운샘플링으로 양을 줄이고, 그리기가 UI를 막으면 워커로 위치를 옮기고. 대용량 차트에서 "빠르다"는 결국 이 둘을 상황에 맞게 조합하는 문제였습니다. 이번 기능을 적용하면서 성능을 위해서는 차트의 옵션이 어쩔 수 없이 늘어나게 되는 것을 감수해야하는 구나 라는 생각이 들었습니다. 다음 개선은 차트를 어떻게 하면 쉽게 가져다 쓸 수 있을지 고민해봐야 겠습니다.


회사에서 데이터 시각화를 담당하며 만들기 시작해 지금은 오픈소스로 유지 중인 @keneth80/k-chart 이야기입니다. 요즘은 관심사 커뮤니티(galaxyconstellate.com)를 만들며 이런 개발 기록을 남기고 있습니다.

#시각화#kchart
은하계에서 보기나도 은하계 만들기