주요 컨셉
import React, { useLayoutEffect, useRef, useEffect, useCallback } from "react";
import { createChart, IChartApi, ISeriesApi } from "lightweight-charts";
// 차트에 들어가는 데이터 형태
export interface ILineData {
time: string;
value: number;
}
// 차트 hover 이벤트 발생, 콜백 인터페이스
export interface IonCrossHairChange {
price: number;
}
/**
* 라인 차트를 그립니다.
* @param {ILineData} datas 차트 데이터 입력
* @param {IonCrossHairChange} onCrossHairChange 차트 호버시 콜백함수
* @returns {React.FC} 차트 컴포넌트
*/
interface ILineSeriesChart {
datas?: ILineData[];
onCrossHairChange?: (e: IonCrossHairChange) => void;
}
// 리팩토링 : (1) 생성과 소멸 useEffect로 잘 했는가
// 데이터 set : (2) useEffect로 데이터를 셋팅을 ?
// 데이터 onChange 함수 : (3) 적절한 설계인가?
const LineSeriesChart: React.FC<ILineSeriesChart> = ({
datas,
onCrossHairChange,
}) => {
// 차트 컨테이너를 참조
// useRef 타입 : refObject - JSX 컨테이너는 readonly ( usecallback에 의해 )
const charContainer = useRef<HTMLDivElement>();
// 차트 인터페이스 API 참조
// useRef 타입 : Mutable Object - JSX 컨테이너 안의 chart 인스턴스를 리랜더링 가능
const charApi = useRef<IChartApi>();
// resize Observer
// useRef을 사용하여 observer변수를 참조하자.
// 차트 크기에 대한 반응형을 제공
const resizeObserver = useRef(
new ResizeObserver((entries, observer) => {
// 관찰대상(chart container) 가 있다면
if (entries && entries[0]) {
const width = entries[0].contentRect.width;
// 참조대상(차트 인스턴스) 가 있다면
if (charApi.current) {
// width를 동기화 하자.
charApi.current.applyOptions({ width });
}
}
})
);
// 그래프의 Area 시리즈 데이터를 참조
// const lineSeries = useRef<ISeriesApi<"Line">>();
const SeriesApiArea = useRef<ISeriesApi<"Area">>();
// chart 컨테이너를 참조
const handleContainerRef = useCallback((node) => {
if (node) {
// 컨테이터 요소 참조
charContainer.current = node;
// 리사이즈 구독
resizeObserver.current.observe(node);
}
}, []);
// 랜더링 전에, 차트에 대한 초기화를 진행한다.
// HTML 컨테이너 요소가 변경되면 , 차트를 cleanup 시킨다.
// TODO 리팩토링 : onCrossHairChange 콜백함수 역시
// layout effect에 있을 필요가 없다.
// 라인데이터 변경 effect처럼, 콜백함수가 변경되면 subscribeCrosshairMove 핸들러만 변경
useLayoutEffect(() => {
if (charContainer.current) {
// 차트를 만들고
const chart = createChart(charContainer.current, {
height: 400,
layout: {
fontSize: 11,
},
rightPriceScale: {
visible: false,
},
leftPriceScale: {
visible: false,
},
grid: {
horzLines: {
color: "#ebebeb",
},
vertLines: {
color: "#ebebeb",
},
},
});
// 라인 데이터 요소를 만들고
charApi.current = chart;
SeriesApiArea.current = chart.addAreaSeries({
lineColor: "rgb(243, 188, 47)",
topColor: "rgb(243, 188, 47,0.5)",
bottomColor: "rgb(243, 188, 47,0)",
lineWidth: 2,
});
// 구독 콜백 추가
chart.subscribeCrosshairMove(function (param) {
if (onCrossHairChange) {
const price = param.seriesPrices.get(SeriesApiArea.current!);
onCrossHairChange({ price: Number(price) });
}
});
}
return () => {
// 컴포넌트가 unmount 되면 차트DOM 제거
if (charContainer.current) {
// console.log("will remove child!", charContainer.current.childNodes[0]);
charContainer.current?.removeChild(charContainer.current.childNodes[0]);
}
// 컴포넌트가 unmount 되면 차트 Ref 제거
charApi.current = undefined;
// 컴포넌트가 unmount 되면 차트 DataSetter Ref 제거
SeriesApiArea.current = undefined;
};
}, [charContainer, onCrossHairChange]);
const setLineData = (data?: ILineData[]) => {
if (SeriesApiArea.current && data) {
SeriesApiArea.current?.setData(data);
}
};
// 차트의 데이터만 바뀌는 경우
// 차트를 다시 그릴 필요 없이 , 데이터만 교체
useEffect(() => {
setLineData(datas);
return () => {};
}, [datas]);
return (
<div
className="chartContainer"
ref={(node) => handleContainerRef(node)}
></div>
);
};
export default LineSeriesChart;
// 빈 컨테이너를 리턴
return (
<div
className="chartContainer"
ref={(node) => handleContainerRef(node)}
></div>
);
// 차트 컨테이너를 참조
// useRef 타입 : refObject - JSX 컨테이너는 readonly ( usecallback에 의해 )
const charContainer = useRef<HTMLDivElement>();
// 차트 인터페이스 API 참조
// useRef 타입 : Mutable Object - JSX 컨테이너 안의 chart 인스턴스를 리랜더링 가능
const charApi = useRef<IChartApi>();
useLayoutEffect(() => {
if (charContainer.current) {
// 차트를 만들고
const chart = createChart(charContainer.current, {...});
// 라인 데이터 요소를 만들고
charApi.current = chart;
SeriesApiArea.current = chart.addAreaSeries({...});
// 구독 콜백 추가
chart.subscribeCrosshairMove(function (param) {
if (onCrossHairChange) {
const price = param.seriesPrices.get(SeriesApiArea.current!);
onCrossHairChange({ price: Number(price) });
}
});
}
return () => {
// 컴포넌트가 unmount 되면 차트DOM 제거
if (charContainer.current) {
// console.log("will remove child!", charContainer.current.childNodes[0]);
charContainer.current?.removeChild(charContainer.current.childNodes[0]);
}
// 컴포넌트가 unmount 되면 차트 Ref 제거
charApi.current = undefined;
// 컴포넌트가 unmount 되면 차트 DataSetter Ref 제거
SeriesApiArea.current = undefined;
};
}, [charContainer, onCrossHairChange]);
// 차트의 데이터만 바뀌는 경우
// 차트를 다시 그릴 필요 없이 , 데이터만 교체
useEffect(() => {
setLineData(datas);
return () => {};
}, [datas]);
const setLineData = (data?: ILineData[]) => {
if (SeriesApiArea.current && data) {
SeriesApiArea.current?.setData(data);
}
};
// 차트의 데이터만 바뀌는 경우
// 차트를 다시 그릴 필요 없이 , 데이터만 교체
useEffect(() => {
setLineData(datas);
return () => {};
}, [datas]);