본문 바로가기

트러블슈팅

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

에러 상황

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. 디버깅과 문제 해결 과정의 중요성: 에러 메시지를 정확히 이해하고, 문제의 원인을 단계적으로 파악하여 해결하는 과정이 얼마나 중요한지 다시금 느끼게 되었습니다.

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