돌멩이 하나/에러는 미래의 연봉

chart.js 플러그인 충돌과 등록 방식에 따른 에러

미래에서 온 개발자 2025. 3. 3. 14:52

새로운 에픽의 주요 요구사항 중 차트 그래프로 데이터를 시각화해서 보여줘야 하는 섹션을 구현하는 부분이 있었다. 나도 드디어 차트를 만져보게 되었다! 기존 프로젝트에서 chart.js를 이미 사용하고 있어서 추가 라이브러리 설치 없이 그대로 chart.js를 사용하기로 했다. 보통 chart.js에 대한 단점으로 주로 언급하는 게 반응형에 대한 부분인데, 이번 프로젝트는 반응형은 크게 고려하지 않고 픽셀 단위로 css를 조작하고 있는 프로젝트라 chart.js로도 요구사항을 충족할 수 있을 거라고 판단했다. 

 

그러던 중 플러그인 관리에 있어 예상치 못한 버그를 만났다. 여러 페이지에서 각기 다른 차트 컴포넌트를 사용하고 있는 중에 새로운 페이지에서 차트 컴포넌트가 새로 추가되었고, 스토리북으로 UI 테스트를 할 때에는 발견하지 못했던 버그를 QA 단계 전에 여러 페이지를 돌아가면서 플로우를 돌리다보니 발견하게 된 버그였다. 

 

문제 상황 

2개 이상의 서로 다른 페이지에서 chart.js의 Bar 컴포넌트를 사용하고 있었다. Bar 컴포넌트는 chart.js를 react 컴포넌트로 래핑해주는 역할을 하는 react-chartjs-2를 사용 중이었다. 

 

- 첫 번째 페이지: Bar 차트에 데이터 레이블(숫자 값)을 표시해야 하는 하는 페이지

- 두 번째 페이지: Bar 차트에 데이터 레이블이 표시되지 않아야 하는 페이지 

 

첫 번째 페이지가 이번 에픽에서 새로 구현한 페이지였다. 해당 페이지에서 `chartjs-plugin-datalabels`를 사용해 막대 그래프 위에 데이터 값을 표시했다. 이를 위해 다음과 같은 코드를 작성했다. 

 

import { Chart, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from "chart.js";
import { Bar } from "react-chartjs-2";
import ChartDataLabels from "chartjs-plugin-datalabels";

Chart.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels);

// Bar 컴포넌트 사용

 

그러자 첫 번째 페이지를 방문한 후 두 번째 페이지로 이동했을 때, 두 번째 페이지의 차트에도 데이터 레이블이 표시되는 문제가 발생했다. 두 번째 페이지에서는 데이터 레이블을 사용하지 않아야 했기에 이는 차트 관련 버그였다. 

 

 

원인 분석

문제의 원인은 `Chart.register()` 메소드에 있었다. 이 메소드는 Chart의 전역 인스턴스에 플러그인을 등록한다. 따라서 첫 번째 페이지에서 ChartDataLabels 플러그인을 등록하면 같은 세션 내에서 이후에 생성되는 모든 Chart 인스턴스에 해당 플러그인이 적용된다.

 

즉, 페이지 A에서 등록한 플러그인이 페이지의 B의 차트에도 영향을 미친 것이다. 

 

 

해결 방법 탐색

1. 컴포넌트별 스케일과 플러그인 등록 

 

첫 번째로 시도한 해결책은 전역 `Chart.register()`를 일체 사용하지 않고, 각 Bar 컴포넌트의 plugins prop으로 필요한 플러그인만 주입하는 방식이었다. 

// Chart.register()를 사용하지 않음

<Bar
  data={...}
  plugins={[CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels]} // 필요한 컴포넌트에만 register() 메소드에 등록하던 것들을 전부 plugins prop에 주입
/>

 

하지만 이 방식으로 변경하자 다음과 같은 런타임 에러가 발생했다. 

 

"category" is not a registered scale.

 

 

chart.js의 공식 문서 한켠에 다음과 같은 내용을 발견할 수 있었다. 

When optimizing the bundle, you need to import and register the components that are needed in your application.

The options are categorized into controllers, elements, plugins, scales. You can pick and choose many of these, e.g. if you are not going to use tooltips, don't import and register the Tooltip plugin. But each type of chart has its own bare-minimum requirements (typically the type's controller, element(s) used by that controller and scale(s)):

