네트워크 인터넷 실시간 통신 구현방 프로토콜 상세 비교

네트워크 인터넷 실시간 통신 구현방 프로토콜 상세 비교

“우리가 매일 사용하는 채팅 앱, 실시간 주식 차트, 그리고 화상 회의는 도대체 어떤 원리로 즉각적인 반응을 보여주는 걸까요?”

웹 서비스를 개발하거나 운영하다 보면 한 번쯤 고민하게 되는 문제가 있습니다. 바로 ‘어떻게 하면 데이터를 가장 빠르고 효율적으로 주고받을 수 있을까?’입니다. 단순히 웹페이지를 보여주는 것을 넘어, 사용자의 동작에 즉각 반응하는 인터넷 실시간 통신 서비스를 구현하려면 상황에 맞는 올바른 네트워크 통신 방식(프로토콜)을 선택하는 것이 필수적입니다.

하지만 인터넷상에는 HTTP부터 WebSocket, SSE, gRPC, MQTT에 이르기까지 너무나 많은 통신 프로토콜이 존재합니다.

이 글에서는 현대의 다양한 네트워크 통신 기술 7가지(HTTP, WebSocket, SSE, WebRTC, gRPC, MQTT, Long Polling)를 꼼꼼하게 비교 정리하고, 그중에서도 실시간 서비스에 가장 널리 쓰이는 웹소켓(WebSocket)을 실제로 설정하는 방법연결을 항상 유지하는 방법까지 코드 레벨로 자세히 다룹니다.

목차

1. 인터넷 실시간 통신이란?

네트워크 통신은 우리가 매일 사용하는 웹사이트, 앱, IoT 기기들이 서로 대화를 나누는 방식입니다. 마치 친구와 편지를 주고받을지, 전화를 할지, 혹은 확성기로 방송을 할지 정하는 것과 비슷하죠.

같은 “대화”라도 목적에 따라 가장 효율적인 수단이 다릅니다. 편지(HTTP)로 충분한 상황이 있고, 실시간 통화(WebSocket)가 반드시 필요한 상황도 있습니다. 특히 최근에는 실시간 채팅, 온라인 게임, 음성 인식(STT)·음성 합성(TTS) 기반 AI 서비스처럼 실시간 양방향 통신이 핵심인 서비스가 늘어나면서, 어떤 네트워크 통신 프로토콜을 선택하느냐가 서비스의 품질을 좌우하는 중요한 설계 결정이 되었습니다.

2. 네트워크 통신(인터넷 실시간 통신) 방식 7가지 정리

2-1. HTTP: 가장 기본이 되는 ‘요청과 응답’

HTTP는 웹의 근간입니다. 가장 직관적이고 표준적인 통신 방식이죠.

  • 개념: 클라이언트(사용자)가 “이거 줘!”라고 요청(Request)하면, 서버가 “여기 있어”라고 응답(Response)하는 방식입니다.
  • 특징: 단방향 통신이며, 서버는 요청이 오기 전까지는 아무것도 하지 않습니다. 또한, 무상태(Stateless)라서 이전의 대화를 기억하지 못합니다.
  • 장점: 구현이 매우 단순하고, 모든 브라우저에서 기본적으로 지원합니다.
  • 단점: 실시간 통신이 불가능합니다. 서버가 능동적으로 데이터를 보내려면 계속해서 요청을 보내야 하는 비효율이 발생합니다.

2-2. WebSocket: 실시간 양방향의 정석

HTTP의 한계를 극복하기 위해 등장한 기술입니다.

  • 개념: 한 번 연결을 맺으면 끊지 않고 계속 유지하며 서로 데이터를 주고받는 방식입니다.
  • 특징: 전이중(Full-Duplex) 통신이 가능합니다. 즉, 서버와 클라이언트가 동시에 서로에게 데이터를 던질 수 있죠.
  • 장점: 연결을 유지하므로 HTTP보다 훨씬 빠르고 효율적입니다. 서버가 먼저 데이터를 보낼 수 있다는 점이 핵심입니다.
  • 단점: 서버가 항상 연결을 유지해야 하므로 메모리와 리소스 소모가 큽니다.

2-3. SSE (Server-Sent Events): 서버의 일방적인 알림

