Tech

빅파이낸스 히트맵, 강력하면서도 부드럽게 만들기

송상수
2024-06-01

유튜브 경제방송 삼프로TV 를 시청하다 보면 심심치 않게 산업별, 기업별 주가와 관련된 각종 데이터를 접하게 됩니다. 삼프로TV에서 활용하는 이러한 각종 지표 데이터는 ‘빅파이낸스(BigFinance)’를 기반으로 하고 있습니다. 삼프로TV 회원을 포함해 여러 고객들이 사용 중인 ‘빅파이낸스’, 주요 기능인 히트맵 개발 과정에 대해 개발을 담당한 개발자의 시각으로 설명해보고자 합니다.

기업정보 모두 담은 ‘빅파이낸스’, 추가기능 달고 삼프로TV까지 진출
(빅파이낸스의 최근 추가기능에 대한 소개는 위 콘텐츠 참조)

아래와 같은 화면을 보신 적이 있으신가요? 아마 주식 투자에 관심이 있으셨다면 눈에 익은 화면일지도 모르겠습니다.

TradingView의 Stock Heatmap 화면
TradingView의 Stock Heatmap 화면

주로 미국 주식시장을 다루는 매체들에서 종종 보이는 화면입니다. 전체 시장이나 특정 산업, 또는 개별종목의 주가의 상승∙하락 추이를 한 눈에 볼 수 있고, 그 추이를 벗어나는 주식의 존재를 쉽게 확인하는 등 시장의 흐름을 직관적으로 파악할 수 있는 것이 장점이죠. 통일된 명칭은 없지만 보통 주식 맵, 히트맵이라고 불리우는데요. 최근, 빅파이낸스가 버전 1.7로 업데이트되며 국내 주식시장을 대상으로 히트맵 기능을 선보였습니다.

빅파이낸스 히트맵 화면
빅파이낸스 히트맵 화면

사용자가 많은 정보를 편하게 확인할 수 있도록 신경 쓴 요소들을 곳곳에서 찾아볼 수 있는데요. 마우스를 올리면 특정 산업의 상위 종목을 함께 확인할 수 있으며, 개별 종목의 세부정보가 있는 대시보드 페이지가 히트맵과 연동돼 관련 기업의 정보를 곧장 살펴볼 수 있습니다. 그 외에도 등락율이나 시가총액의 범위를 필터로 설정해 해당 구간에만 적용되는 기업을 조회하는 등 다양한 편의 기능들이 있습니다. 이외에도 많은 기능들이 계속 추가될 예정입니다.

이번 글에서는 이 빅파이낸스 히트맵을 개발하며 적용한 다양한 기술들을 살펴보면서, 에이셀테크놀로지스 개발팀이 어떻게 함께 일했는지, 그리고 그 과정에서 어떤 것들을 배우고 느꼈는지 공유하고자 합니다.

다루기 어렵지만 다재다능한 d3

빅파이낸스 히트맵 개발에서 중요하게 쓰인 도구는 d3입니다. 프론트엔드 개발에 대해 친숙하지 않더라도, 데이터 시각화나 데이터 아트를 접해보신 분이라면 아마 d3의 난이도에 대한 풍문을 들어보셨을 지도 모르겠습니다.

“만약 당신이 d3 세계에 발을 들여놓으려 한 적이 있었다면, 이미 그 악명 높은 난이도에 친숙할 것입니다…”
- Adam Janes, 데이터 시각화 엔지니어
(5 Crucial Concepts for Learning d3.js and How to Understand Them)

데이터를 직접 차트 등의 형태로 보여주는 다른 라이브러리와 달리 d3는 데이터만을 가공하고, 점, 선, 면 등의 화면 표현은 개발자가 전적으로 처리합니다. 화면을 직접 표현하는 것이 쉽지 않지만 이는 커스터마이징의 범위 또한 넓다는 것을 의미하고, 그렇기 때문에 다른 라이브러리가 지원하지 않는 특수한 화면을 나타내려면 d3의 힘을 빌려야 하는 경우가 종종 있습니다.

