본문 바로가기
✨ python/FastAPI

[FastAPI, Recat] 이전 데이터 + 실시간 데이터(WebSocket) 합하기 (2)

by 환풍 2025. 12. 12.
728x90
반응형

https://bright-landscape.tistory.com/464

 

[FastAPI, Recat] 이전 데이터 + 실시간 데이터(WebSocket) 합하기 (1)

결과화면 ticker_ws.pyfrom fastapi import APIRouter, WebSocketimport websocketsimport jsonrouter = APIRouter()UPBIT_WS_URL = "wss://api.upbit.com/websocket/v1"@router.websocket("/chart")async def ticker(ws: WebSocket): await ws.accept() async with webso

bright-landscape.tistory.com

이전 코드에서 정적인 차트와 거기에 실시간 움직이는 차트를 합쳐보려고한다.

 

CandleChart.js

import React, { useState, useEffect, useRef } from "react";
import Chart from "react-apexcharts";
import axios from "axios";

function CandleChart() {
    /** -----------------------------
    *  1) 캔들 데이터 저장할 state
    * ----------------------------- */
    const [candles, setCandles] = useState([]);

    /** -----------------------------
     *  2) WebSocket 객체 보관용
     * ----------------------------- */
    const wsRef = useRef(null);

    /** -----------------------------
     *  3) 이전 캔들 데이터 로딩
     * ----------------------------- */
    const loadInitialCandles = async () => {
        try {
            const res = await axios.get("http://localhost:8000/upbit/candle");

            // 오래된 → 최신 순 정렬
            const sorted = res.data.sort(
                (a, b) =>
                    new Date(a.candle_date_time_kst) -
                    new Date(b.candle_date_time_kst)
            );

            // ApexCharts 포맷으로 변환
            const formatted = sorted.map((item) => ({
                timestamp: new Date(item.candle_date_time_kst).getTime(),
                open: item.opening_price,
                high: item.high_price,
                low: item.low_price,
                close: item.trade_price,
            }));

            setCandles(formatted);
        } catch (err) {
            console.log("초기 캔들 오류 :", err);
        }
    };

    /** -----------------------------
     *  4) WebSocket 연결 + 실시간 캔들 반영
     * ----------------------------- */
    const connectWs = () => {
        const ws = new WebSocket("ws://127.0.0.1:8000/chart");
        ws.binaryType = "arraybuffer";
        wsRef.current = ws;

        ws.onopen = () => console.log("WS 연결됨");

        ws.onmessage = (event) => {
            try {
                // 업비트 → ArrayBuffer → UTF-8 문자열 → JSON
                const t = JSON.parse(event.data);
                const price = t.trade_price;
                const ts = t.trade_timestamp; // ms

                setCandles((prev) => {
                    if (prev.length === 0) return prev;

                    const last = { ...prev[prev.length - 1] };
                    const list = [...prev];

                    const lastMin = Math.floor(last.timestamp / 60000);
                    const nowMin = Math.floor(ts / 60000);
                    // ① 새 분 → 새 캔들 생성
                    if (nowMin !== lastMin) {
                        const newCandle = {
                            timestamp: ts,
                            open: price,
                            high: price,
                            low: price,
                            close: price,
                        };
                        return [...list, newCandle];
                    }
                    // ② 같은 분 → high/low/close 업데이트
                    last.high = Math.max(last.high, price);
                    last.low = Math.min(last.low, price);
                    last.close = price;

                    list[list.length - 1] = last;
                    return list;
                });
            } catch (err) {
                console.error("WS JSON 파싱 오류:", err);
            }
        };
        ws.onclose = () => {
            console.log("WS 끊김 → 2초 후 재연결");
            setTimeout(connectWs, 2000);
        };
    };

    /** -----------------------------
     *  5) 컴포넌트가 처음 실행될 때
     * ----------------------------- */
    useEffect(() => {
        loadInitialCandles(); // 과거 데이터
        connectWs(); // 실시간 데이터

        return () => {
            if (wsRef.current) wsRef.current.close();
        };
    }, []);

    /** -----------------------------
     *  6) ApexCharts에 넣을 데이터 변환
     * ----------------------------- */
    const seriesData = candles.map((item) => ({
        x: new Date(item.timestamp),
        y: [item.open, item.high, item.low, item.close],
    }));

    const chartOptions = {
        chart: { type: "candlestick", height: 450 },
        xaxis: { type: "datetime" },
        yaxis: { tooltip: { enabled: true } },
    };

    return (
        <div>
            <h2>실시간 업비트 캔들 차트</h2>

            <Chart
                options={chartOptions}
                series={[{ data: seriesData }]}
                type="candlestick"
                height={450}
            />
        </div>
    );
}
export default CandleChart;

 