서버에서 클라이언트로 정보를 쏘아주는 것만 필요하다면 굳이 복잡한 웹소켓을 쓸 필요가 없습니다.

  • 개념: 서버에서 클라이언트로 실시간으로 이벤트를 보내는 단방향 기술입니다.
  • 특징: HTTP 기반으로 동작하여 구현이 매우 간편합니다.
  • 장점: 연결이 끊겨도 자동으로 재연결을 시도하는 기능이 내장되어 있습니다.
  • 단점: 오직 서버에서 클라이언트로만 보낼 수 있습니다. 채팅처럼 양방향이 필요한 서비스에는 부적합합니다.

2-4. WebRTC: 브라우저끼리의 직접적인 연결

중간 서버를 거치지 않고 사용자끼리 직접 연결하는 방식입니다.

  • 개념: P2P(Peer-to-Peer) 기술을 사용하여 클라이언트들이 직접 통신합니다.
  • 특징: 음성, 영상 전송에 최적화되어 있습니다.
  • 장점: 서버를 거치지 않아 지연 시간이 극도로 짧습니다.
  • 단점: 방화벽을 통과하거나 연결을 설정하는 과정(시그널링)이 매우 복잡합니다.

2-5. gRPC: 구글이 만든 고성능 통신

마이크로서비스 아키텍처에서 시스템 간의 통신을 위해 만들어진 강력한 도구입니다.

  • 개념: Protocol Buffers라는 이진(Binary) 데이터를 사용하여 통신 속도를 극대화했습니다.
  • 특징: HTTP/2를 기반으로 하며, 데이터 크기가 작아 매우 빠릅니다.
  • 장점: 다중 언어를 지원하고, 양방향 스트리밍이 가능하며 데이터 규격이 엄격하여 안전합니다.
  • 단점: 브라우저에서 직접 사용하기 어렵고, 구현이 복잡합니다.

2-6. MQTT: IoT를 위한 가볍고 빠른 메신저

스마트홈, 센서 데이터 등 인터넷 연결이 불안정하거나 저전력이 필요한 환경에 쓰입니다.

  • 개념: 발행(Publish)과 구독(Subscribe) 모델을 사용합니다. 특정 주제(Topic)를 구독해두면, 누군가 그 주제로 데이터를 보낼 때 받는 방식입니다.
  • 특징: 헤더가 매우 작아 통신량이 적습니다.
  • 장점: 배터리 소모가 적고, 저사양 기기에서도 잘 작동합니다.

2-7. Long Polling: 고전적인 실시간 흉내내기

최신 기술이 없던 시절, 실시간처럼 보이기 위해 사용하던 방법입니다.

  • 개념: 요청을 보내고 서버에서 응답이 올 때까지 계속 기다립니다. 응답이 오면 즉시 다시 요청을 보내는 방식입니다.
  • 단점: 연결을 맺고 끊는 과정이 반복되어 서버에 엄청난 부하를 줍니다. 요즘은 거의 사용하지 않습니다.

3. 통합 비교표

7가지 네트워크 통신 프로토콜의 방향성, 속도, 실시간성, 복잡도, 대표 사용처를 하나의 표로 정리했습니다.

프로토콜방향속도실시간성복잡도대표 사용처
HTTP단방향중간낮음일반 웹 페이지 로딩, 웹 API, 파일 전송
WebSocket양방향빠름✔️중간채팅, 온라인 게임, AutoPlay(자동 재생)
SSE단방향(서버→클라이언트)빠름✔️낮음실시간 알림, 주식 정보, 뉴스 피드
WebRTCP2P 양방향초고속✔️높음화상 회의, 음성 통화
gRPC양방향초고속✔️높음서버 간 내부 통신, 마이크로서비스
MQTT발행/구독(Pub/Sub)빠름✔️중간IoT 센서 데이터, 스마트홈
Long Polling단방향느림낮음구형 시스템(레거시)

각 방식이 실시간 양방향 통신 서비스에 적합하지 않은 이유

WebSocket이 필요한 상황(서버가 먼저 데이터를 보내야 하고, 양방향 실시간 통신이 필요한 경우)에서 다른 프로토콜을 사용할 수 없는 이유를 정리하면 다음과 같습니다.