빅파이낸스 히트맵 프로젝트를 맡은 개발팀도 d3를 최종적으로 선택하기 전 다른 라이브러리를 리서치했습니다. 기존에 사용하던 Highcharts, 그리고 Apache EChartsTOAST UI의 세 라이브러리가 구현하고자 하는 기능들을 가장 많이 지원했고, 이를 기반으로 프로토타입을 만들어보기로 결정했습니다. 그러나 프로토타입을 만들어본 결과 쉽게 해결하기 어려운 한계점들이 있었습니다. 특히 텍스트 표현의 자유도가 제한적이었죠.

TOAST UI 라이브러리로 만든 프로토타입
Highcharts 라이브러리로 만든 프로토타입

TOAST UI 라이브러리로 만든 프로토타입

다른 라이브러리가 제공하는 기능의 범위도 좁고 개발에 따른 소요시간도 길다면 d3를 선택하지 않을 이유가 없었습니다. ① 다중 데이터를 트리맵으로 표현하는 예제와 ② 트리맵 내 계층 간 전환되는 예제를 찾은 뒤 이를 참고하여 본격적인 개발을 시작하게 됩니다.

데이터 가공, 그리고 SVG를 사용한 최초의 화면 표현

빅파이낸스 히트맵은 ① 산업군, ② 주식 종목의 2단계로 구성되어 있습니다.

히트맵을 나타내려면 우선 준비물이 필요합니다. 각 사각형의 크기, 산업군별 종목 정보, 색상, 텍스트의 내용, 텍스트의 위치 같은 정보들이죠. 미국의 S&P 500과 우리나라 상장 기업 목록의 일부를 활용해 d3 Hierarchy 모듈의 treemap 메소드에 입력해주었습니다. 이 메소드는 입력받은 데이터를 계층화하고, 전체 대비 특정 개체의 비중을 계산한 뒤 x, y 좌표 등 필요한 정보를 매핑해줍니다. 이 정보를 활용해 히트맵을 화면에 그려주는 것이죠.

이렇게 가공된 데이터를 기반으로 화면을 표현하기 위해 SVG를 사용했습니다. SVG는 Scalable Vector Graphics의 약자로, 2차원으로 벡터 그래픽을 표현할 수 있는 언어입니다. 벡터 그래픽 기반이기 때문에, 확대하더라도 해상도의 손실이 없습니다. 또한 d3를 활용한다면 DOM 상의 SVG 요소에 자체적으로 데이터를 매핑해줄 수도 있습니다. 마우스 이벤트 등 추가적인 처리할 때 여러모로 간편합니다. 그렇기에 앞서 참고하던 예제들 또한 SVG 방식을 사용하고 있었죠.

하지만 이 방식은 본질적인 한계가 있습니다. 데이터가 많아질 경우 그에 대응되는 DOM 요소가 늘어나고 그에 따라 렌더링이 느려진다는 것이죠. 빅파이낸스 역시 입력 데이터를 바꾸는 과정에서 동일한 현상을 겪었습니다. 수백 개에 불과한 더미 데이터를 3천 개에 달하는 실제 데이터로 변경하자 급격하게 성능이 저하되었던 것입니다.

개발자 도구를 사용해 성능을 측정해본 결과 너무 많은 DOM 요소로 인한 화면 표현이 낮은 성능으로 이어졌다는 것을 확인할 수 있었습니다.

너무 많은 DOM 요소가 화면에 그려짐 — Lighthouse 탭 측정 결과

화면 확대/축소가 스타일링 관련 많은 시간을 소요하고 있음 — Performance 탭 측정 결과

Google은 화면에 표현되는 DOM 요소를 1,400개 이내로 제한할 것을 권장하고 있지만, 빅파이낸스에 SVG 방식으로 구현한 화면은 2만 개에 가까운 요소를 보여주고 있었습니다. 이는 화면 확대/축소와 같이 많은 요소가 변경될수록 높은 부하로 이어집니다. 위의 Performance 측정 결과에서 보이는 것처럼 스타일 변경과 관련된 연산 (Recalculate Style, Layout)에 매번 0.2초 이상에 소요되고 있습니다. 이런 지연들이 모여 화면이 멈추거나 끊기는 것처럼 보이는 등 사용자 경험을 저하시키게 됩니다.

SVG 방식으로는 화면에 표현되는 DOM 요소가 많다는 본질적인 한계를 해결할 수 없었기에, 개발팀은 빅파이낸스의 화면 표현 방식을 Canvas로 바꾸기로 결정했습니다.

Canvas, 손이 많이 가는 화면 표현 방식

