

- κ°μΈ νλ‘μ νΈ
- 2024.01.02 ~ 2024.02.19 (7μ£Ό)
- μ΅μ μ§μ λ²μ iOS 16.0
- κ°μ κ΄μ¬μ¬λ₯Ό 곡μ νλ μ¬μ©μ κ° μν΅ν μ μλ λ©μ μ μ΄ν리μΌμ΄μ
ReactorKit
RxSwift
,RxCocoa
,RxDataSource
,RxGesture
Firebase Cloud Messaging
,iamPort
,KakaoOpenSDK
SoketIO
,Moya
,Kingfisher
,Realm
,Codable
WebKit
,UIKit
,SnapKit
,Then
,AutoLayout
DiffableDataSource
,CompostionalLayout
AnyFormatKit
,SideMenu
,IQKeyboardManager
,Toast
- μ ν λ‘κ·ΈμΈ, μΉ΄μΉ΄μ€ λ‘κ·ΈμΈμΌλ‘ μμ λ‘κ·ΈμΈ κΈ°λ₯ μΆκ°
ReactorKit
μ νμ©νμ¬ λ¨λ°©ν₯ νλ¦μΌλ‘ μ½λ ꡬ쑰νSocket.IO
λ₯Ό ν΅ν΄ μ€μκ° μ±ν κΈ°λ₯ ꡬνFirebase Cloud Messaging
μλΉμ€λ₯Ό ν΅ν΄ μλ²λ‘λΆν° μ±λ λλ λμ μ±ν λ°© μλ¦Ό μ€μκ° μμPG
μ°λμ ν΅ν κ²°μ μλΉμ€ ꡬν

- μ ν λ‘κ·ΈμΈ λ° μΉ΄μΉ΄μ€ λ‘κ·ΈμΈμΌλ‘ μμ λ‘κ·ΈμΈμ μ¬μ©ν μ μλ€.
- μ΄λ©μΌ νμκ°μ μ μ λ ₯ κ°μ λν μ ν¨μ± λ° μ‘°κ±΄ κ²μ¦ λ‘μ§μ ν΅κ³Όν ν νμκ°μ μ μλ£νλ€.

- λ©μΈ νλ©΄μμ μ¬μ©μμ μ±λ μ±ν λͺ©λ‘κ³Ό Dm λͺ©λ‘μ μ‘°ννλ€.
NSDiffableDataSourceSectionSnapshot
μ ν΅ν΄ νλμ μΉμ λ΄μμ κ³μΈ΅μ ꡬ쑰λ₯Ό κ°μ§ μ μλλ‘ νμ¬ μΉμ λ³ ν κΈμ΄ κ°λ₯νλλ‘ κ΅¬ννμλ€.- μν¬μ€νμ΄μ€ κ΄λ¦¬μ μ¬λΆμ λ°λΌ νΈμ§ λ° μμ κΆνμ΄ μ£Όμ΄μ§λ€.
- μν¬μ€νμ΄μ€λ₯Ό λκ° λ μν¬μ€νμ΄μ€ κ΄λ¦¬μ μ¬λΆμ μ±λ κ΄λ¦¬μ μ¬λΆλ₯Ό νμΈν ν ν΄μ₯ λ‘μ§μ μννλ€.

Realm
μ μ¬μ©νμ¬ κ³Όκ±° μ±ν λ΄μκ³Ό μ΄λ―Έμ§ νμΌμ λ‘컬μ μ μ₯νμ¬ λΉν¨μ¨μ μΈ λ€νΈμν¬ ν΅μ μλ₯Ό μ€μλ€.- DBμ λ§μ§λ§μΌλ‘ μ μ₯λ λ μ§ λ°μ΄ν°λ₯Ό Cursor κ°μΌλ‘ μλ²μ μμ²νμ¬ μ½μ§ μμ λ©μΈμ§λ₯Ό μλ²μκ² μλ΅λ°λλ€.
- μ±ν
λ°© μ§μ
ν
SocketIO
λ₯Ό ν΅ν΄ μμΌ μ°κ²°μ νμ¬ μ€μκ° μ±ν κΈ°λ₯μ ꡬννμκ³ , ν΄λΉ νλ©΄μ λκ°κ±°λ μ± μ’ λ£ λλ λ°±κ·ΈλΌμ΄λ λͺ¨λλ‘ μ ν λ κ²½μ° μμΌ μ°κ²°μ ν΄μ νμ¬ λΆνμν μλ² νΈμΆμ λ°©μ§νμλ€.