Bar chart
- `BarController`
- `BarElement`
- Default scales: `CategoryScale` (x), `LinearScale` (y)

 

https://www.chartjs.org/docs/latest/getting-started/integration.html#bundle-optimization

 

Integration | Chart.js

Integration Chart.js can be integrated with plain JavaScript or with different module loaders. The examples below show how to load Chart.js in different systems. If you're using a front-end framework (e.g., React, Angular, or Vue), please see available int

www.chartjs.org

 

scale은 반드시 register를 해야만 사용할 수 있고, 각 차트 별로 디폴트 스케일이 정해져 있었다. register() 메소드에는 스케일 옵션과 플러그인을 모두 등록할 수 있지만, plugins prop에는 문자 그대로 플러그인만 지정할 수 있다. 

 

 

2. 스케일과 플러그인 분리 등록

 

다음으로 시도한 방법은 필수 스케일 요소는 전역으로 등록하되, 옵션 플러그인은 컴포넌트별로 주입하는 접근법이었다. 

 

// 필수 요소는 전역 등록
Chart.register(CategoryScale, LinearScale, BarElement);

// 컴포넌트에서는 추가 플러그인만 지정
<Bar
  data={...}
  plugins={[Title, Tooltip, Legend, ChartDataLabels]}
/>

 

이 방식은 첫 번째 페이지를 방문한 후, 두 번째 페이지를 방문했을 때 두 번째 페이지의 차트에 불필요한 데이터 레이블이 렌더링되는 문제는 해결해주었지만 새로운 문제가 등장했다. 첫 번째 페이지의 차트에서는 external HTML tooltip 방식으로 커스텀 툴팁을 hook으로 만들어서 사용하고 있었는데, 해당 툴팁이 최초에는 렌더링되지만 마우스가 다른 막대 그래프로 이동할 때에는 새로 렌더링되지 않고, 최초로 렌더링된 커스텀 툴팁만 그대로 화면에 남아있는 문제가 발생했다. 

 

https://github.com/chartjs/Chart.js/issues/11691

 

Tooltip is not registered properbly if loaded as plugin · Issue #11691 · chartjs/Chart.js

Expected behavior As a plugin I expect, that I could load as a plugin per compnentn and do not have to use Chart.register Current behavior Default tooltip is not displayed if registered as plugin. ...

github.com

 

위의 깃헙 이슈에서 chart.js의 contributor가 다음과 같은 코멘트를 남겨두었다.

 

The tooltip plugin uses an hook that only fires once after the chart has been made to create its own tooltip object. This is why the plugin needs to be registered.
출처: https://github.com/chartjs/Chart.js/issues/11691#issuecomment-2089080256

 

툴팁을 register() 메소드의 인자로 넘겨줘야만 매번 트리거가 되는거구나... 

 

 

최종적으로는 데이터 레이블이 필요한 첫 번째 페이지의 컴포넌트에서만 `ChartDataLabels`을 plugins으로 넘겨주고, 나머지 스케일 옵션과 플러그인은 register로 등록했다. 두 번째 페이지에서 불필요한 데이터 레이블이 뜨는 이슈도 해결하고, 마우스가 막대 그래프를 이동할 때마다 마우스가 위치한 막대 그래프에 해당하는 데이터 레이블을 툴팁으로 보여주는 동작도 기대대로 정상 작동했다. 

 

이 과정에서 프론트 챕터 팀원들과 함께 여러 논의가 오고 갔다. Chart.register()를 전역 설정으로 두려는 시도도 해보았고, 차트 별로 reigster를 관리하는 공용 wrapping 컴포넌트를 만들어서 사용해 보면 어떻겠냐는 제안도 있었다. 첫번째 전역 설정 시도에서는 Next.js에서의 클라이언트 컴포넌트와 서버 컴포넌트의 환경 차이를 고려하지 못해 런타임 에러를 또다시 경험하기도 했다 ㅎㅎ 

 

 

chart.js를 사용하다보니 커스텀 툴팁을 컴포넌트로 만들어서 넘길 수 없는 제약이 있는 것부터, register 메소드로 등록하는지 plugins prop으로 넘기는지에 따라 동작에 차이가 나는 것까지 자잘한 불편함들을 경험해 보고 있다. 이래서 다들 rechart로 넘어가는거구나. 하지만 겪어보지 않으면 무엇이 불편한지 왜 다른 라이브러리가 필요한지 알 수 없는 법이니까 😇