Skip to content

πŸ’¬ WeSpace - 같은 관심사λ₯Ό κ³΅μœ ν•˜λŠ” μ‚¬μš©μž κ°„ μ†Œν†΅ν•  수 μžˆλŠ” λ©”μ‹ μ € μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜

Notifications You must be signed in to change notification settings

ji-yeon224/WeSpace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ’¬ WeSpace

미리보기

πŸ—“οΈ ν”„λ‘œμ νŠΈ

  • 개인 ν”„λ‘œμ νŠΈ
  • 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을 톡해 ν•˜λ‚˜μ˜ μ„Ήμ…˜ λ‚΄μ—μ„œ 계측적 ꡬ쑰λ₯Ό κ°€μ§ˆ 수 μžˆλ„λ‘ ν•˜μ—¬ μ„Ήμ…˜ 별 토글이 κ°€λŠ₯ν•˜λ„λ‘ κ΅¬ν˜„ν•˜μ˜€λ‹€.
  • μ›Œν¬μŠ€νŽ˜μ΄μŠ€ κ΄€λ¦¬μž 여뢀에 따라 νŽΈμ§‘ 및 μ‚­μ œ κΆŒν•œμ΄ 주어진닀.
  • μ›Œν¬μŠ€νŽ˜μ΄μŠ€λ₯Ό λ‚˜κ°ˆ λ•Œ μ›Œν¬μŠ€νŽ˜μ΄μŠ€ κ΄€λ¦¬μž 여뢀와 채널 κ΄€λ¦¬μž μ—¬λΆ€λ₯Ό ν™•μΈν•œ ν›„ 퇴μž₯ λ‘œμ§μ„ μˆ˜ν–‰ν•œλ‹€.

βœ”οΈ 채널, DM

  • Realm을 μ‚¬μš©ν•˜μ—¬ κ³Όκ±° μ±„νŒ… λ‚΄μ—­κ³Ό 이미지 νŒŒμΌμ„ λ‘œμ»¬μ— μ €μž₯ν•˜μ—¬ λΉ„νš¨μœ¨μ μΈ λ„€νŠΈμ›Œν¬ 톡신 수λ₯Ό μ€„μ˜€λ‹€.
  • DB에 λ§ˆμ§€λ§‰μœΌλ‘œ μ €μž₯된 λ‚ μ§œ 데이터λ₯Ό Cursor κ°’μœΌλ‘œ μ„œλ²„μ— μš”μ²­ν•˜μ—¬ 읽지 μ•Šμ€ 메세지λ₯Ό μ„œλ²„μ—κ²Œ μ‘λ‹΅λ°›λŠ”λ‹€.
  • μ±„νŒ… λ°© μ§„μž… ν›„ SocketIOλ₯Ό 톡해 μ†ŒμΌ“ 연결을 ν•˜μ—¬ μ‹€μ‹œκ°„ μ±„νŒ… κΈ°λŠ₯을 κ΅¬ν˜„ν•˜μ˜€κ³ , ν•΄λ‹Ή 화면을 λ‚˜κ°€κ±°λ‚˜ μ•± μ’…λ£Œ λ˜λŠ” λ°±κ·ΈλΌμš΄λ“œ λͺ¨λ“œλ‘œ μ „ν™˜ 될 경우 μ†ŒμΌ“ 연결을 ν•΄μ œν•˜μ—¬ λΆˆν•„μš”ν•œ μ„œλ²„ ν˜ΈμΆœμ„ λ°©μ§€ν•˜μ˜€λ‹€.

βœ”οΈ Push Notification

  • Firebase Cloud Messaging μ„œλΉ„μŠ€λ₯Ό ν™œμš©ν•˜μ—¬ μ‹€μ‹œκ°„μœΌλ‘œ μ±„νŒ… Push Notification을 μˆ˜μ‹ ν•  수 μžˆλ‹€.
  • ν‘Έμ‹œ μ•Œλ¦Ό νƒ­ μ‹œ μˆ˜μ‹ λ°›μ€ 데이터λ₯Ό λ””μ½”λ”©ν•˜μ—¬ ν•΄λ‹Ή μ±„νŒ…λ°©μœΌλ‘œ 화면을 μ΄λ™ν•˜λ„λ‘ κ΅¬ν˜„ν•˜μ˜€λ‹€.

βœ”οΈ ν”„λ‘œν•„ 및 μΈμ•±κ²°μ œ

  • ν”„λ‘œν•„ 사진, λ‹‰λ„€μž„, μ—°λ½μ²˜λ₯Ό μˆ˜μ •ν•  수 μžˆλ‹€.
  • ν¬νŠΈμ›μ„ μ›Ήλ·° 기반으둜 μ—°λ™ν•˜μ—¬ 인앱 결제λ₯Ό κ΅¬ν˜„ν•˜μ˜€λ‹€. 결제 μ˜μˆ˜μ¦μ„ 톡해 μ„œλ²„μ— μœ νš¨μ„± 체크 ν›„ κ΅¬λ§€ν•œ 코인을 λ°˜μ˜ν•œλ‹€.

🚨 νŠΈλŸ¬λΈ” μŠˆνŒ…

βœ”οΈ DB 쑰회 및 λ„€νŠΈμ›Œν¬ 톡신 비동기 이슈

  • μ±„νŒ… λͺ©λ‘ 및 μ•ˆμ½μ€ 메세지 개수 데이터 μš”μ²­μ„ μœ„ν•΄ 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에 λŒ€ν•΄ 더 κ³΅λΆ€ν•˜μ—¬ 이후에 비동기 μž‘μ—…μ„ μˆ˜μ›”ν•˜κ²Œ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ ν•΄μ•Όκ² λ‹€.

About

πŸ’¬ WeSpace - 같은 관심사λ₯Ό κ³΅μœ ν•˜λŠ” μ‚¬μš©μž κ°„ μ†Œν†΅ν•  수 μžˆλŠ” λ©”μ‹ μ € μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages