본문으로 건너뛰기
김신건의 로그

[FastAPI] WebSockets

· 수정 · 📖 약 2분 · 778자/단어 #python #fastapi #websocket #realtime
FastAPI WebSockets, FastAPI WebSocket, websockets fastapi, FastAPI 웹소켓, WebSocket broadcast, WebSocket authentication

정의

FastAPI WebSockets 는 Starlette 위에서 양방향 실시간 통신을 제공합니다. HTTP 대신 ws:// / wss:// 스킴을 쓰며, 한 연결 위에서 서버와 클라이언트가 자유롭게 메시지를 주고받습니다.

주요 용도: 채팅, 실시간 알림, 라이브 대시보드, 게임, 협업 편집.

기본 사용

from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_text(f"echo: {data}")
  • accept(): 핸드셰이크 완료, 연결 활성화
  • receive_text() / receive_bytes() / receive_json(): 메시지 수신 (blocking, coroutine)
  • send_text() / send_bytes() / send_json(): 송신

연결 종료 처리

from fastapi import WebSocketDisconnect

@app.websocket("/ws")
async def ws_ep(websocket: WebSocket):
    await websocket.accept()
    try:
        while True:
            data = await websocket.receive_text()
            await websocket.send_text(f"got: {data}")
    except WebSocketDisconnect:
        print("client disconnected")

클라이언트 종료 -> WebSocketDisconnect 예외. 반드시 catch 하고 자원 정리.

WebSocket 은 브라우저 API 상 커스텀 헤더를 못 붙임 (subprotocol 은 가능). 대개 query 나 cookie 로 토큰 전달.

Query 파라미터

from fastapi import Query, WebSocketException, status

@app.websocket("/ws")
async def ws_ep(
    websocket: WebSocket,
    token: str = Query(...),
):
    user = validate_token(token)
    if not user:
        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
    await websocket.accept()
    ...
@app.websocket("/ws")
async def ws_ep(
    websocket: WebSocket,
    session: str | None = Cookie(default=None),
    x_client_id: str | None = Header(default=None),
):
    ...

주의: WebSocket dependency 는 Depends() 도 지원하지만 HTTPException 대신 WebSocketException 을 사용해야 합니다.

Dependency Injection

async def get_current_user_ws(token: str = Query(...)) -> User:
    user = validate_token(token)
    if not user:
        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
    return user

@app.websocket("/ws")
async def ws_ep(
    websocket: WebSocket,
    user: Annotated[User, Depends(get_current_user_ws)],
    db: Annotated[AsyncSession, Depends(get_async_db)],
):
    await websocket.accept()
    ...

Broadcast 패턴

여러 클라이언트에 같은 메시지 전송.

단순 in-memory manager

class ConnectionManager:
    def __init__(self):
        self.active: list[WebSocket] = []

    async def connect(self, ws: WebSocket):
        await ws.accept()
        self.active.append(ws)

    def disconnect(self, ws: WebSocket):
        self.active.remove(ws)

    async def broadcast(self, message: str):
        for ws in self.active:
            try:
                await ws.send_text(message)
            except Exception:
                pass   # 연결 끊긴 클라이언트

manager = ConnectionManager()

