API 연동

바이낸스 WebSocket 시세 구독 방법: 단일/조합 스트림 실무 코드

바이낸스 WebSocket 실시간 시세의 완벽한 솔루션: 단일/조합 스트림 URL, 구독 동적 관리, 연결 끊김 재연결, 하트비트 유지, 스트림 유형, 호가창(Depth) 증량 통합 알고리즘, Python asyncio 및 Node.js 실전 코드 포함.

바이낸스 WebSocket 실시간 시세의 최적 솔루션은 wss://stream.binance.com:9443/stream?streams= 조합 스트림 엔드포인트를 사용하는 것입니다. 하나의 연결로 최대 1024개의 스트림을 구독할 수 있으며, 메시지 지연 시간은 50ms 미만이고 가중치(Weight) 소모는 거의 제로에 가깝습니다. 이 글에서는 단일 스트림, 조합 스트림, 동적 구독, 로컬 호가창 매칭, 연결 끊김 재연결을 포함한 전체 Python 및 Node.js 코드를 제공하며, 모두 즉시 실행 가능합니다. 시작하기 전에 아직 바이낸스 계정이 없다면 바이낸스 공식 사이트에서 공식 접속 경로를 확인하거나, 무료 가입을 통해 계정을 만드실 수 있습니다.

1. WebSocket 엔드포인트 개요

카테고리 URL 용도
현물(Spot) 단일 스트림 wss://stream.binance.com:9443/ws/{stream} 하나의 스트림만 구독
현물(Spot) 조합 스트림 wss://stream.binance.com:9443/stream?streams={s1/s2/...} 여러 스트림 구독, 메시지에 스트림 이름 포함
현물(Spot) 예비 wss://data-stream.binance.vision/ws/{stream} 공식 백업 라인
선물(Futures) USDT 기반 wss://fstream.binance.com/stream?streams= 무기한 선물
선물(Futures) 코인 기반 wss://dstream.binance.com/stream?streams= 코인 기반 선물
테스트넷(Testnet) wss://stream.testnet.binance.vision/ws 테스트 네트워크

연결 수 제한: 단일 IP당 최대 300개의 WebSocket 연결, 연결당 최대 1024개의 스트림 구독이 가능하며, 24시간마다 한 번씩 자동으로 연결이 끊어지므로 클라이언트에서 재연결을 구현해야 합니다.

2. 스트림(Stream) 유형 목록

바이낸스는 11가지 주요 시세 스트림을 제공합니다:

스트림 형식 전송 빈도 설명
aggTrade {symbol}@aggTrade 실시간 통합 체결
trade {symbol}@trade 실시간 개별 체결
kline_1m {symbol}@kline_1m 매초 K라인(1m/5m/1h/...)
miniTicker {symbol}@miniTicker 1000ms 요약 시세
ticker {symbol}@ticker 1000ms 24h 전체 통계
bookTicker {symbol}@bookTicker 실시간 최우선 매수/매도 호가
depth {symbol}@depth 1000ms 전체 호가창(20/50/100단계)
depth@100ms {symbol}@depth@100ms 100ms 호가창 차분(증량)
!miniTicker@arr !miniTicker@arr 1000ms 전 시장 요약 시세
!ticker@arr !ticker@arr 1000ms 전 시장 전체 통계
!bookTicker !bookTicker 실시간 전 시장 최우선 매수/매도 호가

명명 규칙: 소문자 + @ 기호로 구분합니다. 예: BTCUSDT@ticker가 아닌 btcusdt@ticker.

3. Python asyncio 단일 스트림 구독

import asyncio, json
import websockets

async def subscribe_ticker(symbol: str):
    url = f"wss://stream.binance.com:9443/ws/{symbol.lower()}@ticker"
    async with websockets.connect(url, ping_interval=180) as ws:
        async for message in ws:
            data = json.loads(message)
            print(f"{data['s']}: 최신가 {data['c']}, "
                  f"24h 변동 {data['P']}%, "
                  f"거래량 {data['v']}")

asyncio.run(subscribe_ticker("BTCUSDT"))

출력 예시:

BTCUSDT: 최신가 68420.12, 24h 변동 2.35%, 거래량 45231.2
BTCUSDT: 최신가 68421.00, 24h 변동 2.36%, 거래량 45231.8

4. 여러 거래 페어 조합 스트림 구독

하나의 연결로 10개 거래 페어의 ticker + 5개 거래 페어의 K라인을 구독하는 예제입니다:

import asyncio, json
import websockets

STREAMS = [
    "btcusdt@ticker", "ethusdt@ticker", "bnbusdt@ticker",
    "solusdt@ticker", "xrpusdt@ticker", "adausdt@ticker",
    "dogeusdt@ticker", "maticusdt@ticker", "dotusdt@ticker",
    "avaxusdt@ticker",
    "btcusdt@kline_1m", "ethusdt@kline_1m",
    "bnbusdt@kline_1m", "solusdt@kline_1m", "xrpusdt@kline_1m"
]

