0363

FE

[SvelteKit] Chart.js와 date-fns로 만드는 관리자 대시보드

| 서론

안녕하세요, 팡일입니다.

현재 오픈 미션으로 re-log라는 서비스를 개발하고 있는데요? re-log는 사용자가 꾸준히 자신의 생각과 감정을 기록하며 성장할 수 있도록 돕는 회고 서비스입니다. 서비스를 운영하면서 “지금 사용자들이 얼마나 활발하게 활동하고 있는가?”, “언제 가입과 작성이 집중되는가?” 같은 지표가 궁금해졌고, 이러한 흐름을 눈으로 확인할 수 있도록, 관리자 페이지에 데이터 대시보드를 새롭게 만들게 되었습니다.

이번 글에서는 그 대시보드를 구성하는 핵심 요소인 데이터 시각화를 어떻게 구현했는지, 그리고 그 과정에서 Chart.js date-fns를 활용한 방법을 소개하려 합니다.

| 왜 대시보드가 필요했을까?

어느 날 가입자가 많았는지, 회고는 어떤 패턴으로 작성되고 있는지, 서비스는 지금 성장하는 중인지 등에 대해서 파악하기 위해서는 대시 보드가 필요했습니다. 그래서 저는 이러한 대시보드를 위해서 어떤 기능이 필요할지 고민해본 결과 아래와 같은 목표를 세웠습니다.

  1. 대시보드 상단에 핵심 지표(KPI) 표시→ 총 사용자 수, 총 회고 수 등

  2. 최근 30일 기준의 추세를 차트로 시각화→ 일일 회고 작성 수→ 일일 가입자 수

이 목표를 위해 개발 과정을 데이터 가공 단계 시각화 단계로 나누어 진행했습니다.

| 데이터 가공 — date-fns로 날짜를 다루기 쉽게

Firestore에서 가져오는 createdAt은 Timestamp 객체라서 그대로는 날짜별 집계가 어렵습니다. 물론 직접 변환 함수를 만들어 처리할 수도 있지만, 실제로 개발을 진행하면서 date-fns를 선택한 이유를 찾아볼 수 있었습니다.

1) 불필요한 고민 없이 바로 사용 가능(윤년, 월 넘김, 타임존 문제 등…)

: date-fns는 이런 부분이 이미 잘 다듬어져 있어 “그냥 가져다 쓰면 되는” 편리함이 컸습니다. 날짜를 “yyyy-MM-dd” 같은 형식으로 바꾸거나, N일 전 날짜를 계산하는 기능은 직접 구현하면 에지 케이스가 너무 많습니다.

2) 함수 이름이 직관적이라 읽기 쉬움

: 협업 시에도 설명이 필요 없어서 유지보수가 쉬웠습니다. format, toDate, subDays 같은 이름은 보자마자 무슨 일을 하는지 알 수 있죠.

3) 날짜 연산이 반복될 때 코드를 깔끔하게 유지할 수 있음

: 날짜 배열 생성, 일자 비교, 포맷팅 같은 작업을 계속 반복하다 보면, 직접 만든 유틸 함수보다 검증된 라이브러리가 훨씬 안정적이고 코드도 짧아질거라 생각했습니다.

즉, date-fns를 쓰면서 “날짜 계산 때문에 시간을 잡아먹을 필요가 없어졌다”는 게 가장 큰 이점이었습니다. 결과적으로 코드가 더 명확해졌고, 구현 속도도 빨라졌습니다.

1) 사용 사례 1: Timestamp → 날짜 키로 변환해 집계하기

Firestore에서 데이터를 가져오면 createdAt 값은 Timestamp 형태라, 이를 그대로는 “2025-11-17에 몇 명 가입했는가?” 같은 집계를 하기에 까다로웠습니다. 따라서 가장 먼저 해야 했던 일은 Timestamp를 사람이 이해할 수 있는 날짜 문자열로 변환하는 작업이었습니다.

아래 코드는 사용자 목록을 순회하면서, 각 사용자의 가입 날짜를 yyyy--dd 형태의 문자열로 만들고, 그 날짜별로 카운트를 누적합니다.

TypeScript
import { format, toDate } from 'date-fns';

