# Jumak Slide 16_9 - 156 (1) ### [๐Ÿ“ฑ ์•ฑ ์„ค์น˜ํ•˜๋Ÿฌ ๊ฐ€๊ธฐ](https://apps.apple.com/kr/app/%EC%A3%BC%EB%A7%89/id6470310590) > ๋‚ด ์ฃผ๋ณ€์˜ ๋ง‰๊ฑธ๋ฆฌ ์ฃผ๋ง‰ ์ฐพ๊ธฐ ๐Ÿถ, Jumak > > > v1.0.0 **๊ฐœ๋ฐœ๊ธฐ๊ฐ„: 2023.09.26 ~ 2023.10.23** > > **์ง€์†์ ์ธ ์—…๋ฐ์ดํŠธ**: 2023.10.23 ~ (์ง„ํ–‰์ค‘) # **โœจย ํ”„๋กœ์ ํŠธ ์ฃผ์š” ํ™”๋ฉด** ![แ„Œแ…ฎแ„†แ…กแ†จ แ„…แ…ฆแ„‘แ…ฉ แ„‹แ…ตแ„†แ…ตแ„Œแ…ต 002](https://github.com/kimkyuchul/Jumak/assets/25146374/17b3c9a2-82e3-4eb6-96c3-e44ba259a846) ### ์ฃผ์š” ๊ธฐ๋Šฅ - ์‚ฌ์šฉ์ž ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋ง‰๊ฑธ๋ฆฌ, ํŒŒ์ „, ๋ณด์Œˆ ์ฃผ๋ง‰ ์ฐพ๊ธฐ ๊ธฐ๋Šฅ ์ œ๊ณต - ์ฃผ๋ง‰ ์ฆ๊ฒจ์ฐพ๊ธฐ, ํ‰์  ๋“ฑ๋ก ๋ฐ ํ•ด๋‹น ์ฃผ๋ง‰์—์„œ์˜ ์—ํ”ผ์†Œ๋“œ ์ž‘์„ฑ, ์กฐํšŒ - ํ‰์ , ์ฆ๊ฒจ์ฐพ๊ธฐ, ์—ํ”ผ์†Œ๋“œ ๋“ฑ๋ก์„ ํ†ตํ•œ ์ฃผ๋ง‰ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ ๋ฐ ๋‹ค์–‘ํ•œ ํ•„ํ„ฐ๋ง # **โš™๏ธย ๊ฐœ๋ฐœํ™˜๊ฒฝ ๋ฐ ๊ธฐ์ˆ ์Šคํƒ** - Minimum Deployments: iOS 15.5 - Dependence Manager : **SPM & CocoaPod(NaverMap)** - Swift Version: 5.8.1 - `UIKit` `MVVM` `RxSwift` `RxCocoa` - `Codebase UI` `SnapKit` - `DiffableDataSource` `CompositionalLayout` `PHPickerViewController` `RxDataSources` `RxGesture` `RxKeyboard` - `CoreLocation` `NaverMap` - `Alamofire` `RxReachability` - `RealmSwift` - `Firebase Crashlytics` `Firebase Push Notifications` # **๐Ÿ”ฅ ๊ธฐ์ˆ ์  ๋„์ „** ### Clean Architecture แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2023-11-09 แ„‹แ…ฉแ„Œแ…ฅแ†ซ 2 53 48 **Why** - ์•ฝ 4์ฃผ๋ผ๋Š” ๊ธฐ๊ฐ„ ์•ˆ์— ์•ฑ์Šคํ† ์–ด ์ถœ์‹œ๋ผ๋Š” ๋ชฉํ‘œ๋ฅผ ์žก์•˜์Šต๋‹ˆ๋‹ค. ๋””์ž์ธ, ๊ธฐํš ๋“ฑ์ด ๊ฐœ๋ฐœ ์ค‘์—๋„ ์ˆ˜์ •๋˜์–ด, ์„œ๋น„์Šค์˜ ๋ฐฉํ–ฅ๊ณผ ์ŠคํŽ™, UI ๋“ฑ์ด ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋ณ€๊ฒฝ์ด ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์™ธ๋ถ€์˜ ๋ ˆ์ด์–ด๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถœ ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ ์„ค๊ณ„๋ฅผ ๊ณ ๋ฏผํ–ˆ๊ณ , Clean-Architecture์„ ์ฑ„ํƒํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. **Result** - ViewModel์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋“ค์„ UseCase๋กœ, ๋„คํŠธ์›Œํฌ๋‚˜ ์™ธ๋ถ€ ํ”„๋ ˆ์ž„์›Œํฌ์— ๋Œ€ํ•œ ์š”์ฒญ์€ Repository๋กœ ๋ถ„๋ฆฌํ•ด ๊ฐ ๋ ˆ์ด์–ด์˜ ์—ญํ• ์„ ๋ถ„๋ช…ํ•˜๊ฒŒ ๋‚˜๋ˆ„์–ด, ์ฝ”๋“œ์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ , ์˜์กด์„ฑ์ด Domain Layer๋ฅผ ํ–ฅํ•˜๋„๋ก ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. - MVVM ๊ตฌ์กฐ์—์„œ ViewModel์ด ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ํ”ผํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. - ๊ฐ๊ฐ์˜ ๋ ˆ์ด์–ด๋ฅผ ์—ญํ• ์— ๋”ฐ๋ผ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ฐฉ๋Œ€ํ•œ ์–‘์˜ ์ฝ”๋“œ๋ฅผ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. --- ### MVVM + Input Output Patten แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2023-11-09 แ„‹แ…ฉแ„Œแ…ฅแ†ซ 3 10 58 **Why** - ๋ทฐ๊ฐ€ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋Š” ์—ญํ• ๋งŒ ๋‹ด๋‹นํ•˜๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋Œ€ํ•œ ๋ถ„๋ฆฌ๋ฅผ ์œ„ํ•ด MVVM ํŒจํ„ด์„ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. - iOS MVVM์€ ํ‘œ์ค€์ด ์—†๊ณ  ๊ตฌํ˜„ํ•˜๋Š” ์‚ฌ๋žŒ๋งˆ๋‹ค ํŒจํ„ด์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. MVVM ํŒจํ„ด์„ ์ •ํ˜•ํ™”ํ•˜๊ณ , ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๋‹จ๋ฐฉํ–ฅ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด Input/Output ํŒจํ„ด์„ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. **Result** - ํ™”๋ฉด์—์„œ ์ผ์–ด๋‚˜๋Š” ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ Input์œผ๋กœ ์ •์˜ํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์š”์ฒญํ•˜๊ณ , ๊ฒฐ๊ณผ๋กœ ๊ฐฑ์‹ ๋˜๋Š” ๊ฐ’๋“ค์„ Output์— ๋ฐ”์ธ๋”ฉํ•ด ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” Ouput์„ ๋ณด๊ณ  ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋„๋ก ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. - Input/Output ํŒจํ„ด์„ ํ™œ์šฉํ•ด ์ผ๊ด€์„ฑ ์žˆ๋Š” ๊ตฌ์กฐ์˜ ๋ทฐ๋ชจ๋ธ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด, ๊ฐ€๋…์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. --- ### RxSwift **Why** - ๊ฐ ๊ฐ์ฒด์—์„œ ์—ฐ์†๋œ escaping closure์œผ๋กœ ์ธํ•œ ์—ฐ์†๋œ ์ฝœ๋ฐฑ์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ํ”ผํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. - Notification Center, GCD๋“ฑ ๋ณตํ•ฉ์ ์ด๊ณ  ๋‹ค์–‘ํ•œ ๋น„๋™๊ธฐ API๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ๋ณด๋‹จ, ์ผ๊ด€๋œ ๋น„๋™๊ธฐ ํ”„๋ ˆ์ž„์›Œํฌ ํ™œ์šฉํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. **Result** - escaping closure๊ฐ€ ์•„๋‹Œ RxSwift์˜ Operator๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ฝ”๋“œ ์–‘์ด ๊ฐ์†Œํ•˜๊ณ , ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์›Œ์กŒ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ์˜ ๋ฐฉ๋Œ€ํ•ด์ง๊ณผ ์‹ค์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. - ์˜ค๋กœ์ง€ RxSwift๋งŒ ํ™œ์šฉํ•ด ํ•˜๋‚˜์˜ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋กœ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , ๊ธฐ์กด์˜ ๋ณตํ•ฉ์ ์ธ ๋น„๋™๊ธฐ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ์˜ฌ๋ฆฌ๊ณ  ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. - RxTraits๋ฅผ ํ™œ์šฉํ•ด Thread ๊ด€๋ฆฌ๋ฅผ ์‰ฝ๊ณ  ๊ฐ„ํŽธํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. # ๐Ÿซก TroubleShooting ### 1. **๊ฒ€์ƒ‰ํ•œ ์œ„์น˜๊ฐ€ GeocodeLocation์„ ํ•  ์ˆ˜ ์—†๋Š” ์ง€์—ญ์ผ ๊ฒฝ์šฐ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜ ์ด์Šˆ** CLGeocoder์„ ํ™œ์šฉํ•œ Address String Observable์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์•Œ ์ˆ˜ ์—†๋Š” ์œ„์น˜์—์„œ ์ฃผ๋ง‰ ์žฌ ๊ฒ€์ƒ‰ ์‹œ `if let error = error` ๋กœ ๋น ์ง€๋Š” ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. CLGeocoder ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐพ์•„๋ณธ ๊ฒฐ๊ณผ ํŠน์ • ์œ„์น˜์— ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ์—๋Ÿฌ๋ฅผ ์ค€๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค. ```swift func reverseGeocodeLocation(location: CLLocation) -> Observable { let geocoder = CLGeocoder() return Observable.create { emitter in geocoder.reverseGeocodeLocation(location) { placemarks, error in if let error = error { emitter.onError(error) return } guard let placemark = placemarks?.first else { emitter.onError(error.unsafelyUnwrapped) return } let formattedAddress = self.getAddressString(from: placemark) emitter.onNext(formattedAddress) emitter.onCompleted() } return Disposables.create() } } ``` ViewModel์—์„œ ์œ„์น˜ ์žฌ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ ์„ ํƒ ์‹œ flatMap์„ ํ†ตํ•ด ์œ„์˜ reverseGeocodeLocation์„ Output์˜ currentUserAddresst์— ๋ฐ”์ธ๋”ฉ ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ํ•ด๋‹น ๊ตฌ๋ฌธ์—์„œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค. 1๋ฒˆ์ฒ˜๋Ÿผ ์ฒ˜๋ฆฌํ•  ๊ฒฝ์šฐ subscribe์˜ error๋กœ ๋–จ์–ด์ง„ ์ดํ›„ ์ŠคํŠธ๋ฆผ์ด ๋Š๊ฒจ์„œ ์œ„์น˜ ์žฌ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐฉ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค. 2๋ฒˆ์ฒ˜๋Ÿผ flatMap์˜ catchAndReturn์„ ํ†ตํ•ด Error Default๊ฐ’์„ ๋ณด๋‚ด๊ณ  ์ŠคํŠธ๋ฆผ์ด ๋Š๊ธฐ์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌํ–ˆ๋‹ค. ```swift /// 1๋ฒˆ - ์ŠคํŠธ๋ฆผ ๋Š๊น€ input.didSelectRefreshButton .withUnretained(self) .flatMapLatest { owner, location in let reverseGeocodeObservable = owner.locationUseCase.reverseGeocodeLocation(location: location.convertToCLLocation) return reverseGeocodeObservable } .subscribe(onNext: { locationAddress in output.currentUserAddress.accept(locationAddress) }) { error in output.currentUserAddress.accept("์•Œ ์ˆ˜ ์—†๋Š” ์ง€์—ญ") } .disposed(by: disposeBag) /// 2๋ฒˆ - ์ŠคํŠธ๋ฆผ์ด ์œ ์ง€ input.didSelectRefreshButton .withUnretained(self) .flatMapLatest { owner, location in let reverseGeocodeObservable = owner.locationUseCase.reverseGeocodeLocation(location: location.convertToCLLocation) .catchAndReturn("์•Œ ์ˆ˜ ์—†๋Š” ์ง€์—ญ์ž…๋‹ˆ๋‹ค.") return reverseGeocodeObservable } .bind(to: output.currentUserAddress) .disposed(by: disposeBag) ``` --- ### 2. **CollectionView ํŽ˜์ด์ง• ์‹œ Cell์˜ Index์™€ Map Annotation์ด ๋™์ผํ•œ Index๋กœ ์„ ํƒ & ๋งต ์ค‘์‹ฌ ์ขŒํ‘œ ์ด๋™ ๋กœ์ง ๊ตฌํ˜„** ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 06 37](https://github.com/kimkyuchul/Jumak/assets/25146374/66f249df-fed7-4b22-898b-c85f4a55b43b) ์œ„์˜ ์˜์ƒ๊ณผ ๊ฐ™์ด ํ•˜๋‹จ ์ฃผ๋ง‰ ์ •๋ณด CollectionView๋ฅผ ํŽ˜์ด์ง• ์‹œ Annotation๊ณผ ๋งต ์ค‘์‹ฌ ์ขŒํ‘œ๊ฐ€ ์ด๋™๋˜์–ด์•ผ ํ–ˆ๋‹ค. Compositional Layout์˜ visibleitemsinvalidationhandler์„ ํ™œ์šฉํ–ˆ๋‹ค. visibleItems.last?.indexPath.row(ํŽ˜์ด์ง• ๋  ๋•Œ ๋งˆ๋‹ค ํ˜„์žฌ ์„น์…˜ ํ™”๋ฉด์— ํ‘œ์‹œ๋œ ์•„์ดํ…œ์˜ indexPath)์„ Subject์— ๋‹ด์•„์„œ viewModel์˜ input์œผ๋กœ ํ™œ์šฉํ–ˆ๋‹ค. ๋”๋ถˆ์–ด transform ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ํŽ˜์ด์ง• ์‹œ ์…€์ด ์ปค์ง€๊ณ  ์ค„์–ด๋“œ๋Š” Carousel view ํšจ๊ณผ๋ฅผ ์ ์šฉํ–ˆ๋‹ค. ```swift section.visibleItemsInvalidationHandler = { [weak self] (visibleItems, offset, env) in visibleItems.forEach { item in let intersectedRect = item.frame.intersection(CGRect(x: offset.x, y: offset.y, width: env.container.contentSize.width, height: item.frame.height)) let percentVisible = intersectedRect.width / item.frame.width if percentVisible >= 1.0 { if let currentIndex = visibleItems.last?.indexPath.row { self?.visibleItemsRelay.accept(currentIndex) } } let scale = 0.5 + (0.5 * percentVisible) item.transform = CGAffineTransform(scaleX: 0.98, y: scale) } } ``` viewModel์—์„  ํŽ˜์ด์ง• ์‹œ visibleItems Index ๊ฐ’์„ ํ† ๋Œ€๋กœ Annotation๊ณผ ๋งต ์ค‘์‹ฌ ์ขŒํ‘œ๊ฐ€ ์ด๋™๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค. [[iOS] Compositional Layout์˜ visibleitemsinvalidationhandler ํ™œ์šฉ](https://medium.com/@kyuchul2/ios-compositional-layout%EC%9D%98-visibleitemsinvalidationhandler-%ED%99%9C%EC%9A%A9-190cde90c933) ```swift // didScrollStoreCollectionView == visibleItemsRelay input.didScrollStoreCollectionView .distinctUntilChanged() .withUnretained(self) .bind(onNext: { owner, visibleIndex in guard let index = visibleIndex else { return } let store = owner.storeList[index] output.setCameraPosition.accept((store.y, store.x)) output.selectedMarkerIndex.accept(index) }) .disposed(by: disposeBag) ``` --- ### 3. **Map์˜ Annotation ์„ ํƒ ์‹œ CollectionView ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ Annotation์„ ์„ ํƒํ•˜๊ณ  ์˜ค๋Š” ์ด์Šˆ** ![ezgif com-resize (2)](https://github.com/kimkyuchul/Jumak/assets/25146374/e52745c3-651d-497d-8cdf-ebba918b5fad) 2๋ฒˆ ์ด์Šˆ์—์„œ CollectionView ํŽ˜์ด์ง• ์‹œ visibleItems Index๋ฅผ ๋ฐฉ์ถœํ•˜์—ฌ Cell์˜ ์„ ํƒ๋œ Index์™€ Annotation์„ ๋™์ผํ•˜๊ฒŒ ์„ ํƒ๋˜๊ฒŒ ํ•˜๊ณ , ํ•ด๋‹น Index๋กœ ๋งต ์ค‘์‹ฌ ์ขŒํ‘œ๋ฅผ ์ด๋™์‹œํ‚ค๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‹ค. ๋ฐ˜๋Œ€๋กœ Map์˜ Annotation ์„ ํƒ ์‹œ ํ•ด๋‹น Annotation ์ธ๋ฑ์Šค๋กœ CollectionView๊ฐ€ ์Šคํฌ๋กค๋˜์–ด์•ผ ํ–ˆ๋Š”๋ฐ `selectItem` ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋•Œ๋ฌธ์— ๋งต ์ค‘์‹ฌ ์ขŒํ‘œ๊ฐ€ ์—ฌ๋Ÿฌ ๋งˆ์ปค๋ฅผ ๋“ค๋ ธ๋‹ค ์˜ค๋Š” ์ด์Šˆ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ๋‹ค. ```swift output.selectedMarkerIndex .distinctUntilChanged() .withLatestFrom(output.storeList) { index, storeList in return (index, storeList) } .withUnretained(self) .bind(onNext: { owner, data in let (selectedIndex, storeList) = data owner.setUpMarker(selectedIndex: selectedIndex, storeList: storeList) owner.locationView.storeCollectionView.selectItem(at: IndexPath(row: selectedIndex ?? 0, section: 0), animated: true, scrollPosition: .centeredHorizontally) }) .disposed(by: disposeBag) ``` ์Šคํฌ๋กค(ํŽ˜์ด์ง•) ์ด๋ฒคํŠธ input ์˜ต์ €๋ฒ„๋ธ”์— `debounce` ๋ฅผ ๊ฑธ์—ˆ๋‹ค. `debounce`๋Š” ์ผ์ • ์‹œ๊ฐ„๋™์•ˆ ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ๊ฐ€ ์—†์„ ๋•Œ์—๋งŒ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•˜๋ฉฐ, ์ค‘๊ฐ„์— ๋“ค์–ด์˜ค๋Š” ์ด๋ฒคํŠธ๋“ค์„ ๋ฌด์‹œํ•œ๋‹ค. ์ด๋ฅผ ํ™œ์šฉํ•˜์—ฌ `selectItem`์˜ ์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋•Œ ๋“ค์–ด์˜ค๋Š” visibleItems Index๋ฅผ ๋ฌด์‹œํ•˜๊ณ , ์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋๋‚˜๊ณ  ๋งˆ์ง€๋ง‰์— ๋“ค์–ด์˜จ visibleItems Index ๊ฐ’๋งŒ ๋ฐ›์•„์„œ ์„ ํƒํ•œ Annotation์˜ ๋งต ์ค‘์‹ฌ ์ขŒํ‘œ๋กœ ์ด๋™์‹œํ‚ฌ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ```swift locationView.visibleItemsRelay.asObservable().debounce(.milliseconds(300), scheduler: MainScheduler.asyncInstance) ``` --- ### 4. **RxReachability ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ์ง€** NaverMap์˜ ๊ฒฝ์šฐ Map์ด init๋˜๋Š” ์‹œ์ ์— ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ ๋ฌดํ•œ naver map error code -1020 ์—๋Ÿฌ๋ฅผ ๋ฐฉ์ถœ -> ์ฆ‰, Map์ด ํฌํ•จ๋œ ๋ทฐ๊ฐ€ ๊ทธ๋ ค์ง€๊ธฐ ์ „์— ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ์ง€๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค. ๋”๋ถˆ์–ด NaverMap์„ ์‚ฌ์šฉํ•˜๋Š” ์˜์นด, ์š”๊ธฐ์š” ๋“ฑ์˜ ๊ฒฝ์šฐ MapView๊ฐ€ ๊ทธ๋ ค์ง€๊ธฐ ์ „์— ๋„คํŠธ์›Œํฌ ์ฒดํฌ๋ฅผ ์ง„ํ–‰ํ•˜๋Š”๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์˜€๋‹ค. (B์— Map์ด ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๊ณ  ์น˜๋ฉด, A์—์„œ ๋„คํŠธ์›Œํฌ๋ฅผ ๊ฐ์ง€ํ•ด์„œ ๋„คํŠธ์›Œํฌ ๋ฏธ์—ฐ๊ฒฐ ์‹œ B๋กœ ์ง„์ž…ํ•˜๋Š” ๋ทฐ๋ฅผ ๋ง‰์•„๋ฒ„๋ฆผ) BaseViewController์—์„œ Reachability์„ ํ™œ์šฉํ•ด viewWillAppear ์‹œ startNotifier() viewWillDisappear ์‹œ stopNotifier() ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜๊ณ  reachability?.rx.isDisconnected ์‹œ rx.makeErrorAlert๋ฅผ ๋ฐฉ์ถœํ•˜๋„๋ก ํ–ˆ๋‹ค. ```swift import Reachability import RxReachability class BaseViewController: UIViewController, BaseViewControllerProtocol, BaseBindableProtocol { var disposeBag: DisposeBag = .init() var reachability: Reachability? override func viewDidLoad() { super.viewDidLoad() do { reachability = try Reachability() } catch { print("Reachability ์—๋Ÿฌ: \(error)") } bindReachability()) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) try? reachability?.startNotifier() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) reachability?.stopNotifier() } func bindReachability() { reachability?.rx.isDisconnected .withUnretained(self) .flatMap { owner, _ in return owner.rx.makeErrorAlert( title: "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜", message: "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์ด ๋ถˆ์•ˆ์ • ํ•ฉ๋‹ˆ๋‹ค.", cancelButtonTitle: "ํ™•์ธ" ) } .subscribe() .disposed(by: disposeBag) } } ``` ์•ฑ์˜ ์ฒซ๋ฒˆ์งธ๋กœ ๋ณด์—ฌ์ง€๋Š” ๋ทฐ์—์„œ NaverMap์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์—, Splash์—์„œ ๋„คํŠธ์›Œํฌ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ `reachability?.rx.isConnected` ์‹œ์—๋งŒ Main์œผ๋กœ ์ด๋™๋˜๋„๋ก ํ–ˆ๋‹ค. Main์—์„œ๋„ `reachability?.rx.isReachable` ๋กœ ๋„คํŠธ์›Œํฌ ๋ฏธ์—ฐ๊ฒฐ ๋ทฐ๋ฅผ ํ•ธ๋“ค๋งํ•˜๋Š”`rx.handleNetworkErrorViewVisibility`๋ฅผ ๋ฐ”์ธ๋”ฉํ•ด์„œ ๋„คํŠธ์›Œํฌ ๋ฏธ์—ฐ๊ฒฐ ์‹œ Detail๋กœ ์ด๋™๋˜์ง€ ๋ชปํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค. ```swift // Splash์—์„œ ๋„คํŠธ์›Œํฌ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ, ๋„คํŠธ์›Œํฌ ๋ฏธ์—ฐ๊ฒฐ ์‹œ Main์œผ๋กœ ์ด๋™๋˜์ง€ ๋ชปํ•˜๋„๋ก ๊ตฌํ˜„ (Main์— Map์ด ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ) final class SplashViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() reachability?.rx.isConnected .withUnretained(self) .subscribe(onNext: { owner, _ in RootHandler.shard.update(.main) }) .disposed(by: disposeBag) } // Main์—์„œ ๋„คํŠธ์›Œํฌ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ, ๋„คํŠธ์›Œํฌ ๋ฏธ์—ฐ๊ฒฐ ์‹œ Detail๋กœ ์ด๋™๋˜์ง€ ๋ชปํ•˜๋„๋ก ๊ตฌํ˜„ (Detail์— Map์ด ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ) final class LocationViewController: BaseViewController { override func bindReachability() { super.bindReachability() let isReachable = reachability?.rx.isReachable .distinctUntilChanged() .share() isReachable? .bind(to: locationView.rx.handleNetworkErrorViewVisibility) .disposed(by: disposeBag) isReachable? .withUnretained(self) .bind(onNext:{ owner, isReachable in if !isReachable { owner.clearMarker() } }) .disposed(by: disposeBag) } ``` ![ezgif com-resize (1)](https://github.com/kimkyuchul/Jumak/assets/25146374/37bf445e-8679-4f71-a51e-4ea8ad068cb2) ![ezgif com-resize](https://github.com/kimkyuchul/Jumak/assets/25146374/a21dae17-b79f-4a7d-b7b3-c5659a8649ec) --- ์ฃผ๋ง‰ ์„œ๋น„์Šค์˜ ์ฃผ์ฐจ๋ณ„ ๊ฐœ๋ฐœ ์ผ์ง€๋ฅผ ๋ณด๊ณ  ์‹ถ์œผ์‹œ๋‹ค๋ฉด!ย [๐Ÿถ ์ฃผ๋ง‰ ํ”„๋กœ์ ํŠธ Iteration](https://www.notion.so/bee21dc07a0a46aea22f20a6a15c3615?pvs=21) # **๐Ÿซ€ย ์‹คํ–‰ ํ™”๋ฉด** ๐Ÿถ ๋‚ด ์œ„์น˜ ๊ทผ์ฒ˜ ํ˜น์€ ๋‚ด๊ฐ€ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์‹ถ์€ ์œ„์น˜์—์„œ ๋ง‰๊ฑธ๋ฆฌ ์ฃผ๋ง‰์„ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด์š”. ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 05 37](https://github.com/kimkyuchul/Jumak/assets/25146374/1e0d07bf-c1fb-4c56-9dc8-c0a3b8ec666f) ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 07 03](https://github.com/kimkyuchul/Jumak/assets/25146374/28b681be-a941-413a-8b87-04415fb43a1b) ๐Ÿถ ๋ง‰๊ฑธ๋ฆฌ์ง€๋„์—์„œ ์ฐพ์€ ์ฃผ๋ง‰์„ ์„ ํƒํ•ด ์ƒ์„ธ ์ •๋ณด๋ฅผ ์–ป์œผ์„ธ์š”. ์ฃผ๋ง‰๊นŒ์ง€์˜ ๊ธธ์ฐพ๊ธฐ ๊ธฐ๋Šฅ๊ณผ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ฐ ํ‰์  ๋“ฑ๋ก์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 35 00](https://github.com/kimkyuchul/Jumak/assets/25146374/a77c3536-884f-46df-8aab-1137acf0315a) ๐Ÿถ ํ•ด๋‹น ์ฃผ๋ง‰์—์„œ ์žˆ์—ˆ๋˜ ์—ํ”ผ์†Œ๋“œ๋ฅผ ๋“ฑ๋กํ•˜์„ธ์š”. ์—ํ”ผ์†Œ๋“œ ์กฐํšŒ์™€ ์‚ญ์ œ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 43 26](https://github.com/kimkyuchul/Jumak/assets/25146374/a50761bc-c5b4-4440-88ba-a8bed6455884) ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 39 30](https://github.com/kimkyuchul/Jumak/assets/25146374/7a02d966-b4f6-4fac-bd73-f646606546db) ๐Ÿถ ํ‰์ , ์ฆ๊ฒจ์ฐพ๊ธฐ, ์—ํ”ผ์†Œ๋“œ ๋“ฑ๋ก์„ ํ†ตํ•ด ๋‚˜๋งŒ์˜ ์ฃผ๋ง‰ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”. ๋‹ค์–‘ํ•œ ํ•„ํ„ฐ ๊ธฐ๋Šฅ์œผ๋กœ ์ตœ์ ํ™”๋œ ์ฃผ๋ง‰์„ ์„ ์ • ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 53 56](https://github.com/kimkyuchul/Jumak/assets/25146374/daaae176-dbe2-4b2a-9119-9e394cad87f1) ![Simulator Screen Recording - iPhone 14 Pro - 2023-11-02 at 19 46 58](https://github.com/kimkyuchul/Jumak/assets/25146374/0abca9be-ba36-4f74-827d-1c5d32b76ae1)