← Back to Projects

Derma:code Chart Library

제품 고유의 시각 표현을 구현하기 위해 직접 만든 사내 공용 차트 라이브러리.

회사가이아랩스
역할설계·개발
팀 구성1
기간2025년 10월 - 2026년 5월
운영GitHub Packages DERMA:CODE V1·V2 사용

DERMA:CODE의 차트를 한 라이브러리로 모은 사내 공용 패키지입니다. 막대·선·육각 레이더·양방향 막대·파이·스코어·레벨·게이지 8종을 Canvas API로 직접 그리고, 디자인 토큰과 테마 4종을 갖습니다. DERMA:CODE V1과 V2가 씁니다. 설계부터 구현, 테스트, 배포, 문서까지 단독으로 수행하고 15개 버전을 배포했습니다. 테스트 과정은 블로그 시리즈로 기록했습니다.

왜 만들었나 (블로그) →

문제와 해결

문제

육각 레이더 차트 위의 그라데이션 오버레이, 유리 질감 마커, 순서대로 나타나는 애니메이션은 디자이너로서 Figma에 먼저 설계해 둔 것이었습니다. Recharts·Chart.js·D3의 기본 기능만으로는 원하는 모양과 동작을 그대로 구현하기 어려웠습니다.

판단

구현에 바로 들어가기 이전에, 가장 유연한 D3로 먼저 만들어 봤습니다. 육각 레이더·게이지·스코어 세 차트를 D3+SVG와 Canvas 두 방식으로 구현해 결과와 구현 비용을 비교했습니다.

해결
같은 세 차트, DOM 노드 수개
D3 + SVG65
Canvas1
번들 증가량KB
D3 + SVG+42
Canvas0

사용자가 보는 결과와 렌더링 속도는 거의 차이가 없었습니다. 대신 구현 비용은 달랐습니다. DOM 노드는 65개 대 1개, 번들은 +42KB 대 0, 같은 효과를 구현하는 코드는 Canvas가 49~82% 적었습니다.

D3 방식에서는 요소마다 transition을 따로 제어해야 했고, DOM을 직접 조작하는 방식도 React의 렌더링 구조와 맞지 않았습니다.

결과

이 프로젝트에서는 Canvas가 더 적합하다고 판단해 D3를 사용하지 않고, 차트 8종을 Canvas API로 직접 구현했습니다.

↗ 왜 D3를 버렸나 (블로그)

문제

DERMA:CODE에 화면이 늘어날 때마다 차트를 새로 스타일링했습니다. 같은 지표를 그려도 화면마다 색과 간격이 달랐고, 수정할 때 여러 화면을 함께 손봐야 했습니다.

차트를 공용 라이브러리로 묶기로 했고, 먼저 어떤 렌더링 방식을 표준으로 삼을지 결정해야 했습니다.

판단
차트 30개 · CPU 4× 스로틀링fps · 높을수록 좋음
Canvas76
SVG36
Recharts11
차트 30개 · DOM 노드 수개
Canvas60
SVG1,320
Recharts1,830

주장하지 않고 쟀습니다. 같은 차트를 Canvas·SVG·Recharts 세 방식으로 구현한 벤치마크 앱을 만들고, 차트 1개·5개·30개를 일반 환경과 CPU 4배 스로틀링 환경에서 비교했습니다.

CPU 4배 스로틀링에서 차트 30개는 Canvas 76fps, SVG 36fps, Recharts 11fps였고, 클릭 반응 시간은 일반 환경에서도 Canvas 7.1ms, SVG 14.66ms였습니다. DOM 노드 수도 60개 / 1,320개 / 1,830개로 차이가 났습니다.

해결

현재 실제 화면의 차트 수만 보면 SVG도 충분했습니다. 다만 공용 라이브러리의 기준을 정할 때는 현재 화면뿐 아니라 애니메이션·인터랙션을 하나의 렌더링 방식으로 통제할 수 있는지, 차트 수가 늘어날 때의 비용까지 함께 고려했습니다.

그 결과 Canvas를 공통 렌더링 방식으로 정하고, 차트별 구현은 하나의 라이브러리 안에서 통일했습니다.

결과

