본문 바로가기
✨ python/FastAPI

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

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

 

결과화면

 

 


ticker_ws.py

from fastapi import APIRouter, WebSocket

import websockets
import json

router = APIRouter()

UPBIT_WS_URL = "wss://api.upbit.com/websocket/v1"


@router.websocket("/chart")
async def ticker(ws: WebSocket):
    await ws.accept()

    async with websockets.connect(UPBIT_WS_URL) as upbit_ws:

        subscribe = [{"ticket": "proxy"}, {"type": "ticker", "codes": ["KRW-BTC"]}]
        await upbit_ws.send(json.dumps(subscribe))

        while True:
            raw = await upbit_ws.recv()  # bytes
            text = raw.decode("utf-8")  # JSON 문자열로 변환
            await ws.send_text(text)  # 문자열로 전송

1️⃣ WebSocket이 뭔데?

일반 HTTP는 “요청 → 응답” 1번만 하고 끝나는 연결이다.

하지만 WebSocket은 서버 ↔ 클라이언트가 계속 연결을 유지해서
서버가 ‘자유롭게’ 데이터를 실시간으로 보낼 수 있게 만드는 통신 방식
이다. 즉,

  • HTTP: "나 이거 줘" → 서버: "여기" → 끝
  • WebSocket: 서로 초대 → 연결 유지 → 필요할 때마다 계속 "야 이거 새로 들어왔어"라고 보낼 수 있음

2️⃣ async / await는 왜 필요한가?

🌟 비유로 설명

일반 함수는 한 번에 하나의 일만 처리한다. 내부에서 느린 작업이 있으면 그 시간 동안 아무것도 못 하고 멈춘다.

→ 하지만 실시간 WebSocket은 “기다리는 시간”이 매우 많다. 업비트에서 데이터가 들어올 때까지 컴퓨터는 놀고 있음.

그래서 Python은 “비동기(async)” 기능을 제공한다.

async = 이 함수는 비동기 작업을 할 수 있는 함수다.
await = 여기서 잠깐 쉬어도 된다. 다른 작업은 계속 해라. 

즉 async/await의 핵심 목적:

✔️ 서버가 멈추지 않게 하기
✔️ WebSocket처럼 “데이터 올 때까지 기다려야 하는 작업”을 효율적으로 처리


3️⃣ async def ticker(ws: WebSocket) 설명

@router.websocket("/chart")
async def ticker(ws: WebSocket):
 
  • FastAPI에게 "이 URL은 WebSocket용"이라고 알려주는 코드
  • ws는 브라우저와 연결되는 WebSocket 객체

즉,

🔹 React → ws://localhost:8000/chart 로 접속
🔹 FastAPI는 이 함수를 실행해서 브라우저와 WebSocket 연결을 맺음

 

4️⃣ await ws.accept()는 뭐야?

브라우저가 WebSocket 연결을 하려고 하면 서버에 인사함:

“나 WebSocket 연결해도 돼?”

ws.accept()는:

✔️ 서버가 그 요청을 받아들이고
✔️ WebSocket 통신을 시작하는 단계

await은:

✔️ "이 작업 끝날 때까지 기다리되, 다른 작업은 해도 됨"

 

5️⃣ Upbit WebSocket 연결(서버 → 업비트)

asyncwith websockets.connect(UPBIT_WS_URL) as upbit_ws:

이 줄은:

FastAPI 서버가 업비트의 WebSocket 서버에 접속하는 코드다.

브라우저가 FastAPI에게 “실시간 데이터줘”
→ FastAPI는 Upbit에게 “데이터 주세요” 하고 연결함

브라우저 ←→ FastAPI ←→ 업비트(실시간)

 

6️⃣ 구독 요청(subscription) 보내기

subscribe = [ {"ticket": "proxy"}, {"type": "ticker", "codes": ["KRW-BTC"]} ]
await upbit_ws.send(json.dumps(subscribe))