그렇다면 Canvas 방식은 무엇일까요? SVG 방식과 달리, Canvas 방식에서는 각 데이터가 DOM 요소로 표현되지 않습니다. 이 말은, 처음과 화면 구성이 달라질 경우 다시 화면을 표현해줘야 한다는 것을 의미합니다. 최초 화면의 요소들을 재활용할 수 있는 SVG 방식에 비해 여러 가지를 추가로 고려해줘야 하죠. 그 중 많은 영향을 끼쳤던 몇 가지를 말씀드려볼까 합니다.

우선, 방금 말씀드린 것처럼 Canvas 방식은 화면 재활용이 불가능합니다.

화면을 확대하거나 축소했을 때, 확대한 채로 이동했을 때 화면을 다시 표현해줘야 하죠. Canvas는 래스터 방식이라서 단순히 배율을 확대할수록 픽셀 사이의 경계선이 흐려지게 됩니다. 그렇기 때문에 확대하거나 축소할 때 매번 다시 그려주는 함수를 호출하고, 이 때 확대 배율을 적용하여 다시 그려줌으로써 흐려지는 문제를 해결했습니다.

CSS transform 속성을 적용하여 확대한 화면 — 글자의 경계가 흐릿함

다음으로는 클릭이나 이동 같은 마우스 이벤트가 발생했을 때 어떤 데이터를 대상으로 하는지 일일이 지정해줘야 합니다.

SVG 방식에서는 DOM 요소들이 존재하기 때문에 마우스 이벤트가 발생한 요소를 특정하기 쉽고, 그 요소에 저장된 데이터를 활용할 수 있습니다. 하지만 Canvas 방식에서는 상응하는 DOM 요소가 없으므로 마우스 이벤트가 발생할 때마다 이벤트의 좌표를 확인하고 해당하는 데이터를 찾는 단계가 추가로 필요합니다.

이를 위해 아래와 같은 코드를 추가해주었습니다. 코드 자체는 단순하지만, 확대/축소 등 화면이 바뀔 경우에는 입력받는 x, y 좌표를 추가적으로 가공해줘야합니다.

function findCompanyAtCursor(
  [x, y]: [number, number],
  root: d3.HierarchyRectangularNode,
): d3.HierarchyRectangularNode | null {
  const companies = root.children;
  if (companies) {
    for (const company of companies) {
      const { x0, x1, y0, y1 } = company;
      const isCursorInsideCompany = x >= x0 && x <= x1 && y >= y0 && y <= y1;
      if (isCursorInsideCompany) {
        return company as unknown as d3.HierarchyRectangularNode;
      }
    }
  }
  return null;
}

마우스와 관련하여 하나 더. 마우스를 특정 종목 위로 움직였을 때 색상을 바꿔주는 것 같은 강조 효과는 어떻게 구현해야 할까요?

SVG 방식에서라면 쉽습니다. 종목마다 DOM 요소가 존재하니 해당 요소에 CSS 속성을 적용해주면 됩니다. 하지만 Canvas 방식에서는 그런 게 없죠. 마우스 커서가 움직일 때마다 어떤 종목인지 파악하고, 해당하는 영역을 강조 색상으로 칠해줘야 합니다. 여기서 끝난 게 아닙니다. 커서가 다른 종목으로 이동했을 때 이전 종목의 영역을 다시 원래 색상으로 되돌리고 새로운 종목을 강조해줘야 하죠.

이런 번거로움을 Canvas를 하나 더 추가함으로써 해결했습니다. Canvas 요소는 기본적으로 투명하다는 속성을 갖고 있습니다. 이를 활용해 기존과 동일한 크기의 Canvas (이하 오버레이 Canvas)를 만들고, 마우스 커서가 위치한 종목이 바뀔 때마다 오버레이 Canvas 전체를 초기화하고 새 종목의 영역을 칠해주도록 했습니다. 코드는 아래와 같습니다.

d3.select(canvas).on(
  'mousemove',
  function (e) {
    const { left, top } = canvas.getBoundingClientRect();
    const x = e.clientX - left;
    const y = e.clientY - top;
    const [adjustedX, adjustedY] = adjustCoordinatesByZoomTransform([x, y]);
    const targetCompany = findCompanyAtCursor([adjustedX, adjustedY], root);
    if (targetCompany) {
      drawCompanyHoverEffect();
    }
  }
);

