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를 받아서 캔들(마지막 봉 또는 새로운 봉)을 업데이트한다.
단계별 설명
- const ws = new WebSocket("ws://127.0.0.1:8000/chart")
- 브라우저가 FastAPI의 /chart WebSocket 엔드포인트에 연결 시도.
- ws.binaryType = "arraybuffer"
- 서버가 바이너리로 보낼 가능성을 염두에 둔 설정. (너의 서버가 문자열로 보낸다면 이 줄 없어도 됨 — 하지만 있으면 ArrayBuffer를 처리 가능)
- wsRef.current = ws
- 연결한 WebSocket 객체를 wsRef에 저장(나중에 닫거나 상태 확인할 때 사용).
- ws.onopen
- 연결이 성공했을 때 한 번 실행. 디버깅용 로그.
- ws.onmessage
- 서버가 메시지를 보낼 때마다 이 함수가 호출된다.
- event.data는 서버가 보낸 문자열(JSON)이므로 JSON.parse(event.data)로 객체로 만듦.
- t.trade_price, t.trade_timestamp 같은 업비트 필드를 읽음.
- setCandles(prev => { ... }) : 상태 갱신을 안전하게 이전 상태 기반으로 처리(동시성 방지).
- 로직 핵심: 마지막 캔들(배열의 마지막 요소)의 분(minute)과 현재 메시지의 분을 비교해서,
- 같은 분이면 high/low/close만 업데이트,
- 다른(새로운) 분이면 새로운 캔들 객체를 추가.
- 로직 핵심: 마지막 캔들(배열의 마지막 요소)의 분(minute)과 현재 메시지의 분을 비교해서,
- 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
반응형
'✨ python > FastAPI' 카테고리의 다른 글
| [FastAPI, React] Telegram Bot 연동하기 (0) | 2025.12.21 |
|---|---|
| [FastAPI, Recat] 이전 데이터 + 실시간 데이터(WebSocket) 합하기 (3) (0) | 2025.12.14 |
| [FastAPI, Recat] 이전 데이터 + 실시간 데이터(WebSocket) 합하기 (1) (0) | 2025.12.12 |
| [FastApi, React] 업비트 WebSocket으로 실시간 시세 불러오기 (0) | 2025.12.10 |
| FastAPI로 업비트 차트 React로 JSON 형식 데이터 가져오기 (0) | 2025.12.08 |
댓글