Upbit WebSocket은 연결만 된다고 해서 데이터를 막 보내지 않음.

그래서 “KRW-BTC의 ticker 데이터를 보내줘”라고 서버에 요청해야 한다.

json.dumps(subscribe)는:

✔️ 파이썬 리스트/딕셔너리를 JSON 문자열로 변환하는 작업

 

7️⃣ while True는 왜 필요한가?

 
while True:

이건 “무한 반복”이다.

왜 무한 반복?

업비트 실시간 데이터는:

🔸 1번 요청 → 1번 받는 게 아니라
🔸 연결이 유지되고 계속 들어온다

즉 패턴은:

  • 업비트: "새로운 데이터!"
  • 업비트: "새로운 거래가 발생!"
  • 업비트: "또 발생!"
  • … 이렇게 수십, 수백, 수천 번 무한히 보내줌

그래서 우리는 계속 recv() 하고 → 브라우저로 send() 해야 함.

8️⃣ raw = await upbit_ws.recv() ← 이게 핵심

🔥 recv()란?

WebSocket 서버가 보낸 메시지를 받는 함수
→ 업비트가 실시간으로 보내는 데이터를 받아오는 역할

반환 타입

  • Upbit는 “바이너리(bytes)”로 데이터를 보낸다.
  • 즉 파이썬이 보기엔 그냥 이진 데이터 뭉텅이.

9️⃣ .decode("utf-8")는 왜 필요한가?

 
text = raw.decode("utf-8")

이진(bytes) 데이터를 사람이 읽을 수 있는 문자열로 바꿈.

비유하면:

  • bytes = 암호처럼 생긴 디지털 0/1 조합
  • decode = 그것을 인간이 읽을 수 있는 글자로 변환

🔟 await ws.send_text(text)는 뭐야?

이건 FastAPI가 React 브라우저에게 실시간적으로 보내는 부분.

즉 전체 흐름은:

업비트 → bytes → raw
→ decode → 문자열 text
→ React 브라우저로 send_text

 

CandleChart.js

import React, { useState, useEffect } from "react";
import Chart from "react-apexcharts";
import { getCandle } from "../services/upbitApi";

function CandleChart() {

    // 1) 이전 캔들 데이터 조회
    const [candle, setCandle] = useState([]);

    useEffect(() => {
        getCandle().then(res => setCandle(res.data));
    }, []);
    const seriesData = candle.map(item => ({
        x: new Date(item.candle_date_time_kst),
        y: [
            item.opening_price,
            item.high_price,
            item.low_price,
            item.trade_price,
        ]
    }));

    // 2) 실시간 웹소켓 받은 데이터
    const [liveData, setLiveData] = useState(null);
    //  WebSocket 실시간 ticker 수신 -> 캔들 업데이트
    useEffect(() => {
        const ws = new WebSocket("ws://127.0.0.1:8000/chart");
        ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            setLiveData(data);
        };
        ws.onopen = () => console.log("WS 연결됨");
        ws.onclose = () => console.log("WS 종료됨");
        return () => ws.close();
    }, []);

    const chartOptions = {
        chart: { type: "candlestick", height: 450 },
        xaxis: { type: "datetime" },
        yaxis: { tooltip: { enabled: true } },
    };
    return (
        <div>  {liveData ? (
            <div>
                <p>현재가: {liveData.trade_price}</p>
                <p>체결 시간: {new Date(liveData.trade_timestamp).toLocaleTimeString()}</p>
                <p>거래량: {liveData.trade_volume}</p>
            </div>
        ) : (
            <p>실시간 데이터 수신 중...</p>
        )}
            <Chart
                options={chartOptions}
                series={[{ data: seriesData }]}
                type="candlestick"
                height={450}
            />
        </div>
    );
}
export default CandleChart;