방식안 되는 이유
HTTP서버가 먼저 데이터를 보낼 수 없음 (AutoPlay 같은 서버 주도형 기능 구현 불가)
SSE단방향이라 클라이언트가 서버로 데이터를 보낼 수 없음
WebRTCP2P 연결이 필요 없는 구조에는 시그널링 등 구현이 과도하게 복잡
gRPC브라우저 네이티브 지원이 좋지 않아 웹 프론트엔드에 부적합
MQTT브로커(Broker) 서버가 별도로 필요해 오버스펙
Long Polling연결을 계속 맺고 끊어 너무 느리고 서버 부하가 큼

4. 웹소켓(WebSocket)을 선택해야 하는 이유

다양한 네트워크 통신 방식 중에서 실시간 음성 인식(STT), 음성 합성(TTS), 자동 재생(AutoPlay) 기능이 필요한 서비스에는 웹소켓(WebSocket)이 가장 적합합니다. 그 이유는 명확합니다.

1) 서버의 능동적 개입이 가능합니다. AutoPlay와 같이 서버가 스스로 판단하여 클라이언트에게 즉시 명령을 내려야 하는 경우, 서버가 데이터를 먼저 보낼 수 있는 웹소켓이 필수입니다. HTTP는 클라이언트가 요청해야만 서버가 응답할 수 있는 구조이기 때문에 이런 시나리오 자체가 불가능합니다.

2) 실시간 상호작용에 최적화되어 있습니다. 사용자의 음성 인식(STT/TTS) 결과는 0.1초의 지연도 체감될 수 있습니다. 연결을 유지하는 웹소켓은 데이터를 주고받을 때마다 다시 연결을 맺을 필요가 없어 속도가 빠릅니다.

3) 완전한 양방향성을 제공합니다. 클라이언트와 서버가 쉼 없이 데이터를 주고받아야 하는 환경에서, 웹소켓은 가장 안정적이고 효율적인 선택지입니다.

4) 다른 대안들은 저마다의 한계가 명확합니다. HTTP는 서버가 먼저 데이터를 줄 수 없고, SSE는 양방향이 안 되며, WebRTC는 P2P 연결이 필요 없는 일반적인 서비스 목적상 과도하게 복잡합니다. 따라서 성능, 구현 난이도, 서비스 요구사항을 종합적으로 고려했을 때 웹소켓을 항상 연결 유지하는 것이 가장 현명한 설계 방향입니다.

이를 정리하면 아래와 같은 체크리스트로 요약할 수 있습니다.

웹소켓을 선택해야 하는 이유 요약

  • 서버가 먼저 보내야 함 (AutoPlay 자동 재생)
  • 실시간 양방향 통신 필요 (STT/TTS 결과 전송)
  • 빠른 응답 속도 (음성 인식 결과 즉시 표시)
  • 연결 유지 (항상 대기 상태 유지 가능)

결론

  • AutoPlay 자동 재생 가능
  • 실시간 STT/TTS 결과 전송 가능
  • 완전한 양방향 통신 지원
  • 적절한 복잡도와 우수한 성능의 균형

네트워크 통신 방식은 정답이 정해져 있지 않습니다. 서비스의 목적에 따라 가장 효율적인 ‘대화법’을 선택하는 것이 개발의 시작입니다. 지금부터는 웹소켓을 실제로 어떻게 설정하고, 어떻게 항상 연결 상태를 유지하는지 구체적인 구현 방법을 살펴보겠습니다.

5. 웹소켓(WebSocket) 설정 방법

5-1. 웹소켓이란?

웹소켓(WebSocket) = 실시간 양방향 통신을 위한 프로토콜

  • 한 번 연결하면 계속 유지됩니다.
  • 서버가 먼저 데이터를 보낼 수 있습니다 (AutoPlay 기능 구현에 필수!).
  • HTTP보다 빠르고 효율적입니다.

5-2. 웹소켓 전체 구조

브라우저(클라이언트)와 서버가 어떻게 연결을 맺고 데이터를 주고받는지 전체 흐름을 도식화하면 다음과 같습니다.

인터넷 실시간 통신 방식

이 구조에서 핵심은 연결이 한 번 맺어지면 계속 유지된다는 점, 그리고 서버가 4번 단계처럼 클라이언트의 요청 없이도 먼저 데이터(auto_play_start)를 보낼 수 있다는 점입니다. 이것이 바로 HTTP와 근본적으로 다른 웹소켓만의 장점입니다.

5-3. 서버 측 설정 (web_app.py, FastAPI 기준)

