next.js(CSR)에서 chart.js 사용하기

성훈·2023년 8월 19일

cyp.gg

목록 보기
2/2
post-thumbnail

서론

전적을 조회할 때 승률과 포지션 별 픽률을 차트로 표현해 주고 싶었다.

단순 수치로 표현할 수도 있겠지만, 사실 직관적으로 한눈에 보기에는 차트만 한 게 없지 않겠는가?

공홈의 경우 승률만 나오고, 다른 웹 앱의 경우엔 너무 커서 보기 힘들게 나오거나, 뭔가 내 취향이 아니게 나온다.

그래서 승률은 원형 그래프로, 포지션은 바형 그래프로 만들어 보기로 했다.

우선, 만들어보자.

형태가 먼저 정해진 것은 포지션별 픽률 그래프이다.

바 형 그래프로, 정확한 퍼센트보단 대략적으로 내가 이 포지션을 이만큼 하는구나, 다른 포지션과 직관적으로 비교할 수 있는 형태를 띠게 만들고 싶었다.

이것 역시 chart.js 를 사용할까 생각했는데, 그래프에 별다른 애니메이션도 넣을 생각이 없고, 간단한 바 형 그래프라 CSS로 해결하기로 결정.

간단한 구조로는 차트라는 컴포넌트 내 그래프바 컴포넌트를 자식으로 받고, 그 자식에 포지션과 선택 비중을 프롭스로 전달하는 방식으로 결정했다.

// Chart.jsx

...
return (
	<div>
		<Graph value={value} classType={'tanker'} />
		<Graph value={value} classType={'melee'} />
		<Graph value={value} classType={'range'} />
		<Graph value={value} classType={'supporter'} />
	</div>
)

대충 이런 방식.

그래프 내에는 단순히 크기가 같고, 색이 다른 div 태그로 CSS 스타일로 값을 전달해 조정하는 방식이다.

진짜 레거시하고 레거시한 방식으로 구현했다.

데이터를 패칭 받고 있을 때 에러가 발생하는 경우가 있었는데, 이 경우 react-query 의 isLoading을 사용해서 데이터 페칭 전 스켈레톤을 임시로 만들어 놓는 방식으로 해결했다. 타입스크립트였으면 발생하지 않았을 문제.. 어서 포팅하던가 해야지..

문제는 내가 만들고 싶은 승률 원형 그래프.

이 녀석도 라이브러리 사용하지 않는 방식으로 하자면, SVG로 어떻게 어떻게 하면 될 것 같은데, 그렇게까지 품을 들이고 싶지 않았고, 현업에서도 많이 사용하는 차트 라이브러리인 chart.js를 사용하기로 했다.

선택한 이유는, 여러 차트 라이브러리 중 커스터마이징 옵션이 가장 많고, 유저가 많아서 선택했다.

자 이제 사용하는 것이 문제인데, 우선 이 승률 차트에 사용되는 데이터는 상위 컴포넌트에서 프롭스로 전달되는 방식으로 구성된다.

우선 chart.js는 canvas 태그를 찾아서 그 canvas 태그에 차트를 그리는 방식으로 구현된다.

const chartRef = useRef(null);
const chartConfig = {
  type: "doughnut",
  data: {
    labels: ...대충 라벨,
    datasets: [
      ...대충 데이터
    ],
  },
  options: {
    ...대충 옵션
    },
  };

  useEffect(() => {
    const context = chartRef.current.getContext("2d");
    new Chart(context, chartConfig);
  }, []);

여기까지 하니 두 가지 차트가 짜잔.

생각보다 비주얼은 잘 나온 것 같아서 만족하던 찰나..

아직 구현하지 않았던 기능이 추가되면서 에러가 발생했다.

문제 발생

사이퍼즈 역시 일반게임, 랭크게임이 구분되어 있는데, 나는 랭크게임 유저인지라, 랭크게임 전적만 조회하는 방식으로 우선 구현했었다.

하지만, 안 그래도 없는 유저인데, 여기서 갈라치기를 해버린다면, 반쪽짜리라고 생각해서 일반게임도 조회할 수 있게 구현하니, 그 옆에 승패 값 텍스트는 업데이트가 되는데 차트가 업데이트가 안 된다. 렌더링을 강제로 시켜버리니

네~ 이미 캔버스를 이미 쓰고 있어서 안된다고 합니다!

해결 과정

내가 처음 생각했던 방식은 프롭스가 전달되면, 렌더링 다시 되고, 바뀐 프롭스를 가지고 다시 렌더링하니 차트도 바뀐 프롭스로 다시 그려질 것! 이라고 생각했었다.

심지어, 게임타입으로 삼항 연산자로 랭크게임 차트, 일반게임 차트를 따로 둬서 렌더링시키려고 했는데, 안됐다.

뭐 당연한 게 삼항 연산자로 빼내도 재조정. 과정에서는 ‘니가 원하는 게 알게 머임, 내 눈엔 같은 엘리먼트임’ 해버리면 더티 체크 과정에서 쓰루 되어버리는 거니..

결국 다른 방식을 찾아서 했다.

그리고, 공식 문서를 통해 조금 더 찾아보니, 애초에 chart.js 에서 제공하는 방식이 따로 존재했다.

바로 데이터가 들어있는 차트 콘픽을 수정하고 업데이트를 시키는 것.

이걸 상태에 넣어서 업데이트하려고 하니, 공식문서에서 제공하는 방식과 맞지 않는 것 같았다.

따라서, 리프 훅을 이용해 차트를 저장하고, 프롭스가 전달되면 업데이트하는 방식으로 변경했다.

const chartRef = useRef(null);
const chart = useRef(null);

const chartConfig = {
  type: "doughnut",
  data: {
    labels: ...대충 라벨,
    datasets: [
      ...대충 데이터
    ],
  },
  options: {
    ...대충 옵션
    },
  };

  useEffect(() => {
    const context = chartRef.current.getContext("2d");
    chart.current = new Chart(context, chartConfig);
  }, []);

	useEffect(()=>{
		// 최초 실행 방지
		if (!!chart.current) {
      chart.current.data.datasets.pop();
      chart.current.data.datasets.push(변경된 데이터);
      chart.current.update();
    }
	}, [props])

이런 식으로 변경하니, 해결되었다.

역시 머리 깨지면서 이것저것 해보는 것도 좋지만, 공식 문서를 잘 찾아보고 사용하는 게 중요하구나 싶었다.

가성비가 안 좋아~

profile
어떻게 이걸 풀어낼 수 있을까

2개의 댓글

comment-user-thumbnail
2023년 8월 19일

잘 읽었습니다. 좋은 정보 감사드립니다.

답글 달기
comment-user-thumbnail
2023년 8월 19일

맛있게 먹고 갑니다.

답글 달기