트러블슈팅

[Chart.js] Canvas is already in use. Chart with ID '0' must be destroyed before the canvas with ID 'myChart' can be reused.

아임실버 2024. 8. 5. 21:49

에러 상황

React 프로젝트에서 Chart.js를 사용해 API 데이터를 시각화하던 중, 

🚨 Canvas is already in use. Chart with ID '0' must be destroyed before the canvas with ID 'myChart' can be reused. 

에러 메시지가 발생했습니다.

 

이 에러는 Chart.js가 동일한 canvas 요소에서 여러 번 초기화되면서 발생하는 문제입니다. 특히 React의 useEffect 훅 내에서 Chart.js를 사용할 때, 컴포넌트가 리렌더링되거나 useEffect가 다시 실행될 때마다 차트가 중복으로 생성되어 이러한 에러가 발생하게 됩니다.

문제 해결을 위한 접근 방법

이 문제를 해결하기 위해, 먼저 새로운 차트를 생성하기 전에 기존의 차트를 명시적으로 파괴(destroy)해야 한다는 점을 깨달았습니다. 이를 구현하기 위해 몇 가지 수정을 거쳤습니다:

  1. Chart.js 객체를 전역 변수로 선언: useEffect 내에서 차트 객체(myChart)를 전역 변수로 선언하여 컴포넌트의 라이프사이클 전반에서 접근할 수 있도록 했습니다.
  2. Cleanup 함수 구현: useEffect에서 반환되는 cleanup 함수를 사용해 컴포넌트가 언마운트되거나 useEffect가 다시 실행되기 전에 기존의 차트를 파괴했습니다. 이를 통해 동일한 canvas 요소를 여러 번 사용하는 상황을 방지했습니다.
import React, { useEffect } from 'react';
import { Chart } from 'chart.js';

function App() {
    useEffect(() => {
        let myChart; // Chart.js 객체 선언

        const OPEN_API = "https://api.open-meteo.com/v1/forecast?latitude=37.566&longitude=126.9784&hourly=temperature_2m";

        const draw = (res) => {
            const opt = {
                year: 'numeric',
                month: 'numeric',
                day: 'numeric',
                hour: 'numeric'
            };
            res.hourly.time = res.hourly.time.map(e => {
                return new Intl.DateTimeFormat("ko-KR", opt).format(new Date(e));
            });
            const data = {
                labels: res.hourly.time,
                datasets: [{
                    label: '서울의 온도차트',
                    data: res.hourly.temperature_2m,
                    borderColor: 'rgb(255,99,132)',
                    backgroundColor: 'rgba(255,99,132,0.5)',
                    pointStyle: 'circle',
                    pointRadius: 10,
                    pointHoverRadius: 15,
                }]
            };
            const ctx = document.getElementById('myChart').getContext('2d');

            // 기존 차트 파괴 후 새 차트 생성
            if (myChart) {
                myChart.destroy();
            }

            myChart = new Chart(ctx, {
                type: 'line',
                data: data
            });
        };

        const fetchData = async () => {
            const ret = await fetch(OPEN_API).then(res => res.json());
            draw(ret);
        };

        fetchData();
        
        // cleanup 함수 반환: 컴포넌트가 언마운트될 때 또는 useEffect가 다시 실행되기 전에 실행됩니다.
        return () => {
            if (myChart) {
                myChart.destroy();
            }
        };
    }, []);

    return (
        <div style={{width: '100%', height: '100vh'}}>
            <canvas id="myChart" width="400" height="400"></canvas>
        </div>
    );
}

export default App;

 

버전 호환성 문제와 해결

위의 수정에도 불구하고, 여전히 에러가 발생하는 상황이 있었습니다. 이 문제를 파악한 결과, Chart.js의 특정 버전(특히 최신 버전)이 React와의 호환성 문제를 일으킬 수 있다는 사실을 알게 되었습니다.

npm i chart.js@2.9.4 명령어를 통해 Chart.js의 버전을 2.9.4로 다운그레이드한 후, 문제없이 차트가 정상적으로 렌더링되는 것을 확인할 수 있었습니다.

배운 점 및 느낀 점

이번 트러블슈팅을 통해 다음과 같은 중요한 점들을 배울 수 있었습니다.

  1. 컴포넌트 라이프사이클 관리: React에서 외부 라이브러리를 사용할 때는 컴포넌트의 라이프사이클을 잘 관리하는 것이 중요합니다. 특히, 반복적으로 초기화되거나 리렌더링되는 리소스의 경우에는 명시적인 정리가 필요하다는 것을 깨달았습니다.
  2. 호환성 문제 인식: 외부 라이브러리의 버전이 중요하며, 최신 버전이 항상 안정적이지 않을 수 있다는 점을 경험했습니다. 버전 간의 호환성을 신중히 고려해야 한다는 것을 배웠습니다.
  3. 디버깅과 문제 해결 과정의 중요성: 에러 메시지를 정확히 이해하고, 문제의 원인을 단계적으로 파악하여 해결하는 과정이 얼마나 중요한지 다시금 느끼게 되었습니다.

이번 경험을 통해, 더 나은 코드 작성과 디버깅 능력을 기를 수 있었으며, 앞으로 유사한 문제를 더 빠르고 효율적으로 해결할 수 있을 것이라 생각합니다.