async def combined_stream():
    streams = "/".join(STREAMS)
    url = f"wss://stream.binance.com:9443/stream?streams={streams}"
    async with websockets.connect(url, ping_interval=180) as ws:
        async for message in ws:
            payload = json.loads(message)
            stream = payload["stream"]
            data = payload["data"]
            if "@ticker" in stream:
                print(f"[{stream}] {data['s']}: {data['c']}")
            elif "@kline" in stream:
                k = data["k"]
                if k["x"]:  # K라인이 마감될 때만 출력
                    print(f"[{stream}] {k['s']} {k['i']} "
                          f"O:{k['o']} H:{k['h']} L:{k['l']} C:{k['c']}")

asyncio.run(combined_stream())

5. 동적 구독/취소 (SUBSCRIBE 메서드)

연결이 수립된 후 JSON 메시지를 통해 구독을 동적으로 추가하거나 제거할 수 있습니다:

import asyncio, json
import websockets

async def dynamic_sub():
    url = "wss://stream.binance.com:9443/ws"
    async with websockets.connect(url) as ws:
        # BTC 및 ETH의 bookTicker 구독
        await ws.send(json.dumps({
            "method": "SUBSCRIBE",
            "params": ["btcusdt@bookTicker", "ethusdt@bookTicker"],
            "id": 1
        }))

        count = 0
        async for msg in ws:
            data = json.loads(msg)
            if "result" in data:
                print(f"구독 확인: {data}")
                continue
            count += 1
            print(f"{data['s']} 매수1 {data['b']} 매도1 {data['a']}")

            # 50개 메시지를 받은 후 ETH 구독 취소, BTC만 유지
            if count == 50:
                await ws.send(json.dumps({
                    "method": "UNSUBSCRIBE",
                    "params": ["ethusdt@bookTicker"],
                    "id": 2
                }))
                print("ETH 구독 취소됨")

asyncio.run(dynamic_sub())

6. 호가창 증량 통합 (로컬 오더북 유지 관리)

고주파 전략에서는 로컬 매칭 엔진 수준의 오더북이 필요한 경우가 많습니다. 프로세스는 다음과 같습니다:

  1. @depth@100ms 차분 스트림을 구독하고 버퍼에 넣습니다.
  2. REST API로 GET /api/v3/depth?symbol=X&limit=1000 스냅샷을 가져옵니다.
  3. 버퍼에서 u < lastUpdateId인 메시지를 폐기합니다.
  4. U <= lastUpdateId+1 <= u인 메시지를 순차적으로 적용합니다.
  5. 이후 메시지는 직접 적용하며, pu는 이전 메시지의 u와 같아야 합니다.
import asyncio, json, aiohttp
import websockets
from sortedcontainers import SortedDict

class OrderBook:
    def __init__(self, symbol):
        self.symbol = symbol.lower()
        self.bids = SortedDict()  # 가격 -> 수량
        self.asks = SortedDict()
        self.last_update_id = 0
        self.buffer = []

    def apply_update(self, bids, asks):
        for price, qty in bids:
            price, qty = float(price), float(qty)
            if qty == 0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = qty
        for price, qty in asks:
            price, qty = float(price), float(qty)
            if qty == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = qty

    async def load_snapshot(self):
        async with aiohttp.ClientSession() as s:
            url = f"https://api.binance.com/api/v3/depth?symbol={self.symbol.upper()}&limit=1000"
            async with s.get(url) as r:
                snap = await r.json()
                self.last_update_id = snap["lastUpdateId"]
                self.apply_update(snap["bids"], snap["asks"])

    async def run(self):
        url = f"wss://stream.binance.com:9443/ws/{self.symbol}@depth@100ms"
        async with websockets.connect(url) as ws:
            # 1단계: 먼저 일정 시간 동안 차분 데이터 수집
            asyncio.create_task(self._collect(ws))
            await asyncio.sleep(1)
            # 2단계: 스냅샷 로드
            await self.load_snapshot()
            # 3단계: 버퍼에서 유효한 차분 데이터 적용
            for diff in self.buffer:
                if diff["u"] < self.last_update_id:
                    continue
                self.apply_update(diff["b"], diff["a"])
                self.last_update_id = diff["u"]
            self.buffer = None
            # 4단계: 이후 데이터 즉시 적용
            async for msg in ws:
                diff = json.loads(msg)
                self.apply_update(diff["b"], diff["a"])
                self.last_update_id = diff["u"]
                best_bid = self.bids.keys()[-1] if self.bids else 0
                best_ask = self.asks.keys()[0] if self.asks else 0
                print(f"매수1 {best_bid} 매도1 {best_ask} 차이 {best_ask-best_bid:.2f}")

    async def _collect(self, ws):
        if self.buffer is None:
            return
        async for msg in ws:
            if self.buffer is None:
                break
            self.buffer.append(json.loads(msg))

