ChartJS로 성장곡선 그래프 그리기

2

오늘도 레벨업

목록 보기
3/4

성장곡선?

매일매일 목표한 바를 이루었는지 여부를 확인하고 한 눈에 확인하고 싶었다. 그리고 매일매일 내가 어떻게 성장하는 지 확인을 하고 싶었다.

의도한 그래프는 총 2가지로 총 누적점수의 추이를 보여주는 그래프와 각 목표별 점수 추이 그래프이다. 최종 결과물을 스포하자면 아래와 같다.

ChartJS vs amChartJS

처음 차트를 그리기로 했을 때는 amChart를 이용해서 만들고 싶었다. 조금 더 세련된 느낌이고 애니메이션도 많았기 때문이다.

그러나 중간에 작업을 진행하면서 차트의 작동 원리를 이해하기 조금 버거운 부분이 있어서 커스터마이징의 한계가 있었다. 그래프를 커스터마이징 못하면 의미가 없다고 생각해서 조금 더 단순한 chartJS로 그래프를 그리기로 결정했다.
(추가로 chartJS가 별도의 react 라이브러리를 제공해서 리액트 친화적이었다)

chartjs - tutorial youtube

자료형태에 대한 고민

graph를 그릴 때는 특정 형태에 맞춰서 데이터를 만들어주어야 패키지가 잘 작동하여 그래프가 잘 그려진다.

그런데 백엔드 api를 먼저 만들고 해당 api에서 얻은 자료를 기반하여 그래프용 자료를 프론트에서 만들려고 하니 성능 문제가 생길 것 같았다. (실제로 api를 3개를 복합적으로 날리고 조합하고 하다보니 오래걸렸다....)
그래서 그래프 데이터를 만드는 api를 만들기로 했다. 실제 개발 과정에서는 백엔드 api를 먼저 만들고 프론트 작업을 하는 것으로 아는데 조금 부적절하더라도 프론트와 백엔드를 동시에 만들었을 때의 이득이라 생각하고 효율적인 성능을 택하기로 했다.

backend raw data

userscore를 받는 api에서 얻는 자료는 위와 같이 object의 array 형태로 받게 된다.

{

author: "622f276f5d13c893eb139020",

createdAt: "2022-05-13T15:34:18+09:00",

goal: "622f28485d13c893eb139031",

score: 10,

updatedAt: "2022-05-13T15:34:18+09:00",

\_\_v: 0,

\_id: "6281f06ab1d286093d8084fd",

}
[object1, object2 .... ]

graph 가공 data

그러면 chartjs의 요구 데이터 형태에서 알아보자.

{
	"data": {
		"labels": [
			"2022-06-11","2022-06-12","2022-06-13","2022-06-14",
			
		],
		"datasets": [
			{
				"label": "목표1",
				"data": [
					10,
					0,
					0,
					0,
					
				]
			},
			{
				"label": "목표2",
				"data": [
					10,
					0,
					10,
					0,
					
				]
			},
			{
				"label": "목표3",
				"data": [
					0,
					0,
					10,
					0,
					
				]
			}
		]
	}
}

데이터의 형태는 x-label부분인 labelsdatasets로 크게 구성이 된다. 그리고 datasets안에서 각 데이터별로 labeldata가 들어간다. 이 때 각 데이터 별로 다른 색상등의 옵션을 부여하고자 한다면 아래와 같이 추가적으로 필드를 넣어주면된다.


datasets : [
  
  {
  	label : 'label1'
    data: [0,1,2],
  	backgroundColor : 'yellow', // 라인 차트- 선 색깔, 막대 차트- 내부색
    borderColor : 'purple',// 라인 차트- 각 점 색깔, 막대 차트- 외곽선
	lineTension :  0.5 //1과 가까울수록 직선 0과 가까울수록 곡선
  }
	...
]

api에 대한 고민

처음 score를 생성하는 api를 만들때만 하더라도 그래프에 대한 고민 없이 작성을 했다. 그렇다보니 실제 오늘 제출한 목표에 대해서만 점수가 생성이 되고 제출이 되지 않은 점수는 기록으로 남지가 않았다. 그렇다보니 그래프에 표시해줄 자료도 없었다. 그래프로 특정 날에 목표를 어떤 것들을 완료했고 어떤 것은 못했는지 기록을 확인하려면 그래프에 0인 부분을 표시해주어야 했다.

그러면 백엔드적으로 데이터 추가하는 과정이 들어가야 하는데 아래 순서도는 고민을 한 과정이다.

위 과정에서 조금 수정을 하고 NestJS schedueling까지 적용을 해서 점수 생성을 하면 점수가 0인 score 자료들이 생긴다. 이를 바탕으로 그래프를 그려볼 수 있다.

그래프 그리기

자료가 준비되었으니 이제 그래프를 그릴 여건이 완료되었다고 할 수 있지만 아직 추가적으로 필요한 부분이 있다. 리액트에 맞춰서 그래프를 그릴 수 있도록 해줘야 한다.

react 배치

우선은 chartjs의 좋은 점은 react와 호환을 위한 별도의 라이브러리를 제공을 한다는 것이다.

npm install chart.js
npm install react-chartjs-2 

이제 chart를 위한 component를 만들어 본다.

import React, { useEffect } from "react";
import { Line } from "react-chartjs-2";

//이 문구를 적어줘야 자동으로 차트가 나오게 된다.
import { Chart as ChartJS } from "chart.js/auto";

//style component
import { GraphContainer } from "./GraphElement";

const Graph = ({ accumData, lineData }) => {
  useEffect(() => {}, [accumData,lineData]);

  return (
    <GraphContainer>
      {accumData && <Line data={accumData} />}
      {lineData && <Line data={lineData} />}
    </GraphContainer>
  );
};

export default Graph;

Line data에 props로 위에서 지정한 데이터 형태의 자료를 넣어주기만 하면 그래프가 그려진다.

이슈 - 렌더링 순서 문제

위와 같이 작성을 하였어도 문제가 발생을 했다. 자료가 undefined된 것이었는데 이 부분은 리액트의 작동 순서에서 기인했다는 것을 확인했다.

접근

리액트는 우선 렌더링을 먼저 하고 그 다음 useEffect를 하는 방식으로 작동하는 것을 확인했다.

근데 렌더링을 할 때 chartjs가 차트를 그리려는데 아직 리액트 훅이 작동하지 않아서 자료가 없었고 위와 같이 내부적으로 map을 돌릴 때 문제가 발생한 것이었다.

해결

해결 방법은 크게 두가지로 접근을 했다. 우선 렌더링을 할 때 컨디셔널 렌더링을 통해서 자료를 받지 않았다면 그리지 않도록 하였다.

const Graph = ({ accumData, lineData }) => {
  useEffect(() => {}, [accumData,lineData]);

  return (
    <GraphContainer>
      {accumData && <Line data={accumData} />}
      {lineData && <Line data={lineData} />}
    </GraphContainer>
  );
};

그리고 두번째는 기본 자료를 만들어 처음 렌더링에서 에러가 나지 않도록 하였다.

  const [lineData, setLineData] = useState({
    labels: ["label1", "label2"],
    datasets: [{ label: "label1", data: [1, 2] }],
  });

이렇게 하면 처음 자료를 얻지 못하더라도 그래프를 그릴 수 있어서 에러를 막을 수 있고 이후 자료가 들어오면 useEffect에 의해서 새롭게 그래프가 그려진다.

이슈 - 다이나믹 색깔 부여

백엔드에서 받는 자료는 현재 각 데이터의 라벨(목표이름)과 데이터만 존재한다. 이 데이터를 그대로 사용하게 되면 각 목표에 따른 색깔 구분이 어려워서 각 목표별로 별도의 색깔을 부여하고 싶었다.

접근

그러면 우선 백엔드에서 주어진 자료를 다시 분해하고 각 data 목표 별로 색깔을 각 object에 추가해주어야 했다.

백엔드 api요청하는 함수 이후에 그 자료를 바탕으로 다시 자료를 재구성하는 함수를 만들었다.

해결

  const lineChartConfig = (data) => {
    const color = ["#ffeba8", "#FFFB8C", "#bae9f7", "#b2d2f7", "#a3baf0"];
    const dataSet = data.datasets;

    for (let e of dataSet) {
      let obj = {
        ...e,
      };
      obj["backgroundColor"] = [color[dataSet.indexOf(e)]];
      obj["borderColor"] = [color[dataSet.indexOf(e)]];
      obj["lineTension"] = 0.5;
      dataSet[dataSet.indexOf(e)] = obj;
    }
	
    let result = { labels: data.labels, datasets: dataSet };

    setLineData(result);
  };

현재 임의로 테마에 맞춰서 6개의 색깔을 부여한 상태이다. 만일 목표가 6개보다 크면 이를 loop돌리는 방식으로 코드를 추가하겠지만 현재는 목표가 6개 이상일 경우는 많지 않다고 생각하여 이렇게 진행하기로 했다. colorcode를 랜덤하게 하는 방법도 있었지만 테마 색깔과 맞지 않으면 부적절하다고 생각했다.

각 data에 backgroundColor, borderColor, lineTension을 추가해주고 해당 자료가 원래 있던 인덱스에 자료와 대체해주었다.

마무리

아직 본격적으로 setting을 건드려본거는 아니지만 웹에서 그래프실시간 데이터로 그려보는 경험이 재미있었던 것 같다. 여러 setting을 바꿔가면서 달라지는 점을 확인해봐야겠다

profile
기록을 통해 한 걸음씩 성장ing!

0개의 댓글