-
Swift SocketIO를 활용한 실시간 채팅 앱 구현iOS/Swift 2022. 1. 15. 21:00
안녕하세요!! 이번엔 SocketIO를 이용해서 실시간 채팅을 구현하는 방법을 정리했습니다.
이번 글도 노션에서 옮겨와 말이 짧습니다!! 양해해주세용😽
평소에 메세지 앱을 한번 구현해보고 싶었지만, 뭔가 서버에 연결하는 과정이 필요할 것 같아서 미루고 있다가
SeSAC 과정을 통해서 구현해 볼 수 있는 기회가 생겼다!!🤩
먼저 UI구성에 대해서 생각해본 것은
테이블 뷰가 있고, 내 메세지를 표시할 셀과 상대방들의 메세지를 표시할 셀 두가지가 필요하다고 생각했다.
그리고 메세지를 보여줄 테이블 뷰 아래에 TextField를 둬서 사용자가 원하는 메세지를 보낼 수 있도록 처리했다.
UI는 모두 코드로 작성했고, Snapkit 라이브러리를 사용해서 Constraints를 잡아줬다. Http 통신을 위한 라이브러리로는 Alamofire를 사용했고, 소켓 통신은 SocketIO 라이브러리를 사용했다.
SocketIO?
웹 소켓을 쉽게 사용할 수 있게 해주는 라이브러리
주요 기능(메서드)
- connect
- 설정한 URL과 포트로 연결 시도
- disconnect
- 연결된 소켓과의 연결 종료
- emit
- socket.emit(”AnyEvent”, [”Data1”, “Data2”])
- “AnyEvent”라는 이름으로 DataArray 전송
- on
- socket.on(”AnyEvent”)
- “AnyEvent”라는 이름으로 된 이벤트를 수신
- 이벤트를 수신하기 위한 리스너
- 이외에도 connect 됐을 때, disconnect 됐을 때의 이벤트도 수신 가능하다.
구현
먼저, 소켓을 관리할 클래스를 싱글톤 패턴으로 만들었다.
class SocketIOManager: NSObject { static let shared = SocketIOManager() let token = "{YOUR TOKEN}" //서버와 메세지를 주고받을 클래스, 소켓을 연결하고 해제하는 기능 등 메인 기능 탑재 var manager: SocketManager! //클라이언트 소켓 var socket: SocketIOClient override init() { self.manager = SocketManager(socketURL: URL(string: "<http://test.monocoding.com:1233>")!, config: [ .log(true), .compress, .extraHeaders(["auth":token]) ]) self.socket = manager.defaultSocket // "/"로 된 룸 super.init() // 소켓 연결될 때 실행 socket.on(clientEvent: .connect) { data, ack in print("Socket Connected", data, ack) } // 소켓 해제될 때 실행 socket.on(clientEvent: .disconnect) { data, ack in print("Socket Disconnected", data, ack) } // 소켓 채팅 듣는 메서드, sesac 이벤트로 날아온 데이터를 수신 // 데이터 수신 -> 디코딩 -> 모델 추가 -> 갱신 socket.on("sesac") { dataArray, ack in let data = dataArray[0] as! NSDictionary let chat = data["text"] as! String let name = data["name"] as! String let date = data["createdAt"] as! String let id = data["id"] as! String print("SESAC RECEIVED \\(chat), \\(name), \\(date)") NotificationCenter.default.post(name: NSNotification.Name("getMessage"), object: self, userInfo: ["chat":chat, "name":name, "createdAt":date, "id": id]) } } func establishConnection() { socket.connect() } func closeConnection() { socket.disconnect() } }
초기화 구문에서 manager를 정의할 때, SocketManager의 config 매개변수를 통해 옵션을 조절할 수 있다.
.log는 소켓 통신 중에 로그를 표시할 것인지 안할 것인지,
.compress는 데이터를 압축해서 전송할 것인지,
그리고 .extraHeaders에서는 헤더를 포함해서 보낼 것인지 설정할 수 있다.
소켓은 defaults값을 줬는데, forNamespace라는 매개변수에 값을 넣어 원하는 룸을 넣어줄 수 있다.
소켓은 룸 단위로 구분할 수 있는데, 클라이언트가 /AnyRoomName이라는 룸에 속한 소켓이라면, 서버에서도 /AnyRoomName 이라는 룸으로 설정해주어야 서로 통신이 가능하다.
앱에서 소켓 통신의 사이클을 나눠보자면
- 데이터를 서버로부터 받음
- 받은 데이터를 원하는 형태로 디코딩
- 디코딩 한 데이터를 뷰에 보여줄 모델에 추가
- 뷰 갱신
의 네 단계로 나눌 수 있다.
socket.on(”sesac”) 이라는 메서드를 통해 “sesac”이라는 이벤트를 수신할 수 있고, 서버로부터 데이터를 받을 수 있다.
받은 데이터를 원하는 형태로 변환한 뒤에 NotificationCenter를 통해 전달해 줄 수 있다.(다른 방법으로 전달해도 상관은 없다)
이 과정을 거치고 나면 2번까지는 끝났다고 생각하면 된다.
이제 뷰모델 혹은 뷰 컨트롤러로 이동해서 코드를 작성한다.
이번 앱에도 MVVM을 적용해볼까 했지만 일단은 뷰컨트롤러에 갱신 관련 코드를 작성해두었다.
override func viewDidLoad() { super.viewDidLoad() tableViewConfig() navBarConfig() NotificationCenter.default.addObserver(self, selector: #selector(getMessage(notification:)), name: NSNotification.Name("getMessage"), object: nil) requestChats() bind() self.title = "새싹채팅" }
서버에서 “sesac” 이벤트로 데이터를 전송할 때 마다 데이터를 NotificationCenter로 post하기때문에 데이터를 받기위해서 viewDidLoad에 Observer를 추가해주었다.
@objc func getMessage(notification: NSNotification) { let chat = notification.userInfo!["chat"] as! String let name = notification.userInfo!["name"] as! String let date = notification.userInfo!["createdAt"] as! String let id = notification.userInfo!["id"] as! String let value = ChatElement(id: id, text: chat, userID: "", name: name, username: "yeon", createdAt: date, updatedAt: "", v: 0, chatID: "") self.list.append(value) self.mainView.tableView.reloadData() self.mainView.tableView.scrollToRow(at: IndexPath(row: self.list.count - 1, section: 0), at: .bottom, animated: false) }
옵저버가 데이터를 받을 때 마다 이 함수가 실행되게 된다. 받아온 데이터를 디코딩하고 모델에 넣어준 뒤, reloadData로 테이블 뷰를 갱신한다. 그리고 가장 최신 데이터를 보여주기 위해 테이블뷰의 바닥으로 스크롤이 된다.
func requestChats() { let header: HTTPHeaders = [ "Authorization" : "Bearer \\(token)", "Accept" : "application/json" ] AF.request(url, method: .get, headers: header).responseDecodable(of: Chat.self) { response in switch response.result { case .success(let value): SocketIOManager.shared.establishConnection() self.list = value self.mainView.tableView.reloadData() self.mainView.tableView.scrollToRow(at: IndexPath(row: self.list.count - 1, section: 0), at: .bottom, animated: false) case .failure(let error): print(error) } } }
위 코드는 Alamofire로 서버 DB에 저장되어있는 채팅을 불러오는 코드다.
통신에 성공했을 때 responseDecodable 메서드를 통해 JSON을 원하는 형태로 디코딩해서 가져올 수 있게 되었다.
그리고 SocketIOManager에 정의해둔 establishConnection 메서드로 소켓 통신을 시작했다.
여기서 문제점은, 이 코드로는 사용자가 과거의 메세지를 보고 있을 때에도 새로운 메시지가 올 때마다 바닥까지 스크롤 된다는 점이다.
해결방법이 잘 생각나지는 않는데,
사용자가 보고있는 테이블 뷰 컨텐츠 오프셋의 높이와 테이블 뷰 컨텐츠 사이즈의 높이값을 비교해보고 비교한 값을 통해서
사용자가 테이블 뷰의 바닥에 있다고 판단하면 스크롤을 자동으로 내려주고,
바닥에 있지 않다고 하면 그냥 토스트 메세지 같은걸로 새로운 메세지가 도착했다고 알려주기만 하면 어떻게 될 것 같긴 하다.
func postChat(text: String) { let header: HTTPHeaders = [ "Authorization" : "Bearer \\(token)", "Accept" : "application/json" ] AF.request(url, method: .post, parameters: ["text":"\\(text)"], encoder: JSONParameterEncoder.default, headers: header) .responseString { response in switch response.result { case .success(let value): print(value) case .failure(let error): print(error) } } }
이 함수는 클라이언트에서 서버로 데이터를 전송하는 코드다.
서버에 데이터를 emit 해주는 방법도 있지만,
그렇게 코드를 작성하면 서버 DB에 데이터가 저장이 되지않아 지난 기록을 봐야하는 채팅앱에는 어울리지않는다.
post로 요청을 보내게 되면 서버 DB에 데이터가 저장되고 서버가 데이터를 emit해주는 구조로 작성했다.
이렇게 소켓 통신의 한 사이클이 끝나게 된다.
'iOS > Swift' 카테고리의 다른 글
Realm-cocoa 라이브러리 활용해보기(Swift) (0) 2022.01.22 TableViewCell 내부의 UIView에서 Gesture로 이벤트 처리하기 (0) 2022.01.22 Swift Localization 처리하기(로컬라이징, 현지화) (0) 2022.01.14 Swift 위치정보를 받아서 행정구역 단위로 나타내기(역 지오코딩, reverse geocoding) (0) 2022.01.13 Delegate 패턴을 이용하여 TableViewCell 내부의 버튼 이벤트 처리하기 (0) 2022.01.13 - connect