1) 캔들 데이터 저장용 state

const [candles, setCandles] = useState([]);
  • candles는 차트에 표시할 캔들(봉) 배열을 보관하는 변수.
  • setCandles는 이 값을 바꿀 때 쓰는 함수.
  • useState([])로 초기값을 빈 배열로 설정 — 아직 데이터가 없을 때 대비.

주의: candles를 직접 수정하면 안 되고(immutability), 항상 setCandles로 새 배열을 할당해야 React가 변화를 인지한다.

 

2) WebSocket 객체 보관용 ref

const wsRef = useRef(null);
  • wsRef.current에 WebSocket 객체를 저장한다.
  • useRef는 렌더가 다시 일어나도 같은 객체를 유지하므로 WebSocket처럼 “컴포넌트 전체에서 하나만 쓰는 값”을 보관하기에 적합.

3) 과거 캔들 데이터 로딩 함수

const loadInitialCandles = async () => {
    try {
        const res = await axios.get("http://localhost:8000/upbit/candle");

        const sorted = res.data.sort(
            (a, b) =>
                new Date(a.candle_date_time_kst) -
                new Date(b.candle_date_time_kst)
        );

        const formatted = sorted.map((item) => ({
            timestamp: new Date(item.candle_date_time_kst).getTime(),
            open: item.opening_price,
            high: item.high_price,
            low: item.low_price,
            close: item.trade_price,
        }));

        setCandles(formatted);
    } catch (err) {
        console.log("초기 캔들 오류 :", err);
    }
};

역할 한 줄

  • 서버에서 과거 캔들(역사 데이터) 를 가져와서, 차트가 이해하는 형태로 변환해 candles에 저장한다.

세부 설명

  • await axios.get(...) : 비동기로 서버에 GET 요청. await 붙여서 응답이 올 때까지 기다림.
  • res.data : 서버가 준 실제 데이터(보통 배열).
  • sort(...) : 오래된 순 → 최신 순으로 정렬. 차트는 시간순이어야 보기 편함.
  • formatted : ApexCharts가 기대하는 형태로 각 항목을 { timestamp, open, high, low, close } 로 바꿈.
    • timestamp는 밀리초 숫자(Date.getTime())로 바꿔서 X축에 넣기 좋게 함.
  • setCandles(formatted) : 변환된 데이터를 상태로 저장 -> 화면(차트) 자동 갱신.

주의 / 디버깅

  • 서버 응답 구조가 다르면 res.data.sort에서 에러 남. (ex. res.data가 객체거나 null일 때)
  • 네트워크 에러면 catch 블록에 잡히니 콘솔 확인.

4) WebSocket 연결 및 실시간 캔들 반영

const connectWs = () => {
    const ws = new WebSocket("ws://127.0.0.1:8000/chart");
    ws.binaryType = "arraybuffer";
    wsRef.current = ws;

    ws.onopen = () => console.log("WS 연결됨");

    ws.onmessage = (event) => {
        try {
            const t = JSON.parse(event.data);

            const price = t.trade_price;
            const ts = t.trade_timestamp; // ms

            setCandles((prev) => {
                if (prev.length === 0) return prev;

                const last = { ...prev[prev.length - 1] };
                const list = [...prev];

                const lastMin = Math.floor(last.timestamp / 60000);
                const nowMin = Math.floor(ts / 60000);

                if (nowMin !== lastMin) {
                    const newCandle = {
                        timestamp: ts,
                        open: price,
                        high: price,
                        low: price,
                        close: price,
                    };
                    return [...list, newCandle];
                }

                last.high = Math.max(last.high, price);
                last.low = Math.min(last.low, price);
                last.close = price;

                list[list.length - 1] = last;
                return list;
            });
        } catch (err) {
            console.error("WS JSON 파싱 오류:", err);
        }
    };

    ws.onclose = () => {
        console.log("WS 끊김 → 2초 후 재연결");
        setTimeout(connectWs, 2000);
    };
};