과거 차트는 HTTP로 가져오고, 실시간 가격은 WebSocket으로 받아서 화면에서 계속 보여주는 코드이다.

 

🧱 1. React 기본 구조부터 설명

React 컴포넌트는 하나의 화면 조각이라고 생각.

function CandleChart() {
     return (화면);
}

CandleChart는 “캔들 차트 화면”을 표시하는 컴포넌트.

 

🧱 2. useState — 변수 저장하는 곳

React에서는 일반 변수를 쓰지 않고 화면이 자동으로 다시 그려지도록 하는 특별한 변수 useState를 사용.

예)

const [candle, setCandle] = useState([]);
  • candle → “캔들 데이터”가 들어감
  • setCandle → candle에 값을 넣는 함수
  • useState([]) → 처음엔 빈 배열로 시작하겠다

🧱 3. useEffect — “페이지 켜질 때 한 번 실행”

React에서 useEffect는:

“화면이 처음 로딩될 때 실행해줘!”

라는 의미.

useEffect(() => {
       getCandle().then(res => setCandle(res.data));
}, []);

✔ 이 코드가 하는 일

  1. Upbit API를 HTTP로 불러옴 (getCandle())
  2. 그 결과를 candle에 저장 (setCandle(res.data))

 

🧱 4. 과거 차트 데이터 → apexcharts에 넣기 위한 가공

const seriesData = candle.map(item => ({
       x: newDate(item.candle_date_time_kst),
       y: [
          item.
opening_price, item.high_price, item.low_price, item.trade_price,
     ]
}));

🧠 하는 일

Upbit에서 받은 데이터 형태는 차트가 읽을 수 없어서,
차트 라이브러리(apexcharts)가 원하는 모양으로 바꿔줌.

  • x: 시간
  • y: [시가, 고가, 저가, 종가]

캔들 차트의 기본 구조 그대로.

 

🧱 5. 실시간 데이터 저장소 만들기

const [liveData, setLiveData] = useState(null);

여기에 업비트 WebSocket에서 받은 실시간 가격을 넣는다.

 

🧱 6. WebSocket 연결 — 실시간 가격 받기

여기가 이 파일의 핵심 👇

useEffect(() => {
    const ws = newWebSocket("ws://127.0.0.1:8000/chart");

📌 WebSocket은 뭐야?

HTTP는 "한 번 요청하면 한 번만 응답"
하지만,

WebSocket은 양방향 실시간 통신이 가능한 파이프

코인 가격은 계속 바뀌니 한 번만 받으면 안 되잖아?

그래서 “웹소켓”으로 계속 들어오는 데이터를 받는 것.

🧠 ws.onmessage

ws.onmessage = (event) => {
          const data = JSON.parse(event.data);
         setLiveData(data);
};
  • 코인 가격이 변경될 때마다 server → client로 메시지를 보냄
  • event.data = 서버에서 보낸 JSON 문자열
  • JSON.parse() 해서 실제 객체로 변환
  • setLiveData(data)로 상태에 저장 → 화면 자동 업데이트됨

🧠 ws.onopen / ws.onclose

ws.onopen = () => console.log("WS 연결됨");
ws.onclose = () => console.log("WS 종료됨");
  • 연결될 때 로그 찍기
  • 페이지 나갈 때 종료시키기

🧱 7. 차트 옵션 설정

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

그냥 apexcharts 설정.

  • 캔들 차트로 표시
  • X축은 시간
  • Y축은 숫자

 

다음엔, 실시간으로 받은 데이터 소켓으로, 이전 데이터 차트와 합하여, 실시간으로 차트가 움직이는걸 구현해보려고한다.

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

 

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

CandleChart.jsimport React, { useState, useEffect, useRef } from "react";import Chart from "react-apexcharts";import axios from "axios";function CandleChart() { /** ----------------------------- * 1) 캔들 데이터 저장할 state * ---------------------

bright-landscape.tistory.com

 

728x90
반응형

댓글