웹소켓 엔드포인트 생성

FastAPI에서 웹소켓 엔드포인트를 만드는 가장 기본적인 형태입니다.

from fastapi import WebSocket, WebSocketDisconnect

@app.websocket("/ws/voice")
async def websocket_voice(websocket: WebSocket):
    # 1. 연결 수락
    await websocket.accept()
    print("[WS] Client connected")
    
    # 2. 연결 관리
    try:
        while True:
            # 3. 메시지 수신 대기 (60초 타임아웃)
            message = await asyncio.wait_for(websocket.receive(), timeout=60.0)
            
            # 4. 메시지 처리
            if "bytes" in message:
                # 오디오 데이터 처리
                await process_audio(message["bytes"])
            elif "text" in message:
                # 텍스트 메시지 처리
                data = json.loads(message["text"])
                if data.get("type") == "ping":
                    # 5. pong 응답 (연결 유지)
                    await websocket.send_json({"type": "pong"})
                    
    except WebSocketDisconnect:
        print("[WS] Client disconnected")
    except Exception as e:
        print(f"[WS] Error: {e}")
    finally:
        # 6. 연결 정리
        await cleanup_connection(websocket)

WebSocketPool로 연결 관리하기

동시에 여러 클라이언트가 접속할 수 있으므로, 연결을 체계적으로 관리하는 풀(Pool) 클래스를 만들어두면 유지보수가 훨씬 쉬워집니다.

class WebSocketPool:
    def __init__(self, max_connections: int = 3):
        self.max_connections = max_connections
        self.connections: List[WebSocket] = []
        self.lock = asyncio.Lock()
        
    async def add(self, websocket: WebSocket):
        async with self.lock:
            if len(self.connections) < self.max_connections:
                self.connections.append(websocket)
                return True
            return False
            
    async def remove(self, websocket: WebSocket):
        async with self.lock:
            if websocket in self.connections:
                self.connections.remove(websocket)
                
    async def broadcast(self, data: dict):
        """모든 클라이언트에게 메시지 전송"""
        if not self.connections:
            return 0
            
        disconnected = []
        success = 0
        
        for ws in self.connections:
            try:
                await ws.send_json(data)
                success += 1
            except:
                disconnected.append(ws)
                
        # 끊긴 연결 정리
        for ws in disconnected:
            await self.remove(ws)
            
        return success

asyncio.Lock()을 사용해 동시성 문제를 방지하고, max_connections로 최대 접속 수를 제한하며, broadcast() 메서드로 끊긴 연결을 자동 정리하는 것이 핵심 포인트입니다.

5-4. 클라이언트 측 설정 (voice-chat.js)

웹소켓 연결 생성

브라우저에서 웹소켓 객체를 생성하고 기본 이벤트 핸들러를 등록하는 방법입니다.

// WebSocket 객체 생성
var ws = new WebSocket("wss://buddha.qcai.kr/ws/voice");

// 연결 성공 시
ws.onopen = function() {
    console.log("WebSocket Connected!");
    // 연결 성공 메시지 전송 (선택사항)
    ws.send(JSON.stringify({ 
        type: "register", 
        client: "browser" 
    }));
};

// 메시지 수신 시
ws.onmessage = function(event) {
    var data = JSON.parse(event.data);
    console.log("Received:", data.type);
    
    if (data.type === "auto_play_start") {
        // AutoPlay 처리
        playAudioFile(data.filename, data.audio_url);
    }
};

// 연결 종료 시
ws.onclose = function(event) {
    console.log("WebSocket Closed:", event.code);
    // 자동 재연결
    setTimeout(function() {
        connectWebSocket();
    }, 3000);
};

// 에러 발생 시
ws.onerror = function(error) {
    console.error("WebSocket Error:", error);
};

Ping/Pong으로 연결 유지하기

웹소켓 연결이 오래 유지되다 보면 중간에 프록시나 방화벽이 유휴 연결을 끊어버리는 경우가 있습니다. 이를 방지하기 위해 주기적으로 ping을 보내고 서버가 pong으로 응답하는 하트비트(Heartbeat) 방식을 사용합니다.

// 클라이언트 → 서버 (ping)
var pingInterval = setInterval(function() {
    if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ 
            type: "ping", 
            timestamp: Date.now() 
        }));
    }
}, 15000);  // 15초마다