역할 한 줄

  • WebSocket을 열고, 서버에서 오는 실시간 ticker를 받아서 캔들(마지막 봉 또는 새로운 봉)을 업데이트한다.

단계별 설명

  1. const ws = new WebSocket("ws://127.0.0.1:8000/chart")
    • 브라우저가 FastAPI의 /chart WebSocket 엔드포인트에 연결 시도.
  2. ws.binaryType = "arraybuffer"
    • 서버가 바이너리로 보낼 가능성을 염두에 둔 설정. (너의 서버가 문자열로 보낸다면 이 줄 없어도 됨 — 하지만 있으면 ArrayBuffer를 처리 가능)
  3. wsRef.current = ws
    • 연결한 WebSocket 객체를 wsRef에 저장(나중에 닫거나 상태 확인할 때 사용).
  4. ws.onopen
    • 연결이 성공했을 때 한 번 실행. 디버깅용 로그.
  5. ws.onmessage
    • 서버가 메시지를 보낼 때마다 이 함수가 호출된다.
    • event.data는 서버가 보낸 문자열(JSON)이므로 JSON.parse(event.data)로 객체로 만듦.
    • t.trade_price, t.trade_timestamp 같은 업비트 필드를 읽음.
    • setCandles(prev => { ... }) : 상태 갱신을 안전하게 이전 상태 기반으로 처리(동시성 방지).
      • 로직 핵심: 마지막 캔들(배열의 마지막 요소)의 분(minute)과 현재 메시지의 분을 비교해서,
        • 같은 분이면 high/low/close만 업데이트,
        • 다른(새로운) 분이면 새로운 캔들 객체를 추가.
  6. ws.onclose
    • 연결이 끊겼을 때(서버 끊김 또는 네트워크 문제) 로그 찍고 2초 후 재연결 시도(간단한 재시도 로직).

왜 이렇게 하냐?

  • 업비트에서 매체결(tick) 단위로 데이터가 들어옴. 우리는 1분봉을 만들기 위해 “같은 분”인지 비교해서 하나의 봉으로 합쳐야 함.
  • setCandles 내부에서 prev를 사용하면 React 상태 업데이트가 안전하게 처리된다.

주의 / 디버깅

  • prev.length === 0 조건: 초기 데이터가 아직 로드되지 않았을 때 WebSocket이 먼저 왔을 경우 대비. (초기 candles가 필요하면 WebSocket 연결을 초기데이터 로딩 후에 시도하는 편이 더 안전)
  • JSON.parse 에러가 발생하면 onmessage에서 catch로 잡히니 콘솔 확인.
  • ws.binaryType과 event.data 타입(문자열 vs ArrayBuffer)은 서버(백엔드) 전송 방식과 맞춰야 한다. 서버가 send_text로 문자열 전송하면 JSON.parse(event.data)로 충분.

5) 컴포넌트 최초 실행(마운트) 시점: useEffect

useEffect(() => {
    loadInitialCandles(); // 과거 데이터
    connectWs(); // 실시간 데이터

    return () => {
        if (wsRef.current) wsRef.current.close();
    };
}, []);
  • useEffect(..., [])는 컴포넌트가 화면에 처음 나타날 때 한 번만 실행된다.
  • 여기에서 과거 데이터 로딩WebSocket 연결을 동시에 시작함.
  • return 부분은 컴포넌트(화면)가 사라질 때 실행되어 WebSocket을 닫아 자원(연결)을 정리한다.

주의: React 개발 모드의 StrictMode는 일부 훅을 두 번 실행할 수 있으니(특히 개발환경) 재연결 로그가 두 번 찍힐 수 있다. 배포 빌드에서는 정상.

6) 차트에 집어넣을 데이터로 변환

const seriesData = candles.map((item) => ({
    x: new Date(item.timestamp),
    y: [item.open, item.high, item.low, item.close],
}));

ApexCharts 캔들 시리즈 형식으로 변환.

  • x = 날짜 객체(시간)
  • y = [open, high, low, close] (캔들 형식)

※ 마지막, 실시간 캔들을 추가하고, 기존에 있던 캔들은 삭제 하는 코드 로직 추가

기존 return 값을 제거하고, MAX_CANDLES 를 추가하여, 업데이트 되도록 로직을 개선하였다.

 

728x90
반응형

댓글