티스토리 뷰
관련 PR: [#40] 소켓 통신을 통해 받은 데이터로 거래소 화면 CoinListView를 실시간 업데이트해요
개요
현재 Public API를 호출해서 불러온 데이터를 Table로 보여주는 작업은 완료가 되었어요!
이제 Web Socket을 통해 업데이트될 정보를 받아오고, 이 변경할 정보를 Cell에 반영해주면 돼요 :)
Web Socket 통신을 위한 기반 작업은 아래 PR에서 완료했어요.
[#33] WebSocket 통신을 위한 기반 작업을 해요
소켓 통신을 통해 받아올 데이터 Entity를 만들고, 소켓 통신을 수행할 소켓 매니저를 구현했어요!
업데이트될 정보 관리하기
첫 번째, 소켓 매니저가 실행되는 시점을 정하고 데이터가 각각 어떤 객체를 통해 전달되는지 설계를 할 필요가 있어요.
당연히 업데이트한 정보를 보여줄 CoinListView를 소유한 ExchangeViewController에서 소켓 매니저를 시작해야 한다고 판단했어요.
ExchangeViewController의 init 시점에 소켓 매니저를 통해 통신을 시작할 계획이에요.
여기서 조심해야 할 부분은 ExchangeViewController가 init 되는 시점에 CoinListView와 소켓 매니저 모두 초기화가 되는데, 소켓 매니저에서 데이터를 넘겨받은 시점에 CoinListView가 아무런 Cell도 보여주고 있지 않은데 갱신하려고 한다면 에러가 발생할 수 있어요.
이 부분을 확인하는 작업이 필요할 것이라고 생각했어요.
그러면 이제 소켓 매니저를 통해 받아온 정보를 받아서 처리할 객체를 정해줘야 해요.
선택지는 2개였어요.
1. ExchangeViewModel
2. CoinListViewModel
ExchangeViewController에서 보내주는 정보이기 때문에 1차적으로 ExchangeViewModel이 가져야 하나 고민했어요.
만약 이 정보를 CoinListView 외에 ExchangeViewController가 갖는 다른 요소들이 알아야 하는 정보라면 ExchangeViewModel이 갖는 것이 맞다고 생각했어요.
하지만 CoinListView 말고 이 정보를 사용할 일이 없고, 애초에 ExchangeViewModel의 역할을 설계할 때 소유한 ViewModel의 연결점 정도로 정의했었기 때문에 2번을 선택했어요.
들어온 소켓 데이터가 보이고 있는 Cell에 포함되는지 확인하기
소켓 통신을 통해 들어온 정보가 만약 화면에 출력되고 있는 코인 중 하나라면 새로운 정보로 갱신해주는 화면을 만들어야 해요.
그래서 필요한 것은 두 가지예요.
1. 기존 데이터
2. 소켓 통신을 통해 전달받은 데이터
2번은 당연히 소켓 통신을 통해 받은 데이터를 Stream으로 관리할 것이고, 1번 데이터는
let coinListCellData = PublishSubject<[CoinListViewCellData]>()
로 관리되고 있기 때문에 이 Stream을 사용하려고 했어요.
두 가지 Stream을 관리하기 때문에 CombineLatest로 구현해 비교를 진행하려고 설계했는데, 구현 중 문제점을 발견했어요.
호출한 전체 데이터를 보여주고 있는 상황에서 소켓을 통해 새로운 정보가 들어올 경우 기존 정보는 새로운 정보로 갱신될 거예요.
여기까지는 문제가 없지만, 이 상태에서 검색을 진행해 다시 전체 데이터를 불러올 경우 문제가 발생해요.
CombineLatest의 경우 두 Stream 중 하나라도 Event가 발생하면 그 두 Stream 각각의 최신 값을 방출해요.
위의 그림처럼 두 Stream을 CombineLatest로 묶어놓을 경우 새로운 전체 데이터를 검색할 때에도 소켓 데이터로 덮어쓰는 작업이 진행되다 보니 오래된 데이터로 새 데이터를 덮어쓰는 문제가 발생한다고 판단했어요.
그래서 문제를 해결하기 위해 Cell을 전체 순회하며 매칭 되는지 여부를 확인하는 로직을 구현했어요.
WebSocketDelegate
WebSocketManager를 어떤 객체에서 활용할 것인지 고민했어요 :)
WebSocket은 오로지 CoinListView의 UI 업데이트에만 활용될 계획이라 CoinListViewModel에서 채택하고 구현했어요.
extension CoinListViewModel: WebSocketDelegate {
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(let headers):
client.write(string: "이름")
self.sendSocketTickerMessage()
print("websocket is connected: \(headers)")
case .disconnected(let reason, let code):
print("websocket is disconnected: \(reason) with code: \(code)")
case .text(let text):
Observable.just(text)
.bind(to: self.socketText)
.disposed(by: self.disposeBag)
case .binary(let data):
print("Received data: \(data.count)")
case .ping(_):
break
case .pong(_):
break
case .viabilityChanged(_):
break
case .reconnectSuggested(_):
break
case .cancelled:
print("websocket is canceled")
case .error(let error):
print("websocket is error = \(error!)")
}
}
메시지를 Receive 할 경우 동작할 메서드예요.
정상적으로 메시지가 수신될 경우 text를 통해 String 값으로 넘어오기 때문에 데이터 처리를 위해 Stream으로 넘겨주는 흐름을 설계했어요.
여기서 2가지 문제가 발생했어요.
1. SocketText의 타입
SocketText는 위에서 언급한 것처럼 들어온 메시지를 처리하기 위한 Stream이에요.
처음에는 이 Stream을 PublishSubject로 선언했어요.
초기값이 필요 없으면서 Observer인 동시에 Observable이어야 하기 때문이에요.
하지만 콘솔로 관찰한 결과 아무 정보도 들어오지 않았어요.
그래서 디버그를 위해서 subscribe를 통해 모든 Event를 체크해봤어요.
그 결과, 첫 번째 이벤트에서 completed와 disposed까지 방출되며 stream 연결 자체가 종료되는 것을 확인했어요.
이 Stream의 경우 Socket 연결이 끊어지지 않는 이상 계속되어야 하기 때문에 completed에 대응하지 않는 Stream이 필요했고, Relay로 타입을 변경함으로써 문제를 해결할 수 있었어요.
2. sendMessage의 위치
위에서 보이는 코드와 다르게 처음에는 CoinListViewModel의 init 시점에 sendMessage를 했어요.
init() {
self.cellData = self.coinListCellData
.asDriver(onErrorJustReturn: [])
self.socketTickerData = self.socketText
.map {
return $0.replacingOccurrences(of: "\\", with: "").data(using: .utf8)
}.filter { $0 != nil }
.map {
return try? JSONDecoder().decode(SocketTickerResponse.self, from: $0!)
}.filter { $0 != nil }
.map {
$0!.content
}
// 여기서 sendMessage
WebSocketManager.shared.socket?.delegate = self
}
그런데 이렇게 할 경우 동작하지 않는 문제가 발생했어요!
문제를 분석한 결과, init에서 message를 send 하는 시점에 소켓이 아직 connect가 되어있지 않을 수 있다는 점이라고 생각했어요 :)
그래서 위치를 receive 메서드의 connect로 옮긴 결과, 정상적으로 message가 전송되는 것을 확인했어요.
UI 업데이트 하기
이번 PR에서 가장 많은 시간을 쏟은 기능이었어요.
고민의 포인트는 Cell을 그리기 전 사용한 데이터를 활용할 것인가, 혹은 이미 그려진 Cell을 순회하며 검색을 할 것인가였어요.
viewModel.socketTickerData
.subscribe(onNext: { socketTickerData in
let numberOfRows = self.numberOfRows(inSection: 0)
guard let tickerName = socketTickerData.ticker,
let coinListViewCellData = socketTickerData.coinListViewCellData else { return }
for row in 0..<numberOfRows {
guard let cell = self.cellForRow(at: IndexPath(row: row, section: 0)) as? CoinListViewCell else { continue }
if cell.hasSameTickerName(with: tickerName) {
cell.setData(with: coinListViewCellData)
cell.contentView.layer.do {
$0.borderColor = UIColor.bithumb.cgColor
$0.borderWidth = 3
}
} else {
cell.contentView.layer.borderColor = UIColor.white.cgColor
}
}
}).disposed(by: self.disposeBag)
결과적으로 Cell을 직접 탐색하면서 업데이트를 위해 들어온 데이터가 Cell에 매칭 되는지 확인하는 로직을 구현했어요.
더 좋은 로직이 있을지 고민이 추가적으로 필요할 것 같아요.
발생한 문제
guard let cell = self.cellForRow(at: IndexPath(row: row, section: 0)) as? CoinListViewCell else { return }
문제가 되었던 코드는 최종 코드와 다르게 위와 같았어요.
로직상 당연히 현재 View가 가지고 있는 Cell은 CoinListViewCell일 수밖에 없기 때문에 타입 캐스팅에서 문제가 발생할 이유가 없다고 판단했어요.
원하는 기능이 작동하지 않아서 Break Point를 통해 확인한 결과 반복적으로 return에 들어가며 로직이 종료되는 상황이 벌어졌어요.
추론한 결과를 간단히 설명하면, 문제가 발생한 이유는 row를 전수조사하는 과정에서 화면에 나타나지 않는 Cell은 CoinListViewCell의 형태로 존재하지 않기 때문이에요.
예를 들어, 현재 화면에 나타난 Cell은 10번째부터 20번째 요소라고 했을 때, 30번째 요소는 이미 그려져 있는 것이 아니에요. 170번째 요소가 나타날 때까지 화면이 내려가야 Cell을 deque 하며 그리게 되는데, 위의 로직에서는 화면에 보이지 않는 셀까지 전수조사를 하기 때문에 문제가 발생했다고 판단했어요.
그래서 cell이 캐스팅되지 않는 경우 return이 아닌 continue로 수정함으로써 문제를 해결할 수 있었어요.
guard let을 반복적으로 사용하다 보면 기계적으로 return을 사용하기 쉬운데, 위와 같은 문제 상황을 대비해 항상 로직의 흐름을 생각하는 것이 중요하다는 것을 배운 좋은 경험이었어요.
후기
잡다한 버그와 문제가 가장 많았던 PR이었어요.
문제를 맞닥뜨리고, 해결해나가는 과정에서 느낀 건 역시 기본이 중요하다는 것이었어요 :)
'iOS 앱개발 > 암호화폐 거래소 앱 프로젝트' 카테고리의 다른 글
[암호화폐 거래소 앱 만들기] - TableView에서 Cell을 클릭할 경우 다른 화면으로 넘어가기 (0) | 2022.01.30 |
---|---|
[암호화폐 거래소 앱 만들기] - SearchBar에 검색어를 입력해 검색 결과를 출력해주는 기능을 구현 (0) | 2022.01.26 |
[암호화폐 거래소 앱 만들기] Cell에서 사용할 Coin의 한글 이름을 서버에서 제공하지 않는 문제 해결하기 (0) | 2022.01.26 |
[암호화폐 거래소 앱 만들기] 받아온 데이터에서 원하지 않는 값 제거하기 (0) | 2022.01.26 |
[암호화폐 거래소 앱 만들기] 프로젝트 시작 및 첫 화면 설계하기 (0) | 2022.01.25 |
- Total
- Today
- Yesterday
- 나무자르기#BOJ#이분탐색#Python
- 반복수열#백준알고리즘#Python
- 종이자르기#분할정복#BOJ#Python
- 암호코드#dp#BOJ#Python
- 리모컨#완전탐색#BOJ#Python
- 텀 프로젝트#백준알고리즘#Python
- N으로 표현#DP#Programmers#Python
- Swift#Tuples#Range
- django
- 배열합치기#분할정복#BOJ#Python
- 순열사이클#BOJ#Python
- PassingCars#Codility#Python
- 토마토#백준알고리즘#Python
- Triangle#Sorting#Codility#Python
- 랜선자르기#이분탐색#BOJ#Python
- 쿼드트리#BOJ#분할정복#Python
- 섬의개수#백준알고리즘#Python
- 터틀비치#리콘#xbox#controller
- API#lazy#
- NumberofDiscIntersections#Codility#Sort#Python
- 공유기 설치#BOJ#이분탐색#Python
- filter#isalnum#lower
- 미로 탐색#백준알고리즘#Python
- 파이썬알고리즘인터뷰#4장
- 백준 알고리즘#BackTracking
- 병든 나이트#BOJ#탐욕법#Python
- Distinct#Codility#Python
- 날짜 계산#BOJ#완전탐색#Python
- Brackets#Stacks and Queues#Codility#Python
- django#slicing
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |