[iOS] STOMP로 실시간 채팅 구현하기 (Web Socket 통신) with SwiftUI

parkgyurim·2022년 1월 26일
2

iOS

목록 보기
3/5
post-thumbnail

💻 이 글은 Swift 5.0, Xcode Version 13.2.1 를 바탕으로 작성하였습니다.

STOMP

STOMPSimple (or Streaming) Text Orientated Messaging Protocol 의 줄임말으로, 특정 토픽을 Subscribe하고 있는 클라이언트에게 메시지를 전달하고 특정 토픽에 메시지를 Publish할 수 있도록 하는 프로토콜입니다.

➡️ Stomp 공식 사이트

특히 iOS에서 Stomp를 활용한 채팅 구현 방식은 자료가 매우 부족해서 어려움을 겪었고, 한국어 자료는 더욱이 부족해서 제가 구현해본 방식을 한번 정리해보겠습니다.


StompClientLib

StompClientLib는 Swift를 사용하여 Stomp client를 구현할 수 있도록 하는 SDK입니다.
[WrathChaos / StompClientLib] ➡️ https://github.com/WrathChaos/StompClientLib

- StompClientLib 설치

Cocoapods를 이용해서 StompClientLib를 설치합니다.
Pod init 이후, Podfile을 열어 아래 문장을 추가하고 Pod install !

# Pods for [Project_Name]
pod "StompClientLib"

- 프로젝트에 import

import StompClientLib

Stomp Client 구현

- Stomp Client 선언

    // Socket Client instance
    var socketClient = StompClientLib()

    // Socket Connection URL
    private let url = URL(string: "ws://[도메인]/[api]/websocket")!

클라이언트로 사용하고자 하는 클래스 내에 client instance 와 url을 선언합니다.
저는 viewModel로 사용하는 ObservableObject 내에 선언했습니다.

‼️ 이때 http는 ws로, https는 wss로 변경하고 api 뒤에 websocket 를 추가해야 합니다.

    ex) private let url = URL(string: "ws://[Server Address]/stomp/chat/websocket")!

다음으로 클래스 내에 Connection, Subscribe, Publish, Disconnect를 위한 함수를 선언합니다.
위의 4가지 함수들은 실제 동작을 한다고 하기 보단, Delegate 내에 Callback 함수를 호출하기 위한 함수로써 동작들을 명시적으로 실행할 수 있습니다.

- Connection

    // Socket Connection
    func registerSockect() {
        socketClient.openSocketWithURLRequest(
            request: NSURLRequest(url: url),
            delegate: self,
            connectionHeaders: [ "X-AUTH-TOKEN" : accessToken ]
        )
    }

헤더를 작성할 필요가 없다면 connectionHeaders를 지우면 됩니다.

- Subscribe

    func subscribe() {
    	socketClient.subscribe(destination: "[subscribe prefix]/[Destination]")
    }

Stomp 클라이언트가 특정 Topic (Destination)을 구독하는 함수 입니다.

ex) socketClient.subscribe(destination: "/sub/chat/room/"  + chatId)

여기서는 sub가 subscribe prefix이고, 나머지 부분이 Destination입니다.

- Publish (Send)

    // Publish Message
    func sendMessage() {
        var payloadObject : [String : Any] = [ Key 1 : Value 1 , ... , Key N, Value N ]
    
    	socketClient.sendJSONForDict(
                        dict: payloadObject as AnyObject,
                        toDestination: "[publish prefix]/[publish url]")
    }

JSON 형식을 보내기 위해 Dictionary 형을 사용했고, sendJSONForDict 함수를 사용했습니다.

ex) toDestination: "/pub/chat/message"

여기서는 pub가 publish prefix이고, 나머지 부분이 publish url 입니다.

함수가 실행되면 해당 토픽을 구독중인 클라이언트들이 메시지를 받을 수 있습니다. 이 메시지를 받는 부분은 잠시 뒤 기술하겠습니다.

- Disconnect

    // Unsubscribe
    func disconnect() {
        socketClient.disconnect()
    }

Stomp Client Delegate 구현

위 함수들을 구현하는 것으로 끝이 아니라, StompClientLibDelegate 프로토콜을 conform하는 콜백 함수들의 동작을 작성해야 합니다!

코드 가독성을 위해 viewModel을 extension 했습니다.