const dailySignups = users.reduce((acc, user) => {
  const date = toDate(user.createdAt);
  const dateKey = format(date, 'yyyy-MM-dd');

  acc[dateKey] = (acc[dateKey] || 0) + 1;
  return acc;
}, {} as { [key: string]: number });
TypeScript
// 결과
{
  "2025-11-16": 5,
  "2025-11-17": 12
}

여기서 중요한 점은, 날짜를 ‘문자열 키’로 통일해야 이후의 차트 생성이 훨씬 쉬워진다는 것입니다. 차트 데이터는 결국 배열(index 기반)로 보여주기 때문에, 날짜별 집계를 이렇게 미리 정리해두면 이후 단계에서 데이터를 매핑하기가 매우 간단해집니다.

즉, 이 단계는 차트에 들어갈 ‘원본 데이터의 형태를 표준화하는 과정’이라고 볼 수 있습니다.

2) 사용 사례 2: 최근 30일 날짜 배열 생성하기

날짜별 집계를 만들었다고 해서 바로 차트를 만들 수 있는 건 아닙니다. 예를 들어, 특정 날짜에 가입자가 ‘0명’인 날은 dailySignups 객체에 아예 키가 생성되지 않습니다.

하지만 차트에서는 가입자 유무와 상관없이 모든 날짜가 연속적으로 표시되어야 합니다. 즉, “빈 날(데이터가 없는 날)”도 함께 보여줘야 추세가 자연스럽게 이어지죠.

이를 해결하기 위해 최근 30일의 날짜를 완전한 배열 형태로 생성하고, 각 날짜를 기준으로 dailySignups에서 값을 찾아 매핑하는 과정이 필요했습니다.

TypeScript
import { subDays, startOfToday, eachDayOfInterval, format } from 'date-fns';

const endDate = startOfToday();
const startDate = subDays(endDate, 29);

// 1) 최근 30일에 해당하는 모든 날짜를 배열로 생성
const dateLabels = eachDayOfInterval({ start: startDate, end: endDate })
  .map(date => format(date, 'yyyy-MM-dd'));

// 2) 날짜 배열을 기준으로 집계 값 매핑 (없으면 0)
const chartDataValues = dateLabels.map(label => dailySignups[label] || 0);

이 과정이 중요한 이유는 날짜별로 “빈 날”이 있어도 X축이 끊기지 않고, 값이 없던 날을 자동으로 0으로 채워 차트가 부드럽게 이어지는 형태로 그려지기 때문입니다.

즉, 이 단계는 차트가 “시간 흐름에 따라 자연스럽게 이어지도록” 빈 날짜를 포함한 완전한 타임라인을 만드는 과정이라고 할 수 있습니다.

| 차트 시각화 — Chart.js로 직관적인 그래프 만들기

데이터 가공을 마쳤다면, 이제는 보기 좋게 표현할 차례입니다. 이를 위해 선택한 라이브러리가 Chart.js입니다. 단순히 “차트 라이브러리를 하나 선택했다”가 아니라, 실제로 개발하면서 Chart.js가 적합하다고 느낀 이유는 분명했습니다.

1) 커스터마이징이 자유롭고 바로 적용하기 쉬움

: 기본적인 라인 차트에서 색상, 곡률, 툴팁, 배경 채우기까지 대부분의 시각 요소를 바로 조정할 수 있습니다. “기능은 좋은데 스타일링이 어렵다” 같은 걱정 없이, 서비스 UI에 맞춰 빠르게 꾸밀 수 있었던 점이 큰 장점이었어요.

2) Svelte와 자연스럽게 결합됨

: <canvas>에 bind만 하고, onMount에서 Chart를 초기화하면 바로 렌더링됩니다. DOM 준비 → 차트 생성 → 데이터 변경 → chart.update() 흐름이 Svelte의 생명주기와 딱 맞아떨어져서 사용감이 좋았습니다.

3) 배우기 쉽고 예제가 많아 문제 해결 속도가 빠름

: 문서가 친절하고, 검색하면 대부분의 설정 예제나 이슈가 이미 정리돼 있습니다. 복잡한 차트를 만드는 게 아니라 “관리자용 추세 그래프”라면 Chart.js만큼 빨리 결과를 낼 수 있는 라이브러리가 거의 없어요.

