[실시간 트레이딩 대시보드 만들기 - 02] TradingView Charts로 실시간 캔들 차트 구현

2026. 2. 12. 11:55·Frontend Development/[실습] 실시간 트레이딩 대시보드 만들기
728x90
반응형

업비트 실시간 대시보드 시리즈 2편입니다. 이전 글에서 WebSocket 파이프라인을 구축했고, 이번에는 그 위에 캔들 차트를 올립니다. REST API로 과거 200개 캔들을 불러오고, WebSocket 체결 데이터로 마지막 캔들을 실시간 업데이트하는 구조입니다.

전체 소스코드는 GitHub에 공개되어 있습니다.

git clone https://github.com/kimkuns91/upbit-realtime-dashboard.git
cd upbit-realtime-dashboard
npm install && npm run dev


1. 왜 TradingView Lightweight Charts인가?

금융 차트 라이브러리를 고를 때 D3.js, Recharts, ECharts 등을 검토했습니다. 결론은 Lightweight Charts 일택이었습니다.

  • Canvas 기반: SVG 기반 라이브러리(D3, Recharts)와 달리 수천 개 데이터 포인트에서도 성능 문제가 없습니다
  • 금융 특화: 캔들스틱, 볼륨 히스토그램, 크로스헤어 등이 내장되어 있습니다
  • 가볍다: gzip 기준 약 45KB. TradingView 풀 라이브러리의 1/10 수준입니다
  • TypeScript 지원: v5부터 타입이 더 정교해졌습니다

npm install로 바로 들어가는 금융 차트 라이브러리 중에서는 가장 실용적인 선택이었습니다.

2. CORS 프록시: Next.js Route Handler

업비트 REST API는 브라우저에서 직접 호출하면 CORS 에러가 납니다. Next.js의 Route Handler로 서버 사이드 프록시를 만들었습니다.

// src/app/api/candles/route.ts
export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl;
  const market = searchParams.get('market');
  const unit = searchParams.get('unit'); // 분봉 단위 또는 'days'

  // 분봉/일봉에 따라 업비트 API 엔드포인트 결정
  let url: string;
  if (unit === 'days') {
    url = `${UPBIT_API_BASE}/candles/days?market=${market}&count=${count}`;
  } else {
    url = `${UPBIT_API_BASE}/candles/minutes/${unit}?market=${market}&count=${count}`;
  }

  const response = await fetch(url, {
    headers: { Accept: 'application/json' },
  });
  const data = await response.json();
  return NextResponse.json(data);
}

클라이언트에서는 /api/candles?market=KRW-BTC&unit=1&count=200으로 호출하면 됩니다. 서버 사이드에서 업비트 API를 호출하므로 CORS 이슈가 없습니다.

3. 차트 컴포넌트 구현

Lightweight Charts v5에서는 시리즈 추가 방식이 바뀌었습니다. 문자열 대신 SeriesDefinition 객체를 import해서 사용합니다. 처음에 v4 문법으로 작성했다가 타입 에러를 만났습니다.

import {
  createChart,
  CandlestickSeries,  // v5: SeriesDefinition 객체
  HistogramSeries,
  ColorType,
} from 'lightweight-charts';

// 차트 인스턴스 생성
const chart = createChart(containerRef.current, {
  layout: {
    background: { type: ColorType.Solid, color: '#1a1a2e' },
    textColor: '#a0aec0',
  },
  grid: {
    vertLines: { color: '#2a2a4a' },
    horzLines: { color: '#2a2a4a' },
  },
});

// v5 방식: addSeries(SeriesDefinition, options)
const candleSeries = chart.addSeries(CandlestickSeries, {
  upColor: '#ef4444',     // 상승: 빨강 (한국 기준)
  downColor: '#3b82f6',   // 하락: 파랑
  wickUpColor: '#ef4444',
  wickDownColor: '#3b82f6',
});

// 볼륨 히스토그램 (차트 하단 20%)
const volumeSeries = chart.addSeries(HistogramSeries, {
  priceFormat: { type: 'volume' },
  priceScaleId: 'volume',
});
chart.priceScale('volume').applyOptions({
  scaleMargins: { top: 0.8, bottom: 0 },
});

반응형은 ResizeObserver로 처리했습니다. 컨테이너 크기가 변하면 차트도 따라 리사이즈됩니다.

const resizeObserver = new ResizeObserver((entries) => {
  const { width, height } = entries[0].contentRect;
  chart.applyOptions({ width, height });
});
resizeObserver.observe(containerRef.current);

4. 실시간 캔들 업데이트 로직

이 부분이 가장 까다로웠습니다. WebSocket으로 들어오는 체결(trade) 데이터를 받아서 두 가지 케이스를 처리해야 합니다.

  1. 현재 캔들 시간 범위 안이면 → 기존 캔들의 high/low/close 업데이트
  2. 새로운 시간 범위에 진입하면 → 새 캔들 생성
const callbacks: SocketCallbacks = {
  onTrade: (trade: UpbitTrade) => {
    if (trade.code !== market) return;

    const lastCandle = candlesRef.current[candlesRef.current.length - 1];

    // 체결 시각을 캔들 간격에 맞춰 계산
    const tradeTimestamp = Math.floor(trade.trade_timestamp / 1000);
    const candleTime = Math.floor(tradeTimestamp / intervalSeconds) * intervalSeconds;

    if (candleTime === lastCandle.time) {
      // 기존 캔들 업데이트
      lastCandle.high = Math.max(lastCandle.high, trade.trade_price);
      lastCandle.low = Math.min(lastCandle.low, trade.trade_price);
      lastCandle.close = trade.trade_price;

      // 차트에 직접 업데이트 전달 (리렌더링 없음)
      onCandleUpdateRef.current?.({ ...lastCandle }, { ...lastVolume });
    } else if (candleTime > lastCandle.time) {
      // 새 캔들 생성
      const newCandle: CandleData = {
        time: candleTime,
        open: trade.trade_price,
        high: trade.trade_price,
        low: trade.trade_price,
        close: trade.trade_price,
      };
      candlesRef.current.push(newCandle);
      onCandleUpdateRef.current?.(newCandle, newVolume);
    }
  },
};