// 서버 → 클라이언트 (pong 응답)
// 서버에서 자동으로 응답

5-5. 웹소켓 항상 연결 유지하는 방법 (핵심)

지금까지 기본적인 연결 설정을 다뤘다면, 이제 실무에서 가장 중요한 **”웹소켓을 항상 연결 유지하는 방법”**을 단계별로 자세히 설명하겠습니다. 네트워크 불안정, 서버 재시작, 브라우저 절전 모드 등으로 연결이 끊기더라도 서비스가 끊김 없이 동작하려면 아래 4가지 요소가 모두 필요합니다.

① 자동 재연결 (Auto-Reconnect)

연결이 끊어졌을 때 지수 백오프(exponential backoff) 방식으로 재연결을 시도하는 로직입니다. 무한정 재시도하지 않고 최대 시도 횟수를 두어 서버 부하를 방지합니다.

var MAX_RECONNECT_ATTEMPTS = 10;
var wsReconnectAttempts = 0;

function connectWebSocket() {
    // 이미 연결되어 있으면 유지
    if (ws && ws.readyState === WebSocket.OPEN) {
        console.log("Already connected");
        return;
    }
    
    // 최대 재시도 횟수 체크
    if (wsReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
        console.log("Max attempts reached, resetting...");
        wsReconnectAttempts = 0;
        setTimeout(connectWebSocket, 60000);  // 60초 후 재시도
        return;
    }
    
    // WebSocket 생성
    ws = new WebSocket(WS_URL);
    
    ws.onopen = function() {
        wsReconnectAttempts = 0;  // 성공 시 초기화
        console.log("Connected!");
    };
    
    ws.onclose = function() {
        wsReconnectAttempts++;
        var delay = Math.min(2000 * wsReconnectAttempts, 30000);
        console.log("Reconnecting in " + delay + "ms...");
        setTimeout(connectWebSocket, delay);
    };
}

여기서 Math.min(2000 * wsReconnectAttempts, 30000)은 재시도 횟수가 늘어날수록 대기 시간을 늘리되 최대 30초를 넘지 않도록 제한하는 지수 백오프 패턴입니다. 이렇게 하면 서버가 일시적으로 다운되었을 때 클라이언트들이 동시에 몰려서 재접속을 시도하는 부하 폭증을 방지할 수 있습니다.

② 주기적 연결 상태 확인

onclose 이벤트가 항상 정확히 발생하지 않는 네트워크 환경(예: 갑작스러운 와이파이 단절)도 있기 때문에, 별도의 타이머로 연결 상태를 주기적으로 감시하는 이중 안전장치를 두는 것이 좋습니다.

// 5초마다 연결 상태 확인
setInterval(function() {
    if (!ws || ws.readyState !== WebSocket.OPEN) {
        console.log("Connection lost, reconnecting...");
        if (ws) {
            try { ws.close(); } catch(e) {}
            ws = null;
        }
        connectWebSocket();
    }
}, 5000);

③ 페이지 로드 시 자동 연결

사용자가 페이지에 접속하는 즉시 웹소켓을 연결하고, 네트워크 지연을 고려해 1초, 3초 후에도 연결 여부를 재확인하는 다중 시도 로직입니다.

// 페이지 로드 완료 후
document.addEventListener('DOMContentLoaded', function() {
    // 즉시 연결
    connectWebSocket();
    
    // 1초 후 재시도
    setTimeout(function() {
        if (!ws || ws.readyState !== WebSocket.OPEN) {
            connectWebSocket();
        }
    }, 1000);
    
    // 3초 후 재시도
    setTimeout(function() {
        if (!ws || ws.readyState !== WebSocket.OPEN) {
            connectWebSocket();
        }
    }, 3000);
});

5-6. 웹소켓 상태(readyState) 관리

웹소켓 객체는 현재 연결 상태를 나타내는 readyState 속성을 가지고 있습니다. 이 값을 정확히 이해하고 활용하는 것이 안정적인 연결 관리의 기본입니다.

상태의미
CONNECTING0연결 중
OPEN1연결됨 (정상)
CLOSING2닫는 중
CLOSED3닫힘

상태 확인을 위한 헬퍼 함수는 다음과 같이 작성할 수 있습니다.

function isWebSocketConnected() {
    return ws && ws.readyState === WebSocket.OPEN;
}

