Post

[Trouble Shooting] React Strict Mode에서 ECharts 인스턴스 중복 생성 방지

React Strict Mode의 Double Mount로 인한 ECharts disposed 경고 해결

문제 상황

React 프로젝트에서 ECharts를 사용할 때 다음과 같은 경고가 콘솔에 반복적으로 출력됨:

1
[ECharts] Instance ec_1234567890 has been disposed

차트는 정상적으로 렌더링되지만, 경고 메시지가 계속 발생하여 메모리 누수 가능성이 우려됨.


원인

React Strict Mode의 Double Mount

React 18의 Strict Mode는 개발 환경에서 컴포넌트를 두 번 마운트/언마운트하여 부수 효과(side effect)를 감지함.

1
2
3
4
1. 컴포넌트 마운트
2. useEffect cleanup 실행
3. 컴포넌트 다시 마운트
4. useEffect 다시 실행

기존 코드의 문제점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// EChartsWrapper.tsx (문제가 있는 코드)
useEffect(() => {
  if (!chartRef.current) return;

  // 기존 인스턴스 dispose
  if (chartInstanceRef.current) {
    chartInstanceRef.current.dispose();  // ❌ 이미 dispose된 인스턴스를 또 dispose
  }

  // 새 인스턴스 생성
  const chartInstance = echarts.init(chartRef.current);
  chartInstanceRef.current = chartInstance;

  return () => {
    chartInstance.dispose();
  };
}, []);

문제점:

  1. Strict Mode에서 첫 번째 마운트 시 인스턴스 생성
  2. cleanup 함수에서 dispose() 호출
  3. 두 번째 마운트 시 이미 dispose된 인스턴스에 다시 dispose() 시도
  4. “Instance has been disposed” 경고 발생

해결 방법

getInstanceByDom으로 인스턴스 재사용

ECharts의 getInstanceByDom() 메서드를 사용하여 기존 인스턴스를 재사용함.

Before (문제가 있는 코드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
useEffect(() => {
  if (!chartRef.current) return;

  // ❌ 매번 새 인스턴스 생성
  if (chartInstanceRef.current) {
    chartInstanceRef.current.dispose();
  }
  const chartInstance = echarts.init(chartRef.current);
  chartInstanceRef.current = chartInstance;

  return () => {
    chartInstance.dispose();
  };
}, []);

After (개선된 코드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
useEffect(() => {
  if (!chartRef.current) return;

  // ✅ 기존 인스턴스가 있으면 재사용
  let chartInstance = echarts.getInstanceByDom(chartRef.current);

  if (!chartInstance) {
    chartInstance = echarts.init(chartRef.current);
  }

  chartInstanceRef.current = chartInstance;

  // 차트 옵션 설정
  if (option) {
    chartInstance.setOption(option);
  }

  // cleanup: dispose 전에 isDisposed() 체크
  return () => {
    if (chartInstance && !chartInstance.isDisposed()) {
      chartInstance.dispose();
    }
  };
}, [option]);

핵심 변경 사항

  1. getInstanceByDom() 사용: DOM에 이미 연결된 인스턴스가 있는지 확인
  2. 조건부 생성: 인스턴스가 없을 때만 새로 생성
  3. isDisposed() 체크: dispose 전에 이미 dispose되었는지 확인

결과

Before

  • 콘솔에 [ECharts] Instance ec_xxx has been disposed 경고 반복 출력
  • 메모리 누수 위험 존재
  • Strict Mode에서 불안정한 동작

After

  • 경고 메시지 완전히 제거됨
  • 인스턴스가 효율적으로 재사용됨
  • Strict Mode에서도 안정적으로 동작
  • 메모리 관리 개선

주의할 점

1. Strict Mode는 개발 환경에서만 동작

  • 프로덕션 빌드에서는 Double Mount가 발생하지 않음.
  • 하지만 개발 환경에서 경고가 발생하면 메모리 관리 문제가 있을 가능성이 높으므로 반드시 수정해야 함.

2. isDisposed() 체크 필수

1
2
3
4
5
6
7
8
9
10
11
// ❌ 나쁜 예
return () => {
  chartInstance.dispose();  // 이미 dispose된 상태면 에러
};

// ✅ 좋은 예
return () => {
  if (chartInstance && !chartInstance.isDisposed()) {
    chartInstance.dispose();
  }
};

3. 의존성 배열 관리

  • option을 의존성 배열에 포함시키면 옵션 변경 시 차트가 업데이트됨.
  • 빈 배열([])을 사용하면 최초 마운트 시에만 실행됨.

4. 차트 리사이즈 처리

1
2
3
4
5
6
7
8
9
10
useEffect(() => {
  const handleResize = () => {
    if (chartInstanceRef.current && !chartInstanceRef.current.isDisposed()) {
      chartInstanceRef.current.resize();
    }
  };

  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

전체 코드 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// EChartsWrapper.tsx
import { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';

interface EChartsWrapperProps {
  option: EChartsOption;
  height?: number;
}

export const EChartsWrapper: React.FC<EChartsWrapperProps> = ({
  option,
  height = 300,
}) => {
  const chartRef = useRef<HTMLDivElement>(null);
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);

  useEffect(() => {
    if (!chartRef.current) return;

    // 기존 인스턴스 재사용
    let chartInstance = echarts.getInstanceByDom(chartRef.current);

    if (!chartInstance) {
      chartInstance = echarts.init(chartRef.current);
    }

    chartInstanceRef.current = chartInstance;

    // 차트 옵션 설정
    if (option) {
      chartInstance.setOption(option);
    }

    // cleanup
    return () => {
      if (chartInstance && !chartInstance.isDisposed()) {
        chartInstance.dispose();
      }
    };
  }, [option]);

  // 리사이즈 처리
  useEffect(() => {
    const handleResize = () => {
      if (chartInstanceRef.current && !chartInstanceRef.current.isDisposed()) {
        chartInstanceRef.current.resize();
      }
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div ref={chartRef} style={{ width: '100%', height: `${height}px` }} />;
};

React Strict Mode 환경에서 ECharts를 안전하게 사용하려면 getInstanceByDom()isDisposed()를 활용하여 인스턴스를 재사용해야 함. 이를 통해 메모리 누수를 방지하고 경고 메시지를 제거할 수 있음.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.