여기서 중요한 설계 포인트가 세 가지 있습니다.

캔들 데이터는 useRef에 저장합니다. useState에 넣으면 매 체결마다 리렌더링이 일어나서 차트가 버벅입니다.

차트 업데이트는 series.update() 한 번으로 끝냅니다. setData()로 전체를 다시 넣지 않습니다.

콜백 ref 패턴으로 훅과 차트 컴포넌트를 연결합니다. 차트 컴포넌트가 마운트되면 콜백을 등록하고, 언마운트되면 null로 초기화합니다.

5. 타임프레임 전환

TanStack Query의 queryKey에 타임프레임을 포함하면 자연스럽게 처리됩니다.

const queryKey = ['candles', market, timeframe];

const { data, isLoading } = useQuery<CandleChartData>({
  queryKey,
  queryFn: () => fetchCandles(market, timeframe),
  staleTime: 1000 * 60, // 1분간 fresh
});

1분봉 → 5분봉으로 전환하면 queryKey가 바뀌고, 캐시에 없으면 자동으로 새 데이터를 fetch합니다. 이미 캐시에 있으면 즉시 보여줍니다. TanStack Query가 이런 부분을 정말 잘 해줍니다.

6. useEffect cleanup 주의점

차트 라이브러리를 React에서 쓸 때 가장 흔한 실수가 cleanup을 빼먹는 것입니다.

useEffect(() => {
  const chart = createChart(containerRef.current, { ... });
  // ...시리즈 추가

  return () => {
    resizeObserver.disconnect();  // 옵저버 해제
    chart.remove();               // 차트 인스턴스 제거 (DOM 정리)
    chartRef.current = null;      // ref 초기화
  };
}, []);

chart.remove()를 빼먹으면 Hot Reload 때마다 차트가 겹쳐서 생깁니다. ResizeObserver도 해제하지 않으면 메모리 릭이 발생합니다. 실제로 개발 중에 이 문제를 겪었습니다.

정리

이번 글에서 구현한 것은 다음과 같습니다.

  • Next.js Route Handler로 업비트 REST API CORS 프록시
  • TradingView Lightweight Charts v5 래퍼 컴포넌트 (다크 테마, 반응형)
  • TanStack Query로 초기 캔들 데이터 로딩 + 캐싱
  • WebSocket trade → 실시간 캔들 업데이트 (기존 캔들 수정 + 새 캔들 생성)
  • ref 기반 업데이트로 불필요한 리렌더링 방지

다음 글에서는 프론트엔드에서 가장 까다로운 컴포넌트라고 할 수 있는 호가창을 다룹니다. useState, throttle, useRef+rAF 세 가지 방식의 렌더링 성능을 직접 비교해봤습니다.

728x90
반응형
저작자표시 비영리 변경금지 (새창열림)

'Frontend Development > [실습] 실시간 트레이딩 대시보드 만들기' 카테고리의 다른 글

[실시간 트레이딩 대시보드 만들기 - 04] 회고 — 연습용 프로젝트와 실전의 거리  (0) 2026.02.12
[실시간 트레이딩 대시보드 만들기 - 03] 초당 20회 업데이트되는 호가창, 렌더링 횟수를 6,900배 줄인 방법  (0) 2026.02.12
[실시간 트레이딩 대시보드 만들기 - 01] WebSocket 파이프라인 설계와 Blob 파싱  (0) 2026.02.12
'Frontend Development/[실습] 실시간 트레이딩 대시보드 만들기' 카테고리의 다른 글
  • [실시간 트레이딩 대시보드 만들기 - 04] 회고 — 연습용 프로젝트와 실전의 거리
  • [실시간 트레이딩 대시보드 만들기 - 03] 초당 20회 업데이트되는 호가창, 렌더링 횟수를 6,900배 줄인 방법
  • [실시간 트레이딩 대시보드 만들기 - 01] WebSocket 파이프라인 설계와 Blob 파싱
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (151) N
      • Frontend Development (68) N
        • [실습] 실시간 트레이딩 대시보드 만들기 (4) N
      • Backend Development (28)
      • AI · ML (4)
        • Computer Vision (4)
      • Algorithm (35)
        • 백준 (11)
        • 프로그래머스 (18)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (4)
      • Language (6)
        • JavaScript (6)
      • 자격증 공부 (1)
        • GCP Developer (1)
      • Tools (1) N
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    claudecode
    Human-in-the-Loop
    모델비교
    Infra
    객체탐지
    YOLO
    Qwen2.5-VL
    Nextjs
    SRC
    API Gateway
    src layout
    개발자도구
    flat layout
    transformer
    CNN
    frontend development
    Object Detection
    컴퓨터비전
    tailwindcss
    backend
    딥러닝
    MSA
    Nginx
    mlops
    AI개발
    AI
    AgentTeams
    rt-detr
    Vision-Language-Model
    multiagent
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
[실시간 트레이딩 대시보드 만들기 - 02] TradingView Charts로 실시간 캔들 차트 구현
상단으로

티스토리툴바