ob = OrderBook("BTCUSDT")
asyncio.run(ob.run())

7. 연결 끊김 재연결 및 하트비트

바이낸스 WebSocket은 클라이언트가 3분 이내에 pong 응답을 해야 하며, 연결은 24시간 후 강제로 끊어집니다:

import asyncio, json, websockets, logging

async def resilient_ws(url, on_message, max_retries=1000):
    retry = 0
    while retry < max_retries:
        try:
            async with websockets.connect(
                url,
                ping_interval=180,  # 3분마다 ping 전송
                ping_timeout=10,
                close_timeout=5
            ) as ws:
                retry = 0  # 연결 성공 시 초기화
                async for message in ws:
                    await on_message(json.loads(message))
        except (websockets.ConnectionClosed, asyncio.TimeoutError) as e:
            retry += 1
            wait = min(2 ** retry, 60)  # 지수 백오프, 최대 60초
            logging.warning(f"WS 연결 끊김 {e}, {wait}초 후 재연결 시도 ({retry}회차)")
            await asyncio.sleep(wait)

8. Node.js WebSocket 예제

const WebSocket = require('ws');

const streams = ['btcusdt@bookTicker', 'ethusdt@bookTicker'].join('/');
const url = `wss://stream.binance.com:9443/stream?streams=${streams}`;

function connect() {
  const ws = new WebSocket(url);
  ws.on('open', () => console.log('연결 성공'));
  ws.on('message', (raw) => {
    const { stream, data } = JSON.parse(raw);
    console.log(`${data.s} 매수1 ${data.b} 매도1 ${data.a}`);
  });
  ws.on('close', () => {
    console.log('연결 끊김, 3초 후 재연결');
    setTimeout(connect, 3000);
  });
  ws.on('error', (err) => console.error('오류 발생', err.message));
}

connect();

9. 자주 묻는 질문 FAQ

Q1: WebSocket도 가중치(Weight)를 소모하나요?

A: 연결 수립 자체에 가중치 2가 소모되지만, 이후 푸시되는 메시지는 가중치를 전혀 소모하지 않습니다. 단, IP당 연결 수가 300개를 초과하면 거부될 수 있습니다.

Q2: 구독 후 메시지가 전혀 오지 않는 이유는 무엇인가요?

A: 세 가지 흔한 원인이 있습니다: 1) 스트림 이름은 반드시 소문자여야 합니다 (BTCUSDT가 아닌 btcusdt). 2) 거래 페어를 잘못 작성했습니다 (예: BTCUSDT가 아닌 BTC-USDT 사용). 3) 구독 메시지는 /stream 엔드포인트가 아닌 wss://.../ws 엔드포인트에서 전송해야 합니다.

Q3: @depth와 @depth@100ms의 차이점은 무엇인가요?

A: @depth는 1000ms마다 한 번씩 전체 스냅샷(20단계)을 보내고, @depth@100ms는 100ms마다 한 번씩 차분(증량) 데이터를 보냅니다. 로컬 오더북 유지가 목적이라면 @100ms를, 단순 화면 표시용이라면 @depth를 사용하면 됩니다.

Q4: WebSocket으로 주문을 낼 수 있나요?

A: 가능합니다. 바이낸스는 서명 주문을 위한 WebSocket API(wss://ws-api.binance.com:443/ws-api/v3)를 제공하며, 이는 REST API보다 지연 시간이 30-50ms 낮습니다. 하지만 생태계가 아직 성숙하지 않아 대부분의 전략은 여전히 REST 주문 + WS 리스닝 방식을 사용합니다.

Q5: 24시간 자동 연결 끊김을 어떻게 매끄럽게 처리하나요?

A: 듀얼 연결 롤링 방식을 권장합니다. 연결 후 23시간이 지났을 때 새 연결 B를 수립하고, B가 메시지를 안정적으로 받기 시작하면 기존 연결 A를 닫아 데이터 누락을 방지합니다. python-binance와 같은 실무급 프레임워크에는 이 로직이 내장되어 있습니다.

WebSocket 솔루션을 살펴보셨다면, 카테고리 내비게이션으로 돌아가 「API 연동」 카테고리의 다른 SDK教程을 확인해 보세요.

계속 둘러보기

바이낸스 사용에 대한 추가 질문이 있으신가요? 카테고리 페이지로 돌아가 같은 주제의 다른 가이드를 찾아보세요.

카테고리

관련 가이드

바이낸스 API 신청 방법? 키 및 서명 생성 가이드 2026-04-14 바이낸스 현물(Spot) API 사용법: 첫 주문까지 가능한 실행 코드 가이드 2026-04-14 바이낸스 선물(Futures)과 현물(Spot) API의 차이점은? 엔드포인트와 가중치 비교 2026-04-14 바이낸스 API 사용 시 IP 차단될까? 제한 정책 및 가중치 계산법 총정리 2026-04-14