데이터 시각화 프로젝트나 대시보드를 구축할 때, 유사한 형태의 차트를 반복해서 구현하는 것은 비효율적입니다. Vue 3의 Composition API와 ECharts를 결합하여 재사용 가능한 차트 컴포넌트를 캡슐화하는 방법을 살펴보겠습니다.
부모 컴포넌트에서의 활용
캡슐화된 차트 컴포넌트를 부모 컴포넌트에서 호출할 때는 필요한 데이터와 스타일 속성을 Props로 전달합니다. 다음은 <script setup> 문법을 사용하여 반응형 데이터를 정의하고 차트를 렌더링하는 예시입니다.
<template>
<BarChart
:dom-id="chartId"
:categories="xAxisCategories"
:values="yAxisValues"
:chart-title="displayTitle"
:gradient-colors="barColors"
:border-radius="barRadius"
class="chart-container"
/>
</template>
<script setup>
import { reactive, ref, toRefs } from 'vue';
import BarChart from '@/components/BarChart/index.vue';
const chartState = reactive({
xAxisCategories: [],
yAxisValues: [],
});
const chartId = ref('sales-bar-chart');
const displayTitle = ref('월별 매출 현황');
const barColors = ref(['#4facfe', '#00f2fe']);
const barRadius = ref([8, 8, 0, 0]);
const { xAxisCategories, yAxisValues } = toRefs(chartState);
// API 등에서 비동기적으로 데이터를 가져와 할당하는 로직
const fetchChartData = async () => {
// const response = await api.getSalesData();
// chartState.xAxisCategories = response.categories;
// chartState.yAxisValues = response.values;
};
fetchChartData();
</script>
자식 컴포넌트 (BarChart) 구현
자식 컴포넌트에서는 ECharts 인스턴스를 초기화하고, 전달받은 Props를 기반으로 차트 옵션을 구성합니다. 데이터가 비동기적으로 변경되는 상황을 대비하여 watch를 사용해 데이터 변화를 감지하고 차트를 다시 그립니다.
<template>
<div :id="domId" class="echarts-wrapper"></div>
</template>
<script setup>
import { toRefs, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts';
const props = defineProps({
domId: { type: String, required: true },
chartTitle: { type: String, default: '' },
categories: { type: Array, default: () => [] },
values: { type: Array, default: () => [] },
borderRadius: { type: Array, default: () => [0, 0, 0, 0] },
gradientColors: { type: Array, default: () => ['#00c6fb', '#005bea'] },
});
const { domId, chartTitle, categories, values, borderRadius, gradientColors } = toRefs(props);
let chartInstance = null;
const renderChart = () => {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById(domId.value));
}
const chartOption = {
title: {
text: chartTitle.value,
left: 'center',
top: 10,
textStyle: { color: '#333', fontSize: 16, fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
},
grid: {
left: '3%', right: '4%', bottom: '3%', top: '20%',
containLabel: true,
},
xAxis: {
type: 'category',
data: categories.value,
axisLine: { lineStyle: { color: '#ccc' } },
axisLabel: { color: '#666', rotate: categories.value.length > 5 ? 30 : 0 },
},
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#eee', type: 'dashed' } },
axisLabel: { color: '#666' },
},
series: [
{
type: 'bar',
data: values.value,
barWidth: '40%',
itemStyle: {
borderRadius: borderRadius.value,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: gradientColors.value[0] },
{ offset: 1, color: gradientColors.value[1] },
]),
},
},
],
};
chartInstance.setOption(chartOption);
};
const handleResize = () => {
chartInstance?.resize();
};
// 값이 변경될 때마다 차트 업데이트
watch(
values,
() => {
nextTick(renderChart);
},
{ immediate: true, deep: true }
);
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
});
</script>
<style scoped>
.echarts-wrapper {
width: 100%;
height: 400px;
}
</style>
비동기 데이터 처리 및 최적화 고려사항
차트 데이터는 주로 API 호출을 통해 비동기적으로 가져오게 됩니다. 이때 데이터가 완전히 로드되기 전에 차트가 초기화되는 문제를 방지해야 합니다.
- Watch 활용: 위 코드처럼
watch를 사용하여 데이터 배열의 변화를 감지하고nextTick이후에 차트를 렌더링하면 DOM 업데이트가 완료된 후 안전하게 차트를 그릴 수 있습니다. - v-if 조건부 렌더링: 부모 컴포넌트에서 데이터 로딩 상태를 관리하며
v-if="isLoaded"방식을 사용할 수도 있습니다. 하지만 차트의 부드러운 전환이나 로딩 애니메이션을 고려한다면watch또는watchEffect를 사용하는 것이 더 유연합니다. - 메모리 누수 방지: 윈도우 리사이즈 이벤트 리스너와 ECharts 인스턴스는 컴포넌트가 언마운트될 때 반드시 해제(
removeEventListener,dispose)하여 메모리 누수를 방지해야 합니다. - Props 구조 분해:
defineProps로 선언한 후toRefs를 사용하면 반응형을 유지하면서 각 속성을 독립적으로 사용할 수 있어 코드의 가독성이 높아집니다.