function getWebSocketStatus() {
    var states = {
        0: "CONNECTING",
        1: "OPEN",
        2: "CLOSING",
        3: "CLOSED"
    };
    return states[ws?.readyState] || "UNKNOWN";
}

// 사용 예
console.log("Status:", getWebSocketStatus());
if (isWebSocketConnected()) {
    console.log("WebSocket is ready!");
} else {
    console.log(" WebSocket is not connected");
}

5-7. 전체 설정 코드 (최종 통합본)

지금까지 설명한 서버 측 로직(엔드포인트 + WebSocketPool)과 클라이언트 측 로직(연결 + ping/pong + 자동 재연결 + 상태 관리)을 모두 하나로 통합한 실전 투입 가능한 최종 코드입니다.

서버 측 (web_app.py)

# ============================================================
# WebSocket 설정
# ============================================================
from fastapi import WebSocket, WebSocketDisconnect
import asyncio
import json
import time

class WebSocketPool:
    def __init__(self, max_connections: int = 3):
        self.max_connections = max_connections
        self.connections = []
        self.lock = asyncio.Lock()
        
    async def add(self, websocket):
        async with self.lock:
            if len(self.connections) < self.max_connections:
                self.connections.append(websocket)
                return True
            return False
            
    async def remove(self, websocket):
        async with self.lock:
            if websocket in self.connections:
                self.connections.remove(websocket)
                
    async def broadcast(self, data):
        if not self.connections:
            return 0
        disconnected = []
        success = 0
        for ws in self.connections:
            try:
                await ws.send_json(data)
                success += 1
            except:
                disconnected.append(ws)
        for ws in disconnected:
            await self.remove(ws)
        return success
        
    def get_count(self):
        return len(self.connections)

ws_pool = WebSocketPool(max_connections=3)

@app.websocket("/ws/voice")
async def websocket_voice(websocket: WebSocket):
    await websocket.accept()
    await ws_pool.add(websocket)
    
    print(f"[WS] Connected (pool: {ws_pool.get_count()})")
    
    try:
        while True:
            try:
                message = await asyncio.wait_for(websocket.receive(), timeout=60.0)
            except asyncio.TimeoutError:
                # 연결 유지 확인
                try:
                    await websocket.send_json({"type": "ping"})
                    continue
                except:
                    break
                    
            if "text" in message:
                data = json.loads(message["text"])
                if data.get("type") == "ping":
                    await websocket.send_json({"type": "pong"})
                    
    except WebSocketDisconnect:
        print("[WS] Disconnected")
    finally:
        await ws_pool.remove(websocket)
        print(f"[WS] Closed (pool: {ws_pool.get_count()})")

클라이언트 측 (voice-chat.js)

// ============================================================
// WebSocket 설정 - 항상 연결 유지
// ============================================================
var WS_URL = "wss://buddha.qcai.kr/ws/voice";
var ws = null;
var wsConnecting = false;
var wsReconnectAttempts = 0;
var MAX_RECONNECT_ATTEMPTS = 10;
var pingInterval = null;
var reconnectTimer = null;