extension [Class Name] : StompClientLibDelegate {
    func stompClient(
            client: StompClientLib!,
            didReceiveMessageWithJSONBody jsonBody: AnyObject?,
            akaStringBody stringBody: String?,
            withHeader header: [String : String]?,
            withDestination destination: String
        ) { ... }
    
    func stompClientJSONBody(
            client: StompClientLib!,
            didReceiveMessageWithJSONBody jsonBody: String?,
            withHeader header: [String : String]?,
            withDestination destination: String
        ) {  ... }
    
    // Unsubscribe Topic
    func stompClientDidDisconnect(client: StompClientLib!) {
        print("Stomp socket is disconnected")
    }
    
    // Subscribe Topic after Connection
    func stompClientDidConnect(client: StompClientLib!) {
        print("Stomp socket is connected")
    
        subscribe()
    }
    
    // Error - disconnect and reconnect socket
    func serverDidSendError(client: StompClientLib!, withErrorMessage description: String, detailedErrorMessage message: String?) {
        print("Error send : " + description)
        
        socketClient.disconnect()
        registerSockect()
    }
    
    func serverDidSendPing() {
        print("Server ping")
    }
    
    func serverDidSendReceipt(client: StompClientLib!, withReceiptId receiptId: String) {
        print("Receipt : \(receiptId)")
    }
}

실제 동작에 따라 구현이 필요한 부분은 2군데입니다.

- stompClient ❗️

func stompClient(
            client: StompClientLib!,
            didReceiveMessageWithJSONBody jsonBody: AnyObject?,
            akaStringBody stringBody: String?,
            withHeader header: [String : String]?,
            withDestination destination: String
        ) {  ...  }

가장 중요한 부분이라고 생각되는 함수입니다.
구독중인 토픽에서 메시지가 Publish되면 실행되는 함수이고, jsonBody 변수는 실제로 전달 받은 데이터를 담고 있는 변수입니다.
실제로 메시지를 받거나 보낼때, 메시지 배열에 전달받은 jsonBody를 파싱해서 Append하는 방식으로 메시지를 저장했습니다.

예시)
guard let JSON = jsonBody as? [String : AnyObject] else { return }

guard let innerJSON_Message = JSON ["message"] else {return}
guard let innerJSON_Member = JSON ["member"] else {return}

let newMsg = Message(
    member:
        Sender(
            memberId: innerJSON_Member["memberId"] as? Int ?? -1,
            username: innerJSON_Member["username"] as? String ?? "",
            description: innerJSON_Member["description"] as? String ?? "",
            profileImage: innerJSON_Member["profileImage"] as? String ?? ""
        ),
    message :
        MessageContents(
            messageId: lastMessageId + 1,
            message: innerJSON_Message["message"] as? String ?? "",
            image: innerJSON_Message["image"] as? String ?? "",
            createdAt: "\(Date(timeIntervalSinceNow: 32400))"
        )
)
lastMessageId += 1
MessageList.append(newMsg)

- stompClientDidConnect

    // Subscribe Topic after Connection
    func stompClientDidConnect(client: StompClientLib!) {
        print("Stomp socket is connected")
    
        subscribe()
    }

register 함수가 실행되어 socket이 연결이되면 실행되는 함수입니다.
socket 연결과 동시에 토픽을 subscribe 하기위해 이 함수에서 subscribe 함수를 실행하도록 했습니다.
해당 부분을 지워 socket 연결과 subscribe 를 분리할 수 있습니다.


마무리

Stomp 라는 것을 완전 처음 접해서 해당 개념을 알아가는 과정도 시간이 많이 필요했고, iOS에서 Stomp를 구현한 자료들이 매우 부족해서 connection부터 subscribe, publish 등 모두 하나하나 뜯어보며 구현하여 어려움을 많이 겪었습니다. 혹시라도 동일한 어려움을 겪는 분들이 계시다면 제가 작성한 글이 조금이나마 도움이 되었으면 합니다!

제가 구현해본 채팅의 viewModel 코드입니다.
➡️ https://github.com/ParkGyurim99/Bridge-iOS/blob/main/Bridge-iOS/ViewModels/ChatRoomViewModel.swift

틀린 정보 또는 궁금한 점이 있다면 댓글 부탁드립니다! 읽어주셔서 감사합니다‼️

1개의 댓글

comment-user-thumbnail
2023년 12월 13일

혹시 전체코드좀 볼수있나요?

답글 달기