Firebase Cloud Messaging
μλΉμ€λ₯Ό νμ©νμ¬ μ€μκ°μΌλ‘ μ±ν Push Notificationμ μμ ν μ μλ€.- νΈμ μλ¦Ό ν μ μμ λ°μ λ°μ΄ν°λ₯Ό λμ½λ©νμ¬ ν΄λΉ μ±ν λ°©μΌλ‘ νλ©΄μ μ΄λνλλ‘ κ΅¬ννμλ€.

- νλ‘ν μ¬μ§, λλ€μ, μ°λ½μ²λ₯Ό μμ ν μ μλ€.
- ν¬νΈμμ μΉλ·° κΈ°λ°μΌλ‘ μ°λνμ¬ μΈμ± κ²°μ λ₯Ό ꡬννμλ€. κ²°μ μμμ¦μ ν΅ν΄ μλ²μ μ ν¨μ± μ²΄ν¬ ν ꡬ맀ν μ½μΈμ λ°μνλ€.
- μ±ν λͺ©λ‘ λ° μμ½μ λ©μΈμ§ κ°μ λ°μ΄ν° μμ²μ μν΄ DB μ κ·Όκ³Ό μ¬λ¬ λ²μ μλ² ν΅μ μ ꡬννλ©΄μ λͺ¨λ μμ μ΄ μλ£λκΈ° μ μ λ°μ΄ν°κ° 리ν΄λμ΄ μμ½μ κ°μ λ°μ΄ν°κ° λλ½λ μ±λ‘ λ·°μ μ±λ λͺ©λ‘μ΄ λ³΄μ¬μ§λ λ¬Έμ κ° λ°μ
- μ¬λ¬ λ²μ μλ² ν΅μ μΌλ‘ μ¬λ¬ λ²μ λΉλκΈ° μμ μ΄ μνλμ΄ μμ μλ£ μμ μ 체ν¬νμ§ λͺ»νμ¬ λ°μ
DispatchGroup
μ ν΅ν΄ μ¬λ¬ κ°μ λΉλκΈ° μμ μ κ·Έλ£Ήννκ³ , λͺ¨λ μμ μ΄ μλ£λμμμ notifyλ‘ μ²΄ν¬νμ¬ λ°μ΄ν°λ₯Ό ν λ²μ 리ν΄νλλ‘ κ΅¬ν
// HomeReactor.swift
private func requestUnreadCnt(data: [(Channel, String?)]) -> Observable<[Channel]> {
return Observable.create { observer in
let group = DispatchGroup()
var channelItems: [Channel] = []
data.forEach {
var channel = $0.0
let last = $0.1
group.enter()
DispatchQueue.main.async {
self.reqeustUnreadChannel(wsId: channel.workspaceID, name: channel.name, after: last) { unreadCount in
channel.unread = unreadCount ?? 0
channelItems.append(channel)
group.leave()
}
}
}
group.notify(queue: DispatchQueue.main) {
observer.onNext(channelItems)
observer.onCompleted()
}
return Disposables.create()
}
}
// HomeReactor.swift
private func reqeustUnreadChannel(wsId: Int, name: String, after: String?, completion: @escaping ((Int?) -> Void)) {
ChannelsAPIManager.shared.request(api: .unreads(wsId: wsId, name: name, after: after ?? nil), responseType: UnreadChannelCntResDTO.self)
.asObservable()
.subscribe(with: self) { owner, result in
switch result {
case .success(let response):
cnt = response?.count ?? 0
completion(cnt)
case .failure(let error):
completion(nil)
}
}
.disposed(by: disposeBag)
}
- λ€νΈμν¬ ν΅μ μλ₯Ό μ€μ΄κΈ° μν΄ μ΄λ―Έμ§λ₯Ό λ‘컬μ μ μ₯νλλ‘ κ΅¬ννμλλ°, μ¬λ¬ μ₯μ μ΄λ―Έμ§λ₯Ό λ‘컬μ μ μ₯ν ν μ΄λ―Έμ§ μ΄λ¦μ DBμ μ μ₯νλ €κ³ ν λ, μ΄λ―Έμ§ λ€μ΄λ‘λ κ³Όμ μ΄ λΉλκΈ°λ‘ λμνλ©΄μ μ΄λ―Έμ§ μ΄λ¦μ΄ DBμ μ μ₯λμ§ μλ λ¬Έμ κ° λ°μ
- λͺ¨λ μ΄λ―Έμ§ μ μ₯μ μλ£ν μμ μ κΈ°λ€λ¦° ν DBμ μ μ₯νμ¬ λ·°μ μ΄λ―Έμ§λ₯Ό λμΈ λ λ‘컬μμ μ κ·Όνλλ‘ νλ κ²μ λ°μ΄ν°κ° λ§μ μλ‘ μλκ° λλ €μ§ μ μλ€λ λ¬Έμ μ‘΄μ¬
- λλ¬Έμ μ μ₯ μ μ μ ν΄λ κ·μΉμ λ§λ μ΄λ¦μ 미리 μμ±νμ¬ μ μ₯νκ³ , μ΄λ―Έμ§ μ μ₯μ λΉλκΈ°λ‘ λμνλλ‘ νμ¬ μ¬μ©μμκ² λ³΄μ¬μ€ λ λ‘컬μ μ΄λ―Έμ§κ° μμΌλ©΄ λ‘컬μμ, μλ€λ©΄ Kingfisherλ₯Ό μ¬μ©νμ¬ λ€μ΄λ‘λνλ λ°©μμΌλ‘ ꡬν
private func saveChatItems(wsId: Int, data: ChannelDTO, chat: [ChannelMessage]) -> [ChannelMessage] {
let recordList = chat.map {
let urls: [String] = $0.files.map { url in
// μ΄λ―Έμ§ μ΄λ¦ λ¨Όμ μ μ₯
ImageFileService.getFileName(type: .channel(wsId: wsId, channelId: data.channelId), fileURL: url)
}
saveImage(id: wsId, channelId: $0.channelID, files: $0.files, chatId: $0.chatID, fileNames: urls)
let record = $0.toRecord()
record.setImgUrls(urls: urls)
return record
}
do {
try channelRepository.updateChatItems(data: data, chat: recordList)
debugPrint("[SAVE CHAT ITEMS SUCCESS]")
return recordList.map { $0.toDomain() }
} catch {
print(error.localizedDescription)
return chat
}
}
- μ€μκ°μΌλ‘ μ±ν μ μννκΈ° μν΄ μ±ν νλ©΄μ λ€μ΄κ°μλ μμ μλ μμΌμ μ°κ²°νκ³ , νλ©΄μ λκ° κ²½μ°μλ μ°κ²°μ μ’ λ£ν΄μΌ νκΈ° λλ¬Έμ viewWillAppear()μ viewDidDisappear()μμ μ μ°κ²°κ³Ό ν΄μ λ₯Ό νμμ§λ§, μ±ν νλ©΄μμ μ±μ λκ°κ² λμμ κ²½μ°μλ μμΌμ΄ μ’ λ£λμ§ μλ λ¬Έμ κ° λ°μ
- SceneDelegateμμ sceneDidDisconnect()μ sceneDidEnterBackground() μμ μ μμΌ μ°κ²°μ ν΄μ νκ³ , λ°±κ·ΈλΌμ΄λ λͺ¨λμ μλ€κ° λ€μ μ±ν νλ©΄μΌλ‘ λμμ¬ κ²½μ° μ¬μ°κ²°μ μν΄ sceneDidBecomeActive() μμ μ μμΌμ λ€μ μ°κ²°νλλ‘ κ΅¬ν
// SceneDelegate.swift
func sceneDidDisconnect(_ scene: UIScene) {
if SocketNetworkManager.shared.isConnected {
SocketNetworkManager.shared.disconnect()
}
}
func sceneDidBecomeActive(_ scene: UIScene) {
// μ€λ¨λκ² μλ€λ©΄ λ€μ μ°κ²°
SocketNetworkManager.shared.reconnect()
}
func sceneDidEnterBackground(_ scene: UIScene) {
// μ°κ²°λκ² μλ€λ©΄ μ€λ¨
SocketNetworkManager.shared.pauseConnect()
}
// SocketNetworkManager.swift
var flag: Bool = false // true -> μ μ μ€λ¨
func pauseConnect() {
if isConnected && !flag {
disconnect()
flag = true
}
}
func reconnect() {
if flag {
connect()
flag = false
}
}
- λ°±κ·ΈλΌμ΄λ λͺ¨λ μ μμΌ μ°κ²°μ ν΄μ ν λ λ€μ ν¬κ·ΈλΌμ΄λλ‘ λμμ μμΌμ μ¬μ°κ²°μ ν΄μΌν κ²½μ°λ₯Ό 체ν¬νκΈ° μν΄ flagκ°μ μ€μ
- μ΄λ² νλ‘μ νΈμ ReactorKit νλ μμν¬λ₯Ό μ¬μ©ν΄ 보μλ€. μ΄μ μ MVVM + Input-Output ν¨ν΄κ³Ό λ€λ₯΄κ² λ°μ΄ν°κ° λ¨λ°©ν₯μΌλ‘λ§ νλ₯΄λλ‘ κ΅¬μ‘°νκ° λμ΄ μκΈ° λλ¬Έμ μ²μμλ ꡬ쑰λ₯Ό μ΅νκ³ μ΅μν΄μ§λλ° μκ°μ΄ μ’ κ±Έλ Έλ€. μ΅μν΄μ§κ³ λλ λ§μ λ‘μ§λ€μ΄Β μμ±λμ΄ μμ΄λ ꡬ쑰νλμ΄ μκΈ° λλ¬Έμ κ°λ μ±μ΄ λμμ§κ³ , μ μ§ λ³΄μνκΈ° μ¬μ΄ μ½λλ₯Ό μμ±ν μ μκ² λμλ€.
- 본격μ μΈ μλ²μμ μμ μΌλ‘ FCMκ³Ό μΈμ±κ²°μ ꡬνμ κ²½ννκ² λμλ€. μμν¬νΈ λΌμ΄λΈλ¬λ¦¬λ‘ ν¬νΈμ μλΉμ€λ₯Ό μ°λνκ³ , μ€ κ²°μ μ μμμ¦ μ ν¨μ± κ²μ¦ λ‘μ§ νλ‘μ°λ₯Ό κ²½νν΄λ³Ό μ μμ΄μ μ’μλ€.
- FCM μλΉμ€λ₯Ό νμ©νμ¬ μλ²λ‘λΆν° μ€μκ°μΌλ‘ μ±ν μλ¦Όμ λ°μλ³Ό μ μλλ‘ κ΅¬ννμλ€. νΈμ μλ¦Ό ν μ ν΄λΉ μ±ν λ°©μΌλ‘ μ΄λνλλ‘ κ΅¬ννλ©΄μ Coordinator ν¨ν΄ μ μ©μ νμμ±μ λλΌκ² λμλ€. μ¬λ¬ 쑰건μ λ°λΌ νλ©΄ μ΄λνλλ‘ κ΅¬ννλ©° νλ©΄ μ΄λ λ‘μ§μ΄ ViewControllerμ μμ ν μμ‘΄νκ³ μλ€λ κ²μ κΉ¨λ¬μκ³ , ViewController μΈλΆμμ νλ©΄ μ ν λ‘μ§μ ꡬννλ κ²μ΄ νμνλ€λ κ²μ λκΌλ€.
- μ΄ νλ‘μ νΈλ₯Ό νλ©΄μ κΈ°ν, μλ², λμμΈμ΄ μ 곡λκ³ , μ€λ‘μ§ iOS κ°λ°λ§ λ΄λΉνλ©΄ λλ€λ μ μμ μ€μ μ 무 νκ²½κ³Ό λΉμ·ν κ²½νμ ν΄λ³Ό μ μμλ€. μ΄ νλ‘μ νΈλ‘ λΉλκΈ° μμ μ²λ¦¬μ λν μ€μμ±μ λλΌκ² λμλ€. λ§μ λ€νΈμν¬ ν΅μ μ μ°κ²°νλ©° λΉλκΈ° μμ μ μ²λ¦¬νλ κ²μ΄ κ°μ₯ μ΄λ ΅κ² λκ»΄μ‘λ€. GCDμ Swift Concurrencyμ λν΄ λ 곡λΆνμ¬ μ΄νμ λΉλκΈ° μμ μ μμνκ² μ²λ¦¬ν μ μλλ‘ ν΄μΌκ² λ€.