function connectWebSocket() {
    // 이미 연결되어 있으면 유지
    if (ws && ws.readyState === WebSocket.OPEN) {
        console.log("[WS] Already connected");
        return;
    }
    
    // 이미 연결 중이면 대기
    if (wsConnecting) {
        console.log("[WS] Already connecting");
        return;
    }
    
    // 닫혀 있으면 정리
    if (ws) {
        try { ws.close(); } catch(e) {}
        ws = null;
    }
    
    // 최대 시도 초과 시 리셋
    if (wsReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
        console.log("[WS] Max attempts, resetting...");
        wsReconnectAttempts = 0;
        setTimeout(connectWebSocket, 60000);
        return;
    }
    
    wsConnecting = true;
    wsReconnectAttempts++;
    
    try {
        console.log("[WS] Connecting to:", WS_URL);
        ws = new WebSocket(WS_URL);
        window.ws = ws;
        
        // 연결 타임아웃 (3초)
        var timeoutId = setTimeout(function() {
            if (ws && ws.readyState !== WebSocket.OPEN) {
                console.log("[WS] Connection timeout");
                try { ws.close(); } catch(e) {}
                ws = null;
                wsConnecting = false;
                setTimeout(connectWebSocket, 2000);
            }
        }, 3000);
        
        ws.onopen = function() {
            clearTimeout(timeoutId);
            console.log("[WS] Connected");
            wsConnecting = false;
            wsReconnectAttempts = 0;
            
            // ping/pong 시작
            if (pingInterval) clearInterval(pingInterval);
            pingInterval = setInterval(function() {
                if (ws && ws.readyState === WebSocket.OPEN) {
                    try {
                        ws.send(JSON.stringify({
                            type: "ping",
                            timestamp: Date.now()
                        }));
                    } catch(e) {}
                }
            }, 15000);
        };
        
        ws.onmessage = function(event) {
            try {
                var data = JSON.parse(event.data);
                console.log("[WS] Received:", data.type);
                
                if (data.type === "auto_play_start") {
                    // AutoPlay 처리
                    handleAutoPlay(data);
                } else if (data.type === "pong") {
                    console.log("[WS] pong received");
                }
            } catch(e) {
                console.error("[WS] Parse error:", e);
            }
        };
        
        ws.onclose = function(event) {
            clearTimeout(timeoutId);
            console.log("[WS] Closed:", event.code);
            wsConnecting = false;
            ws = null;
            
            if (pingInterval) {
                clearInterval(pingInterval);
                pingInterval = null;
            }
            
            // 자동 재연결
            var delay = Math.min(2000 * wsReconnectAttempts, 30000);
            console.log("[WS] Reconnecting in " + delay + "ms...");
            if (reconnectTimer) clearTimeout(reconnectTimer);
            reconnectTimer = setTimeout(connectWebSocket, delay);
        };
        
        ws.onerror = function(error) {
            clearTimeout(timeoutId);
            console.error("[WS] Error:", error);
            wsConnecting = false;
        };
        
    } catch(e) {
        console.error("[WS] Creation error:", e);
        wsConnecting = false;
        ws = null;
        setTimeout(connectWebSocket, 3000);
    }
}

// AutoPlay 처리 함수
function handleAutoPlay(data) {
    console.log("[AutoPlay] Starting:", data.filename);
    
    if (data.audio_url) {
        var fullUrl = data.audio_url.startsWith("http")
            ? data.audio_url
            : API_URL + data.audio_url;
        
        playAudioFile(data.filename, fullUrl);
        addMessage(data.message, "system");
    }
}

// 연결 상태 확인
function isWebSocketConnected() {
    return ws && ws.readyState === WebSocket.OPEN;
}

// ============================================================
// 초기화 - 항상 연결
// ============================================================
(function() {
    console.log("[Init] WebSocket auto-connect...");
    
    // 1. 즉시 연결
    connectWebSocket();
    
    // 2. 1초 후 재시도
    setTimeout(function() {
        if (!isWebSocketConnected()) {
            console.log("[Init] Retry 1...");
            connectWebSocket();
        }
    }, 1000);
    
    // 3. 3초 후 재시도
    setTimeout(function() {
        if (!isWebSocketConnected()) {
            console.log("[Init] Retry 2...");
            connectWebSocket();
        }
    }, 3000);
    
    // 4. 5초마다 상태 확인
    setInterval(function() {
        if (!isWebSocketConnected()) {
            console.log("[WS] Not connected, reconnecting...");
            if (ws) {
                try { ws.close(); } catch(e) {}
                ws = null;
            }
            wsConnecting = false;
            connectWebSocket();
        }
    }, 5000);
})();

이 최종 코드의 핵심은 3중 안전장치입니다. onclose 이벤트 기반 재연결, 5초마다 도는 상태 확인 타이머, 그리고 페이지 로드 시점의 즉시/1초/3초 다단계 연결 시도가 서로를 보완하면서, 어떤 상황에서도 웹소켓 연결이 끊긴 채로 방치되지 않도록 설계되어 있습니다.

6. 설정 체크리스트

웹소켓을 실제 서비스에 적용하기 전, 아래 항목들을 모두 점검해보세요.

항목확인설명
서버 엔드포인트✔️/ws/voice
WebSocketPool✔️연결 관리 (최대 접속 수 제한, broadcast)
Ping/Pong✔️15초 간격 하트비트
자동 재연결✔️최대 10회 시도, 지수 백오프
연결 모니터링✔️5초마다 상태 확인
페이지 로드 연결✔️즉시 + 1초 + 3초 재시도