Canvas를 선택하면서 얻는 성능 이점뿐 아니라 접근성 처리, 레티나 대응, 모든 그리기를 직접 구현해야 하는 비용도 함께 문서화했습니다. 선택의 장점뿐 아니라 유지 비용까지 라이브러리의 기준으로 남겼습니다.

↗ Canvas vs SVG vs Recharts (블로그)

문제

스토리북에서는 정상적으로 표시되었지만, 실제 사용 프로젝트인 DERMA:CODE에 설치하자 클릭 범위가 어긋나고, 레이블이 겹치고, 레이아웃이 틀어졌습니다.

테스트에도 어려움이 있었습니다. jsdom에는 Canvas 구현이 없어 getContext('2d')가 null을 반환했고, isPointInPath() 같은 Canvas API의 동작까지 그대로 검증하기는 어려웠습니다.

판단

초기에는 가짜 context를 만들어 어떤 API가 어떤 인자로 호출됐는지 검증했습니다. 하지만 클릭 판정처럼 Canvas의 실제 기하 연산에 의존하는 로직까지 목킹으로 재현하는 데는 한계가 있었습니다. 목 파일을 더 정교하게 만드는 대신, Canvas에 의존하는 로직 자체를 줄이기로 했습니다.

해결

히트 판정 로직을 Canvas에서 분리해 순수 함수로 추출했습니다. 좌표와 도형 정보만 받아 안팎을 판정하도록 만들어 Canvas 없이도 테스트할 수 있게 했습니다. 나머지 렌더링은 Vitest와 Canvas 2D mock으로 검증했습니다.

결과

클릭 범위 문제를 테스트로 검증할 수 있게 됐고, 히트 판정 로직은 Canvas 없이도 단위 테스트할 수 있게 됐습니다.

↗ 테스트를 붙인 삽질기 (블로그)

문제

차트를 코드로 만들어 놓는 것만으로는 팀에서 함께 사용할 수 없었습니다. 설치 방법, 안정적인 버전 배포, 사용법을 확인할 문서, 디자이너가 결과를 검수할 화면까지 필요했습니다.

판단

팀이 실제로 사용할 수 있는 경로를 설치·버전·문서·검수 네 가지로 나눠 만들었습니다.

해결

pnpm 모노레포로 라이브러리·데모·벤치마크를 분리하고, tsup으로 CJS와 ESM을 함께 배포하면서 타입 선언도 자동 생성했습니다. 태그를 푸시하면 GitHub Packages에 배포되도록 구성했습니다.

색·타이포그래피·간격·투명도·선 굵기는 디자인 토큰으로 정의하고, 4종 프리셋과 인스턴스별 override를 지원하도록 만들었습니다.

Storybook에는 각 차트와 상태를 등록해 디자이너가 코드 없이 결과를 확인할 수 있도록 했습니다.

결과

총 15개 버전을 배포했고, Storybook에 50개 이상의 스토리를 구축했습니다. 디자이너가 직접 검수할 수 있는 환경을 만들었고, 사내 프로젝트에서 실제 공용 라이브러리로 사용하고 있습니다.

인사이트와 한계

레티나 대응이 되어 있지 않습니다

devicePixelRatio를 처리하지 않아 고해상도 화면에서 선이 흐릿합니다. 이 라이브러리를 고친다면 첫 번째로 할 일입니다.

HexaChart가 783줄입니다

그라데이션·축·레이블·데이터·오버레이·마커를 한 파일이 다 그립니다. 오프스크린 Canvas로 레이어를 나눠 합성하는 구조가 필요합니다.

스크린 리더가 읽지 못합니다

Canvas 콘텐츠는 접근성 트리에 없습니다. ARIA 속성과 대체 테이블을 붙여야 하는데 아직 하지 못했습니다. Canvas를 고를 때 이미 적어 둔 비용입니다.

벤치마크 수치는 상대 비교로만 읽어야 합니다

rAF 기반 측정은 백그라운드 탭 스로틀과 vsync 타이밍 오차가 있습니다. 절대값이 아니라 같은 조건에서 세 방식을 비교한 결과입니다.

Tech Stack

Canvas APITypeScriptpnpm workspacetsup (CJS+ESM)VitestStorybookGitHub ActionsGitHub Packages