즉, Chart.js를 쓰면서 “시각화 때문에 시간을 빼앗길 일”이 줄어들었습니다. 빠르게 그릴 수 있고, 원하는 디자인대로 꾸밀 수 있고, 유지보수도 어렵지 않다는 점에서 가장 현실적인 선택이었습니다.

1) 사용 사례: Svelte에서 라인 차트 컴포넌트 만들기

아래는 Chart.js로 라인 차트를 구축하기 위해 만든 Svelte 컴포넌트 예시입니다. 이 컴포넌트는 부모가 전달하는 labels values만 있으면 자동으로 차트를 그려주기 때문에, 대시보드에서 재활용성이 높고 유지보수가 간단합니다.

TypeScript
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { Chart, registerables } from 'chart.js';

  // Chart.js v3부터는 필요한 요소를 직접 등록해야 합니다.
  Chart.register(...registerables);

  // 부모 컴포넌트에서 날짜 배열(labels)과 값(values)을 전달받습니다.
  export let chartData: { labels: string[]; values: number[] };
  export let label: string;

  let canvasElement: HTMLCanvasElement;
  let chart: Chart;

  // 컴포넌트가 DOM에 마운트된 후 차트를 초기화합니다.
  onMount(() => {
    const ctx = canvasElement.getContext('2d');
    if (!ctx) return;

    chart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: chartData.labels,
        datasets: [{
          label,
          data: chartData.values,
          borderColor: 'rgb(75, 192, 192)',
          backgroundColor: 'rgba(75, 192, 192, 0.2)',
          tension: 0.1,
          fill: true,
        }]
      },
      options: {
        responsive: true,
        scales: {
          y: {
            beginAtZero: true,
            ticks: { stepSize: 1 }
          }
        }
      }
    });
  });

  // 컴포넌트가 제거될 때 차트도 함께 제거해 메모리 누수를 방지합니다.
  onDestroy(() => chart?.destroy());

  // Svelte의 반응성: 전달받은 데이터가 변하면 차트를 즉시 업데이트합니다.
  $: if (chart && chartData) {
    chart.data.labels = chartData.labels;
    chart.data.datasets[0].data = chartData.values;
    chart.update();
  }
</script>

<div style="position: relative; height:40vh; width:80vw">
  <canvas bind:this={canvasElement}></canvas>
</div>

(1) onMount에서 차트 초기화

: 캔버스 요소가 준비된 뒤 Chart.js 인스턴스를 생성합니다. Svelte는 DOM 접근이 필요한 작업을 onMount에서 처리하므로 자연스럽게 이곳에서 초기화합니다.

(2) 반응성($:)을 통해 데이터 변경 시 차트 업데이트

: Firestore에서 데이터를 다시 불러오는 경우처럼 chartData가 변하면 $ 구문이 실행되어 차트 데이터를 교체하고 chart.update()로 즉시 반영합니다.

(3) onDestroy에서 차트 정리

: 페이지 이동 시 차트 인스턴스가 계속 남지 않도록 chart.destroy()를 호출해 메모리 누수를 방지합니다.

마지막으로 <canvas bind:this={canvasElement}>로 캔버스를 변수와 연결해 Chart.js가 그릴 수 있는 실제 DOM을 참조하게 됩니다.

즉, 이 컴포넌트는 초기화 → 데이터 반응 → 정리의 흐름을 Svelte 생명주기와 자연스럽게 결합해 대시보드에서 재사용하기 쉬운 형태로 깔끔하게 구성한 예시입니다.

| 마무리

이번 관리자 대시보드 구현은 단순한 차트 두 개를 그리는 작업이 아니라, 서비스 데이터를 이해하고 개선하기 위한 첫 번째 시도였습니다.

  • date-fns로 데이터를 의미 있게 가공하고

  • Chart.js로 시각적 맥락을 더해주며

  • 관리자 입장에서 바로 활용 가능한 정보로 재탄생

앞으로 이 대시보드를 기반으로 서비스의 흐름을 더 잘 파악하고, 더 나은 사용자 경험으로 이어질 수 있도록 계속 발전시켜 나갈 예정입니다.