지도 프로토타입의 주요 기능은 두 가지이다. 첫째, 사용자가 위치 권한을 동의하면 사용자의 현재 위치를 중심으로 지도에 마커를 표시한다. 둘째, 지도에 표시된 문화예술 콘텐츠 마커를 클릭하면 작은 정보 창을 통해 상세 내용을 보여준다.
문제인식
프로토타입 초기 개발 단계에서 예상치 못한 문제가 발생했다. 위치 권한을 받아 사용자 마커를 지도에 표시하는 데는 성공했으나, 사용자가 지도를 이동시키면 몇 초 후에 자동으로 사용자 마커 위치로 카메라가 다시 이동하는 현상이 나타났다. 처음에는 시뮬레이터의 오류로 추정하여 실제 iPhone에서 테스트해 보았지만, 동일한 문제가 발생했다.
이 문제를 해결하기 위해 네이버 지도 API 문서를 상세히 검토했다. 문서에 따르면, 사용자의 현재 위치를 표시할 때는 일반 마커가 아닌 오버레이를 사용해야 한다는 점을 파악했다. 그러나 오버레이로 변경한 후에도 문제는 지속되었다.
코드를 분석한 결과, 카메라 이벤트에 문제가 있을 것으로 추정했다. 하지만 카메라 이벤트 자체는 단순히 이동 애니메이션을 처리하는 것이었고, 이 문제와 직접적인 연관은 없었다.
각 단계마다 테스트 로그를 추가하여 확인한 결과, 위치 업데이트가 지속적으로 발생하고 있다는 사실을 발견했다. 현재 구현에서는 사용자의 위치가 업데이트될 때마다 카메라를 이동시키고 있어, 사용자가 수동으로 지도를 이동시켜도 다시 현재 위치로 돌아가는 현상이 발생했던 것이다.
이 문제를 해결하기 위해, 초기 위치 설정 후에는 자동으로 카메라 이동을 비활성화하도록 코드를 수정했다
class MapViewController: UIViewController {
// ...
// 초기 위치 설정 여부 추적용 플래그
private var isInitialLocationSet = false
private func bind() {
// ...
output.currentLocation
.drive(with: self, onNext: { owner, location in
owner.addOrUpdateUserMarker(location: location)
owner.addOrUpdateRangeCircle(location: location)
// 초기 위치 설정이 되지 않았을 때만 카메라 이동
if !owner.isInitialLocationSet {
owner.moveCamera(location: location)
owner.isInitialLocationSet = true
}
})
.disposed(by: disposeBag)
// ...
}
// ...
}
수정된 코드에서는 `isInitialLocationSet` 플래그를 사용하여 초기 위치 설정 여부를 추적한다. 처음 위치를 받았을 때만 카메라를 이동시키고, 그 이후에는 사용자 마커와 범위 원만 업데이트하도록 변경했다.
과거의 나라면 이 정도 해결책에 만족하고 더 이상 고민하지 않았을 것이다. 하지만 이번에는 더 나은 해결 방법이 있지 않을까 계속 고민해 보았다.
현재 방식은 카메라 이벤트를 특정 상황에만 업데이트하도록 수정한 것이다. 그러나 위치 업데이트 자체는 지속적으로 발생하고 있어, 불필요한 메모리 사용과 처리가 일어나고 있다. 이는 문제의 근본적인 해결책이 아니라는 생각이 들었다.
따라서 더 효율적인 해결책을 찾기 위해, 사용자의 위치 정보를 업데이트하는 코드를 자세히 살펴보기로 했다. 근본적인 원인을 해결함으로써 불필요한 리소스 사용을 줄이고, 앱의 전반적인 성능을 개선할 수 있을 것이라고 생각했다.
final class LocationService: NSObject {
// ...
func startUpdatingLocation() {
locationManager?.startUpdatingLocation()
}
func observeUpdatedLocation() -> Observable<[CLLocation]> {
return PublishRelay<[CLLocation]>.create({ emitter in
self.rx.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)))
.compactMap({ $0.last as? [CLLocation] })
.subscribe(onNext: { location in
emitter.onNext(location)
})
.disposed(by: self.disposeBag)
return Disposables.create()
})
}
}
처음에 구현한 LocationService이다. startUpdatingLocation() 메소드는 CLLocationManager의 startUpdatingLocation() 메소드를 호출한다. 이 메소드는 지속적으로 위치 업데이트를 요청하며, 새로운 위치가 확인될 때마다 델리게이트 메소드를 호출한다. observeUpdatedLocation() 메소드는 위치 업데이트를 Observable로 변환한다. 위치가 업데이트될 때마다 새 이벤트를 방출한다.
import CoreLocation
import RxCocoa
import RxSwift
final class LocationService: NSObject {
var locationManager: CLLocationManager?
private let authorizationStatus = PublishSubject<CLAuthorizationStatus>()
private let locationSubject = PublishSubject<CLLocation>()
var disposeBag = DisposeBag()
override init() {
super.init()
self.locationManager = CLLocationManager()
self.locationManager?.delegate = self
self.locationManager?.desiredAccuracy = kCLLocationAccuracyHundredMeters
}
func observeUpdateAuthorization() -> Observable<CLAuthorizationStatus> {
return self.authorizationStatus.asObservable()
}
func requestAuthorization() {
self.locationManager?.requestWhenInUseAuthorization()
}
func requestLocation() -> Observable<CLLocation> {
self.locationManager?.requestLocation()
return locationSubject.asObservable()
}
}
extension LocationService: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
locationSubject.onNext(location)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
locationSubject.onError(error)
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus.onNext(manager.authorizationStatus)
}
}
계속적인 업데이트 요청이 문제였기 때문에 일회성으로 위치를 요청하는 것으로 수정했다. requestLoaction() 메소드는 한 번만 위치를 요청하고, 위치가 결정되면 즉시 업데이트를 중단한다.
import RxSwift
import CoreLocation
final class MapRepository: MapRepositoryProtocol {
private let disposeBag = DisposeBag()
private let locationService = LocationService()
private let mockCultureItems: [MapRequestDTO] = [
MapRequestDTO(category: "연극", title: "보도지침", date: "08.02 ~ 08.04", rate: Int(4.7), review: ["Great experience", "Informative", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see"], latitude: "37.5835167", longitude: "127.000494"),
MapRequestDTO(category: "콘서트", title: "두아 리파 내한 공연", date: "08.11 ~ 08.15", rate: Int(3.5), review: ["Amazing performance", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see"], latitude: "37.5846181", longitude: "126.9987679"),
MapRequestDTO(category: "전시회", title: "[슈퍼 얼리버드] 디즈니 100년 특별전", date: "08.12 ~ 08.15", rate: Int(5.0), review: ["Amazing performance", "Must see", "Must see", "Must see", "Must see"], latitude: "37.5868754", longitude: "126.9998622"),
MapRequestDTO(category: "콘서트", title: "코난 그레이 내한공연", date: "08.29 ~ 08.30", rate: Int(4.3), review: ["Amazing performance", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see"], latitude: "37.5815764", longitude: "127.0053164"),
MapRequestDTO(category: "콘서트", title: "아이유 콘서트", date: "09.01 ~ 09.5", rate: Int(5.0), review: ["Amazing performance", "Must see", "Must see", "Must see", "Must see", "Must see"], latitude: "37.576834", longitude: "127.0031357"),
MapRequestDTO(category: "연극", title: "행오버", date: "09.08 ~ 09.11", rate: Int(4.1), review: ["Amazing performance", "Must see", "Must see", "Must see", "Must see"], latitude: "37.5785997", longitude: "126.9862442"),
MapRequestDTO(category: "연극", title: "스위치", date: "09.15 ~ 09.18", rate: Int(4.6), review: ["Amazing performance", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see"], latitude: "37.5716188", longitude: "127.000782"),
MapRequestDTO(category: "연극", title: "두 여자", date: "09.21 ~ 09.24", rate: Int(4.9), review: ["Amazing performance", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see", "Must see"], latitude: "37.5812726", longitude: "127.0015344")
]
var currentUserLocation = PublishSubject<CLLocationCoordinate2D>()
var authorizationStatus = PublishSubject<LocationAuthorizationStatus>()
init() { }
func checkUserCurrentLocationAuthorization() {
self.locationService.observeUpdateAuthorization()
.subscribe(with: self, onNext: { owner, status in
switch status {
case .notDetermined:
owner.locationService.requestAuthorization()
case .authorizedAlways, .authorizedWhenInUse:
owner.authorizationStatus.onNext(.allowed)
owner.requestUserLocation()
case .denied, .restricted:
owner.authorizationStatus.onNext(.disallowed)
default:
owner.authorizationStatus.onNext(.notDetermined)
}
})
.disposed(by: disposeBag)
}
func fetchCultureItem() -> Observable<[Map]> {
return Observable.just(mockCultureItems.map { $0.toEntity()} )
}
private func requestUserLocation() {
self.locationService.requestLocation()
.take(1)
.subscribe(with: self, onNext: { owner, location in
owner.currentUserLocation.onNext(location.coordinate)
})
.disposed(by: disposeBag)
}
}
이에 맞게 레포지토리도 수정했다. 레포지토리에서 requestUserLocation() 메소드는 take(1)을 사용하여 첫 번째 위치만 처리한다.
마무리
이번 경험을 통해 나는 문제 해결에 대한 새로운 시각을 얻었다. 과거의 나였다면 단순히 문제가 해결되었다는 사실에 만족하고 그 자리에 안주했을 것이다. 하지만 이번에는 한 걸음 더 나아가 더 나은 해결책을 모색하고, 문제의 근본적인 원인을 파악하려고 노력했다.
이러한 접근 방식은 단기적인 해결책을 넘어서 장기적으로 더 효율적이고 안정적인 시스템을 구축하는 데 도움이 된다. 또한, 이 과정에서 코드의 효율성과 성능에 대해 더 깊이 고민하게 되었고, 이는 제 개발능력을 한 단계 끌어올리는 계기가 되었다.
앞으로도 문제에 직면했을 때, 단순히 표면적인 해결에 그치지 않고 근본적인 원인을 파악하고 최적의 해결책을 찾기 위해 노력할 것이다.
'우리 같이 협업하자' 카테고리의 다른 글
[우협하] 13~15주차 회고 (1) | 2024.10.21 |
---|---|
[우협하] 9~12주차 회고 (4) | 2024.09.29 |
[우협하] 8주차 회고 - 개발 패러다임은 어렵다. (3) | 2024.09.02 |
[우협하] 7주차 회고 - 초기화 (0) | 2024.08.25 |
[우협하] 6주차 회고 - 본격적인 개발을 시작하다. (+ 코디네이터 패턴) (0) | 2024.08.23 |