우아한 개발계발 블로그

아글라이아 연구소 개발 일지 (3) - Vue 에서 chartjs-chart-financial 사용하기 (프론트 디자인은 맘에 들기 쉽지않아) 본문

Programming/아글라이아 연구소 개발 일지

아글라이아 연구소 개발 일지 (3) - Vue 에서 chartjs-chart-financial 사용하기 (프론트 디자인은 맘에 들기 쉽지않아)

W00_Ah 2024. 9. 6. 17:46

저번에 블로그 글 쓰고 한달이라는 시간이 지나갔다. 아래의 결과물을 만드는데 2주나 걸렸다.
중간에 2주는 개인적인 행사로 많이 바빴다.


아래는 현재까지의 결과물이다.

이건 닉네임을 검색하자 마자 보게되는 화면이고

이건 아래에 나타날 정보들이다.

현재는 하드코딩하여 데이터를 집어넣어 두었다. 

mmr 차트를 처음에는 단순히 line chart 로 그리려고 했으나, 뭔가 뻔하고 재미없어서. 주식에서 이용하는 candle Chart를 적용해보았다.

사람들이 chart.js를 많이 사용하는데, financial chart  라는 이름으로 따로 제공 중 이다.

 

한국에서는 이런 캔들차트를 다루는 블로그도 많지 않아서 GPT의 도움을 받으며 코드를 이해하고 수정해가는데 2~3일 정도를 쏟은 것 같다. 물론 외국인이 예제로 올린 코드가 몇개 있어서 그거라도 참고해가면서 적용했다.

 

지금부터 아래에 쓸 글들은 내가 캔들차트(financial chart)를 그리는데 겪었던 문제들에 대해 늘여놓듯이 말할 것이다.

 

x 축은 날짜 데이터를 넣고 싶었는데, 이를 위해서는'chartjs-adapter-date-fns' 라는 라이브러리가 추가로 필요했다.

데이터 랜덤데이터 집어넣는데 gpt가 잘못알려줘서 헤맸다.

{x: ~, o:~,.. } 인데 {t: ~, o:~,.. }라고 하는 둥.. 

 

캔들차트에서 캔들의 색을 바꾸려면 options에 직접 backgroundColor, borderColor 를 넣어줘야했다.

기존의 단순 chart.js 만을 활용한 Line chart에서 색상을 바꾸는 것과는 차이가 조금 있었다.

그래서 import 한 chartjs-chart-financial 라이브러리를 분석해가면서 알아갔다.

 

chart에 마우스를 hover했을 때 나오는 툴팁 창이 맘에 안들어서 내가 커스텀 해서 만들고 싶었다.

근데 어떻게 만드는 지 몰라 gpt에게 물어봤더니 external 이라는 속성을 통해 만들 수 있다고 했다.

이건 정말 뚝딱 만들어져서 조금 놀랐다. 주석도 잘 달아줬다.

왼쪽이 내가 커스텀한 툴팁, 오른쪽이 기본적으로 지원하는 툴팁

아래에는 해당 컴포넌트의 script 전문이다.

누군가에겐 도움이 됐으면 좋겠다.

import {Chart, registerables} from 'chart.js';
import 'chartjs-chart-financial';
import 'chartjs-adapter-date-fns';
import {CandlestickController, CandlestickElement} from 'chartjs-chart-financial';

// Chart.js 등록
Chart.register(...registerables, CandlestickElement, CandlestickController);