@app.websocket("/chat/{room}")
async def chat(websocket: WebSocket, room: str):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(f"[{room}] {data}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(f"[{room}] user left")

제한: 단일 프로세스. 여러 워커면 프로세스 간 broadcast 안 됨.

다중 프로세스 broadcast (Redis pub/sub)

import redis.asyncio as redis
import asyncio, json

class BroadcastManager:
    def __init__(self, url: str):
        self.pub = redis.from_url(url)
        self.sub = redis.from_url(url)
        self.local_conns: set[WebSocket] = set()

    async def connect(self, ws: WebSocket):
        await ws.accept()
        self.local_conns.add(ws)

    def disconnect(self, ws: WebSocket):
        self.local_conns.discard(ws)

    async def publish(self, channel: str, message: dict):
        await self.pub.publish(channel, json.dumps(message))

    async def subscribe(self, channel: str):
        pubsub = self.sub.pubsub()
        await pubsub.subscribe(channel)
        async for msg in pubsub.listen():
            if msg["type"] != "message":
                continue
            data = json.loads(msg["data"])
            for ws in list(self.local_conns):
                try:
                    await ws.send_json(data)
                except Exception:
                    self.local_conns.discard(ws)

manager = BroadcastManager("redis://localhost")

@asynccontextmanager
async def lifespan(app: FastAPI):
    task = asyncio.create_task(manager.subscribe("chat"))
    yield
    task.cancel()

app = FastAPI(lifespan=lifespan)

또는 broadcaster 패키지 (encode.io) 로 Redis/Postgres/Memory backend 통합.

Subprotocol

클라이언트가 원하는 서브프로토콜을 협상 (예: MQTT over WebSocket, GraphQL Subscriptions).

@app.websocket("/ws")
async def ws_ep(websocket: WebSocket):
    proto = websocket.headers.get("sec-websocket-protocol", "")
    if "myproto.v1" in proto:
        await websocket.accept(subprotocol="myproto.v1")
    else:
        await websocket.close(code=1002)
        return

클라이언트 예시

const ws = new WebSocket("wss://api.example.com/ws?token=abc");
ws.onopen = () => console.log("open");
ws.onmessage = (e) => console.log("msg:", e.data);
ws.onclose = () => console.log("closed");
ws.send("hello");

Python 클라이언트 (테스트용):

import asyncio
import websockets

async def client():
    async with websockets.connect("ws://localhost:8000/ws?token=abc") as ws:
        await ws.send("hello")
        print(await ws.recv())

asyncio.run(client())

Heartbeat / Ping-Pong

Idle 연결을 감지하려면 주기적 ping.

@app.websocket("/ws")
async def ws_ep(websocket: WebSocket):
    await websocket.accept()
    async def sender():
        while True:
            await asyncio.sleep(30)
            try:
                await websocket.send_json({"type": "ping"})
            except Exception:
                return

    async def receiver():
        while True:
            data = await websocket.receive_json()
            if data.get("type") == "pong":
                continue
            # handle real message

    await asyncio.gather(sender(), receiver())

Nginx / ALB / CloudFront 앞단이 있으면 idle timeout 을 유의 (기본 60초).

테스트

TestClient.websocket_connect 는 context manager:

from fastapi.testclient import TestClient

def test_websocket():
    client = TestClient(app)
    with client.websocket_connect("/ws?token=valid") as ws:
        ws.send_text("hello")
        data = ws.receive_text()
        assert data == "got: hello"

배포 시 유의점

Uvicorn workers

uvicorn main:app --workers 4

WebSocket 은 프로세스 로컬 상태. 다중 워커 broadcast 필요 시 Redis pub/sub.

프록시 (nginx, ALB, CloudFront)

  • Upgrade / Connection 헤더 통과 필수 (proxy_set_header Upgrade $http_upgrade)
  • Idle timeout 확대 (예: nginx proxy_read_timeout 3600)
  • CloudFront: WebSocket 지원, 별도 설정 없음
  • ALB: WebSocket 지원, target group 의 stickiness 고려

Kubernetes

  • Service type: ClusterIP + Ingress (Nginx ingress 는 annotation 필요)
  • Session affinity: 필요 시 Ingress 에서 sticky session

SSE (Server-Sent Events) 와 비교

WebSocketSSE
방향양방향서버 -> 클라이언트만
프로토콜ws/wssHTTP text/event-stream
재연결수동자동 (retry)
바이너리지원텍스트만
프록시 호환성별도 설정 필요표준 HTTP
적합채팅, 게임, 협업로그 스트림, 알림

Client-only push 라면 SSE 가 더 단순하고 안정적.

함정

WARNING

WebSocket 은 인증을 처음에만. 세션 만료를 감지하려면 앱 레벨 재검증 (예: 5분마다 서버가 token refresh 요구).

CAUTION

In-memory manager 는 프로세스 한 개용. 다중 워커 + broadcast 는 반드시 Redis / Postgres pub/sub.

WARNING

연결 dangling. 클라이언트가 조용히 사라지면 서버가 오래 알아채지 못함. Heartbeat + idle timeout 필수.

IMPORTANT

응답 스키마 없음. WebSocket 은 OpenAPI 스키마화가 제한적. AsyncAPI 등 별도 문서화.

CAUTION

Backpressure. 클라이언트가 느리면 서버 send buffer 가 쌓임. send 가 대기 상태로. 큐 사이즈 제한 + drop policy 설계.

관련 위키

이 글의 용어 (6개)
[FastAPI] Async / Sync Endpointsfastapi
정의 FastAPI 는 ASGI 프레임워크 이므로 endpoint 를 로 정의해 이벤트 루프에서 실행하거나, 로 정의해 스레드풀에서 실행할 수 있습니다. 어느 쪽을 선택하는지가 …
[FastAPI] Dependency Injectionfastapi
정의 FastAPI Dependency Injection 은 로 함수를 endpoint 에 자동 주입하는 시스템입니다. DB 세션, 인증 사용자, 설정, 서비스 계층 등을 end…
[FastAPI] Deployment (Uvicorn, Gunicorn, Docker)fastapi
정의 FastAPI 배포는 ASGI 서버 (Uvicorn) + 프로세스 관리자 (Gunicorn 또는 Uvicorn 자체) + 컨테이너 (Docker) + 오케스트레이터 (Kub…
[FastAPI] Middlewarefastapi
정의 FastAPI Middleware 는 모든 요청과 응답을 가로채 로깅, 인증, 압축, CORS 등 공통 처리를 하는 계층입니다. Starlette 의 ASGI middlew…
[FastAPI] Testingfastapi
정의 FastAPI Testing 은 endpoint 를 실제 서버 없이 직접 호출해 응답을 검증하는 workflow 입니다. Starlette 의 (내부적으로 sync) 또는 …
[Python] FastAPIfastapi
정의 FastAPI 는 Python 3.8+ 을 위한 ASGI 기반 현대 웹/API 프레임워크 입니다. Sebastián Ramírez (tiangolo) 가 2018년 발표했고…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기