d3.select(canvas).on(
  'mouseleave',
  function () {
    overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
  }
);

글자 표현과 관련해서도 많은 부분을 신경썼습니다.

HiDPI 디스플레이, 즉 정밀도가 높은 디스플레이가 점점 더 많아지는 추세인데요. 레티나 디스플레이를 사용하는 Apple 디바이스가 대표적이라고 할 수 있겠습니다. 그런데 이런 고정밀 디스플레이에서 Canvas 방식으로 화면으로 표현하게 되면 아래처럼 ‘흐릿해’ 보입니다.

디바이스별 정밀도를 보정하기 전과 후의 글자 표현

이런 디스플레이들이 화면을 더 정밀하게 표현하기 위해 1 픽셀을 여러 픽셀로 표현하고 있기 때문입니다. Window의 devicePixelRatio 속성으로 각 디스플레이의 정밀도를 파악할 수 있기 때문에, 아래의 코드를 통해 디스플레이의 정밀도가 높아지더라도 화면이 정상적으로 표현되도록 하였습니다.

function enhanceClarityByDevice() {
  const pixelRatio = window.devicePixelRatio || 1;
  const modifiedWidth = Math.floor(containerWidth * pixelRatio);
  const modifiedHeight = Math.floor(containerHeight * pixelRatio);

  d3.select(canvas).attr('width', modifiedWidth).attr('height', modifiedHeight);
  ctx.scale(pixelRatio, pixelRatio);
}

뿐만 아니라, 최초 렌더링 시 간헐적으로 기본 글꼴로 표현되는 문제가 있었습니다. 최초로 렌더링되는 시점에 해당 글꼴이 다운로드되지 않아서 발생하는 문제였는데요, 이를 해결하기 위해 여러 방법을 적용해보다가, 최종적으로 index.html의 head 부분에 아래와 같은 <link> 태그를 삽입하여 글꼴을 미리 불러오는 방식을 택했습니다.


<link rel="preload" href="" as="font" type="font/woff2" crossorigin>

함께 만들어낸 BigFinance 히트맵

빅파이낸스 히트맵을 개발하는 과정에서 에이셀테크놀로지스의 프론트엔드 팀은 매일 회의를 하며 진행 상황과 어려움을 맞닥뜨린 부분, 도움이 될 만한 내용 등을 공유했습니다. d3를 처음 사용하면서 예제를 찾아 헤맬 때, 실제 데이터를 적용하자 급격히 느려졌을 때, 디스플레이 별로 텍스트의 선명도가 다를 때 등 겪어보지 못했던 문제들이 정말 많았지만 함께 논의하는 과정을 통해 더 효율적인 해결책을 짧은 시간 안에 적용할 수 있었습니다.

또한 사용자가 필요로 하는 기능들을 선별하고자 매주 1회 이상 이해관계자들과 회의하며 개발 방향을 유지했습니다. 이 덕분에 고객 관점에서 유의미하게 활용될 수 있는 핵심 기능 개발에 집중할 수 있었습니다. 사이드 프로젝트로 시작했던 히트맵은 어느새 서비스의 메인화면에 위치하게 되었고, 삼프로TV에서도 빅파이낸스 히트맵을 적극 활용하는 등 중요한 기능으로 자리잡았습니다. 부담감이 있었던 것도 사실이지만, 개발을 잘 마치고 성공적으로 동작해 많은 사용자들에게 쓰이는 것을 봤을 때의 성취감은 이루 말할 수 없었습니다.

서비스 자체의 완성 뿐 아니라, 프론트엔드 개발자 개인으로서도 성장 할 수 있었던 프로젝트였습니다. 데이터 시각화 분야에서 상징적인 입지를 갖고 있는 d3를 익혔고 Canvas를 통해 2차원 좌표를 통한 화면 표현에 익숙해졌으며, 브라우저 렌더링 과정에 대한 이해를 다시금 정비하였고 성능 문제를 체계적으로 측정하고 대응했습니다. 이 외에도 텍스트 표현과 관련된 다양한 이슈를 접하고 해결했습니다.

빅파이낸스 히트맵에서의 경험을 발판 삼아 지속적으로 사용자 경험과 성능 모두를 충족시키는 프로덕트 개발을 지향하고자 합니다.

up to nav link