export default {
  name: 'MMRChart',
  mounted() {
    this.renderChart();
  },
  data() {
    return {
      chartConfig: {
        type: 'candlestick',
        data: {
          datasets: [{
            label: '',
            data: this.getChartData(), // chartdata
            barThickness: 25, // 봉의 고정된 너비를 설정
            maxBarThickness: 15, // 봉의 최대 너비를 제한
          }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false, // 창 크기 조절 시 비율을 유지하지 않음
          scales: {
            x: {
              type: 'time',
              time: {
                unit: 'day',
                displayFormats: {
                  day: 'MM/dd' // 날짜 포맷을 MM/DD 형식으로 설정
                }
              },
              ticks: {
                autoSkip: false, // 자동 생략 비활성화
                maxRotation: 90,  // 라벨의 회전 각도 제한
                minRotation: 55,
              }
            },
            y: {
              beginAtZero: false
            }
          },
          plugins: {
            legend: {
              display: false // 상단의 레전드 보이기/숨기기
            },
            tooltip: {
              enabled: false, // 기본 툴팁 비활성화
              external: function (context) {
                // 1. 툴팁 엘리먼트 생성 또는 가져오기
                let tooltipEl = document.getElementById('chartjs-tooltip');
                const tooltipModel = context.tooltip;

                // 툴팁 엘리먼트가 없다면 생성
                if (!tooltipEl) {
                  tooltipEl = document.createElement('div');
                  tooltipEl.id = 'chartjs-tooltip';
                  tooltipEl.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
                  tooltipEl.style.borderRadius = '3px';
                  tooltipEl.style.color = 'white';
                  tooltipEl.style.opacity = 1;
                  tooltipEl.style.pointerEvents = 'none';
                  tooltipEl.style.position = 'absolute';
                  tooltipEl.style.transform = 'translate(-50%, 0)';
                  tooltipEl.style.transition = 'opacity 0.2s ease, left 0.2s ease, top 0.2s ease';
                  tooltipEl.style.padding = '10px';
                  document.body.appendChild(tooltipEl);
                }
                // 툴팁 숨김 상태 처리
                if (tooltipModel.opacity === 0) {
                  tooltipEl.style.opacity = 0;
                  return;
                }
                tooltipEl.style.opacity = 1;

                // 2. 툴팁의 텍스트 및 데이터 설정
                const label = tooltipModel.dataPoints[0].label;
                const dataIndex = tooltipModel.dataPoints[0].dataIndex;
                const data = tooltipModel.dataPoints[0].dataset.data[dataIndex];
                const parts = label.split(", ");
                const dateString = parts[0] + " " + parts[1];
                const date = new Date(dateString);
                const month = String(date.getMonth() + 1).padStart(2, '0');
                const day = String(date.getDate()).padStart(2, '0');
                const difference = data['c'] - data['o'];
                const formattedDate = `${month}/${day} <span style="font-size: 20px; font-weight: bolder; color: ${difference >= 0 ? 'green' : 'red'};">${difference}</span>`;

                tooltipEl.innerHTML = `
                  <div style="font-size: 16px; font-weight: normal; line-height: 1.5rem;">${formattedDate}</div>
                  <div style="font-size: 14px;">기점: ${data.o}</div>
                  <div style="font-size: 14px;">고점: ${data.h}</div>
                  <div style="font-size: 14px;">저점: ${data.l}</div>
                  <div style="font-size: 14px;">종점: ${data.c}</div>
                  </>
                `;

                // 3. 툴팁의 위치 설정
                const position = context.chart.canvas.getBoundingClientRect();
                tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px';
                tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px';
              }
            }
          },
          backgroundColors: {
            up: 'rgba(75, 192, 192, 0.5)',
            down: 'rgba(255, 99, 132, 0.5)',
            unchanged: 'rgba(201, 203, 207, 0.5)',
          },
          borderColors: {
            up: 'rgb(75, 192, 192)',
            down: 'rgb(255, 99, 132)',
            unchanged: 'rgb(201, 203, 207)',
          }
        }
      }
    }
  },
  methods: {
    renderChart() {
      const ctx = this.$refs.candleChart.getContext('2d');
      this.chart = new Chart(ctx, this.chartConfig);
    },
    getChartData() {
      return this.generateRandomData();
    },
    generateRandomData(initial = 9000, count = 10, changeRange = 4000) {
      let o = initial,
          h,
          l,
          c,
          x = new Date("2024-08-24").getTime();
      return [...Array(count)].map((_, i) => {
        o = c || o;
        h = o + this.randomChange(changeRange) / 2;
        l = o - this.randomChange(changeRange) / 2;
        c = o + (this.randomChange(changeRange) - changeRange / 2);
        x += 24 * 60 * 60 * 1000;
        return {
          o,
          h,
          l,
          c,
          x,
        };
      });
    },
    randomChange(range = 1000) {
      return Math.round(Math.random() * range);
    },
  }
};

 

 


디자인은 Figma를 통해 디자인했는데, 필요한 svg는 figma에서 그려서 저장하고 사용할 수 있는게 장점으로 다가왔다.

그리고 디자인한 프로토 타입을 플러그인을 통해 원하는 프레임워크의 형태로 받아 볼 수 있다는 것 또한 좋았다.

 

웹 디자인을 처음해보다 보니 뭐가 맞을까 라는 생각은 잘 못하고 단지 친구들에게 "이쁜 것 같냐" 라는 말을 많이 한 것 같다.

 

다만 아쉬웠던 것은, 반응형으로 제공되는 것이 아닌, 모든 div가 absolute, top, left 속성으로 설정되어 있었다는 것이다.

그래도 컴포넌트 구조는 내가 그룹으로 작업했던 대로 만들어줘서 나쁘지 않았다 라는 생각이 들었다

figma에서 디자인해서 프로토타입을 만들던 과정

 

디자인적으로 기존의 서비스와 차별화를 두고 싶었고, 다른 전적 분석 사이트(lol.ps)에서 생성형 AI의 평가를 기반한 통계 데이터 제공 기능 또한 적용하고 싶었다. 그러다 보니 시간을 많이 들이게됐던 것 같다.

 

처음 vue를 사용해보면서 SPA 와 컴포넌트 기반 구조와 props, emit, computed, method, data(), mount() 등 의 페이지 로딩과정의 데이터 흐름을 이해하는 것이 내 프론트 개발의 첫 걸음 이었다.

프로젝트 규모가 커지고 컴포넌트가 복잡도해질 수록 데이터 공유의 어려움을 겪다보니 자연스럽게 Vuex를 사용하게 됐다.

 

Vuex를 통해 상태의 일관성과 중앙 관리가 이루어지면서 복잡한 데이터 흐름도 깔끔하게 관리할 수 있었다는 점이 유익했다.

마치. 언제 어디서나 손이 닿는 코드 보관소가 생기는 느낌이었다. 하지만 프론트는 비동기적인 영향을 많이 받다보니, 상태 관리의 중요성을 실감할 수 있었다.

 

그리고 트랜지션...

버튼을 눌렀을 때, 서로 다른 html 객체에 애니메이션을 적용한다고 했을 때, 

진행에 시간차가 발생하는 것이다. 이것은 내가 비동기 처리에 대해 공부해보면 될 것 같은 문제 같다.

 

프론트엔드를 개발하기 전에 디자인만 갖고 개발하는 것이 아니라. 데이터 구조라던가, 그런 것들을 미리 설계하고 개발했다면 시간이 조금 단축되지 않았을까 하는 생각도 든다.

 

우선! PlayerStats 페이지의 Header 와 좌측 stat 컴포넌트의 구현이 어느정도 되었으니, 

다시 백엔드 작업을 해보자..

 

 

Comments