WebSocket 사용 이유?와 Swift 내 URLSessionWebSocketTask

먼저 채팅 서비스를 구현해야한다고 했을때 다들 대부분 WebSocket을 사용해야하는 것은 알고있지만 어떤 이유로 웹소켓을 활용해야하는지 비교를 통해 개념을 잡아보고 iOS 에서는 어떤 방식을 사용해서 웹소켓을 구현할 수 있는지 구현 전략에 대해서 알아보고자 한다.

  • HTTP 방식과 WebSocket의 차이 
    통신 방식 요청 → 응답 (단방향) 지속 연결 (양방향)
    연결 유지 요청마다 새로 연결 한 번 연결 후 유지
    데이터 전송 단위 바이트 기반 메시지 단위 (텍스트/바이너리)
    지연시간 높음 (요청/응답 반복) 낮음 (이벤트 실시간 처리)
    • 메시지 지향 (Message-Oriented)
      • HTTP는 바이트 데이터를 주고 받는 방식이라 데이터 경계를 직접 구분해야 한다.
      • WebSocket은 프레임 → 메시지로 구조화 되어있어 데이터를 한건의 메시지 단위로 주고받을 수 있다.
      • ex : 채팅에서 “안녕”이라는 메시지를 보냈을 때, 그 하나가 독립적인 단위가 되어야 UI에 표시가 가능하다.
    • 낮은 지연 (Low Latency)
      • HTTP기반의 폴링/ 롱폴링 요청-응답 방식이라 새로운 데이터가 있어도 주기적으로 요청해야 한다.
      • WebSocket은 지속 연결로 서버가 즉시 클라이언트에게 Push할 수 있어 지연이 거의 없다.
      • ex : 상대방이 메시지를 보낼 때 0.5초 내로 받으려면 WebSocket이 필요하다.
    • 양방향 통신 (Full Duplex)
      • HTTP는 클라이언트가 요청해야만 서버가 응답이 가능한 형태!
      • WebSocket은 서버도 먼저 메시지를 보낼 수 있다.
      • ex : 타이핑 표시, 읽은 확인, 시스템 알림 등 서버 → 클라이언트 방향으로 이뤄지는 통신이 가능함
    • TCP 기반의 신뢰성과 순서 보장
      • WebSocket은 TCP 위에 있다 → 손실되면 자동으로 재전송되고 순서가 유지 된다.
      • 큰 용량을 차지하는 메시지가 늦게 오면 전체적인 흐름이 막힐 수 있기 때문에 파일 전송과 같은 서비스는 HTTP로, 메시지는 WebSocket으로 분리하여 구현하는 접근이 필요하다.
    • 유휴 연결 관리 : Heartbeat
      • 모바일에서는 네트워크가 바뀌거나, 프록시가 유휴 연결을 끊기도 한다.
      • 그렇기 때문에 WebSocket은 주기적으로 Ping/Pong으로 살아있는지 확인한다.

정리하자면 채팅 서비스를 구현해야할 때 WebSocket이 필요한 이유는 메시지를 즉시 받아야하며, 상대방이 입력 중인 상태를 확인할 수 있는 환경을 구축할 수 있다. 또한 읽음과 전송 확인이 실시간으로 이뤄질 수 있다는 점 등등 물론 HTTP를 활용해서 폴링/푸시를 사용해 구현은 가능하지만 WebSokect으로 더 효율적으로 구현할 수 있다는 점 때문!!


URLSessionWebSocketTask 사용

iOS는 WebSocket을 구현할 수 있는 API 를 제공해주고 있는데 이것이 바로바로 URLSessionWebSocketTask 이다!

 

구성 요소 설명

연결 (Connect) 서버와 연결을 열고 유지 (resume)
수신 (Receive) 메시지 한 건씩 계속 기다리며 받는 루프
송신 (Send) 메시지 보내기, 완료 시 핸들링
Heartbeat 주기적인 Ping으로 연결 상태 확인
재연결 네트워크 오류/종료 시 자동 재연결 로직
멱등성 처리 중복 메시지 처리(예: clientMsgId 사용)
메시지 파싱 JSON 메시지를 Swift 모델로 변환해서 처리

대표적인 기능으로 이렇게 구현할 수 있도록 제공해준다.

final class ChatWebSocketClient {
    private var webSocketTask: URLSessionWebSocketTask?
    private let session = URLSession(configuration: .default)

    // 연결 시작
    func connect() {
        let url = URL(string: "wss://yourserver.com/ws")!
        webSocketTask = session.webSocketTask(with: url)
        webSocketTask?.resume()
        listen()  // 수신 시작
        sendPing() // 핑 시작
    }

    // 연결 종료
    func disconnect() {
        webSocketTask?.cancel(with: .goingAway, reason: nil)
    }
}

기본적으로 사용하는 클라이언트를 구성하게 된다.

 

그렇다면 메시지 수신 루프를 만들어 보자

private func listen() {
    webSocketTask?.receive { [weak self] result in
        switch result {
        case .success(let message):
            switch message {
            case .string(let text):
                print("Received text: \\(text)")
                // 여기에 JSON → 모델 변환 파싱 로직 추가
            case .data(let data):
                print("Received binary data: \\(data)")
            @unknown default:
                break
            }
            self?.listen()  // 계속해서 수신 대기
        case .failure(let error):
            print("Receive failed: \\(error)")
            self?.reconnect() // 실패 시 재연결
        }
    }
}

메시지를 수신했다면 송신은

func send(message: String) {
    let wsMessage = URLSessionWebSocketTask.Message.string(message)
    webSocketTask?.send(wsMessage) { error in
        if let error = error {
            print("Send failed: \\(error)")
        } else {
            print("Message sent")
        }
    }
}

이와 같이 구현하면 메시지 송수신할 수 있는 구조를 만들어 낼 수 있다.

 

그 다음

private func sendPing() {
    webSocketTask?.sendPing { [weak self] error in
        if let error = error {
            print("❌ Ping failed: \\(error)")
            self?.reconnect()
        } else {
            print("✅ Ping success")
            DispatchQueue.global().asyncAfter(deadline: .now() + 15) {
                self?.sendPing() // 반복
            }
        }
    }
}

핑을 꾸준하게 일정 시간을 잡고 요청을 통해 유효한지 확인 해야하는데. 왜냐하면 WebSocket은 하나의 연결을 오랫동안 유지하는 방식이기 때문에 연결이 물리적으로 끊기게 됐을때 클라이언트 단에서는 그 즉시 알지 못할 수 있다.

 

예를 들어 서버가 죽었거나, 클라이언트가 Wi-Fi → LTE로 바뀌었거나, 라우터/NAT가 유휴 연결을 강제로 닫았거나 이런 상황에서는 실제로 끊겼지만 이상없이 유지하고 있다고 착각할 수 있기 때문.

 

그렇기 때문에 주기적으로 Ping을 보내고 Pong을 받아야 이 연결이 살아있음을 확인할 수 있다.

 

특히 백그라운드 진입, 절전, 네트워크 변경이 앱에서는 특히 자주 일어나기 때문에 해당 핑퐁 전략은 매우 필수적이므로 잊지 않도록 하자..!

 

오늘은 여기까지!