웹소켓 설정 3단계 요약

1) 서버 설정 (web_app.py)

  • 웹소켓 엔드포인트 생성
  • WebSocketPool로 연결 관리
  • Ping/Pong 처리

2) 클라이언트 설정 (voice-chat.js)

  • WebSocket 객체 생성
  • onopen / onmessage / onclose / onerror 이벤트 처리
  • Ping/Pong 전송

3) 항상 연결 유지

  • 자동 재연결 (onclose에서 지수 백오프로 재시도)
  • 주기적 상태 확인 (5초마다)
  • 페이지 로드 시 즉시 연결 + 다단계 재시도

7. 자주 묻는 질문 (FAQ)

웹소켓과 SSE는 어떤 차이가 있나요

웹소켓은 서버와 클라이언트가 서로 데이터를 주고받는 완전한 양방향(Full-Duplex) 통신이고, SSE는 서버에서 클라이언트로만 데이터를 보낼 수 있는 단방향 통신입니다. 채팅이나 음성 인식처럼 클라이언트도 서버로 데이터를 보내야 하는 경우에는 웹소켓을, 실시간 알림처럼 서버가 보내주기만 하면 되는 경우에는 SSE를 선택하는 것이 일반적입니다.

웹소켓 연결이 자꾸 끊기는 이유는 무엇인가요?

대표적인 원인은 (1) 프록시나 로드밸런서, 방화벽이 일정 시간 데이터가 오가지 않는 유휴 연결을 강제로 종료하는 경우, (2) 클라이언트 기기의 절전 모드나 네트워크 전환(와이파이↔모바일 데이터), (3) 서버 측 타임아웃 설정입니다. 이를 방지하려면 본문에서 다룬 것처럼 Ping/Pong 하트비트와 자동 재연결 로직을 반드시 함께 구현해야 합니다.

웹소켓 서버는 왜 연결 개수를 제한해야 하나요?

웹소켓은 연결을 계속 유지해야 하므로 HTTP 요청/응답 방식보다 서버 메모리와 리소스를 더 많이 소비합니다. WebSocketPool처럼 max_connections를 두어 동시 접속 수를 제한하면, 예상치 못한 트래픽 폭증으로 서버가 다운되는 상황을 예방할 수 있습니다.

FastAPI 외의 다른 프레임워크에서도 이 구조를 그대로 쓸 수 있나요?

클라이언트 측(JavaScript) 로직인 자동 재연결, Ping/Pong, readyState 상태 관리는 표준 WebSocket API를 사용하므로 Node.js(Express, NestJS), Django Channels, Spring 등 어떤 백엔드 프레임워크와 연동하더라도 동일하게 적용할 수 있습니다. 서버 측 코드만 사용하는 프레임워크의 웹소켓 문법에 맞게 바꿔주면 됩니다.

실시간 STT/TTS 서비스에는 왜 특히 웹소켓을 권장하나요?

음성 인식(STT)과 음성 합성(TTS)은 오디오 데이터를 스트리밍 방식으로 주고받아야 하고, 인식 결과나 AutoPlay 같은 서버 주도형 이벤트를 지연 없이 전달해야 하기 때문입니다. HTTP 폴링 방식으로는 이런 실시간성을 구현하기 어렵기 때문에, 음성 기반 AI 서비스에서는 웹소켓이 사실상 표준으로 자리잡고 있습니다.

네트워크 통신 방식은 정답이 정해져 있지 않습니다. HTTP, 웹소켓, SSE, WebRTC, gRPC, MQTT, Long Polling은 각각 탄생 배경과 목적이 다르며, 서비스의 요구사항에 따라 가장 효율적인 ‘대화법’을 선택하는 것이 개발의 시작입니다.

특히 실시간 양방향 통신이 필요한 채팅, 온라인 게임, AutoPlay 기반 서비스, 실시간 STT/TTS 음성 서비스에서는 웹소켓(WebSocket)이 가장 합리적인 선택입니다. 다만 웹소켓의 진짜 실력은 단순히 연결을 만드는 것이 아니라, Ping/Pong 하트비트, 지수 백오프 기반 자동 재연결, 주기적 상태 확인, 페이지 로드 시 다단계 연결 시도라는 4가지 요소를 통해 “항상 연결을 유지하는 것”에 달려 있습니다.

댓글 남기기