[내일배움캠프 56일차] 날씨 앱 팀 프로젝트 2

NH·2025년 5월 21일

내일배움캠프

목록 보기
56/62
post-thumbnail

팀 프로젝트: 봄여어름갈겨어울

내가 맡은 역할은 사용자의 위치 정보를 가져오는 기능이다.

RxSwift + MVVM 기반 위치 정보를 가져오는 기능 구현

목표

  • 버튼을 누르면 현재 위치의 주소(예: 서울시 강남구 역삼동)를 표시하는 기능 구현
  • CoreLocation, CLGeocoder, RxSwift MVVM 패턴을 활용해 구조화
  • 검색한 위치를 메인 화면에 전달하여 날씨를 변경하는 기능의 흐름 구상

1. Rx 바인딩 개념 정리

.bind(to: relay)

  • Subject나 Observable의 값을 Relay에 전달할 때 사용
  • 내부적으로 relay.accept(value) 호출
  • Relay는 UI 상태 저장이나 뷰모델 내부 상태 저장용

.asDriver().drive(...)

  • Driver는 Main Thread, 에러 없음, UI 바인딩에 안전
  • UILabel.rx.text 같은 UI 요소에 바인딩할 때 사용
  • .drive(relay)는 불가능 (Relay는 ObserverType이 아님)

올바른 예시

// ViewModel 내부
subject.bind(to: relay)

// ViewController에서 UI 바인딩
driver.drive(label.rx.text)

2. 권한 저장 여부

  • CoreLocation의 권한 상태는 iOS 시스템이 자체적으로 저장 및 관리
  • 앱이 authorizationStatus로 권한 상태를 확인할 수 있음
  • UserDefaults에 따로 저장하지 않아도 됨 (필요할 경우 예외적으로 사용 가능)

3. 날씨 앱 구조 구상

목적

  • 검색 화면에서 주소 검색 → 좌표로 변환
  • 메인 화면에서 해당 좌표의 날씨 정보로 변경

구상한 구조

흐름구성
검색 화면CLGeocoder로 주소 → 좌표 변환
전달 방식PublishSubject 또는 클로저로 메인 화면에 전달
메인 화면전달받은 좌표로 날씨 API 요청 (ViewModel.fetchWeather(for:))
결과 바인딩RxCocoa Driver로 UI 업데이트

오늘의 배운 점

  • bind(to:)는 Relay에, drive()는 UI에 사용하는 것!
  • Driver는 UI에 안정적으로 바인딩할 수 있도록 설계된 특별한 Observable
  • 권한 상태는 시스템에서 관리하므로 직접 저장할 필요 없음

오늘 만든 코드 정리

import Foundation
import CoreLocation
import RxSwift

final class LocationManager: NSObject {
    /// 위치 서비스 관리자 싱글톤 객체
    static let shared = LocationManager()

    private let locationManager = CLLocationManager() // CoreLocation의 핵심 객체. 위치를 요청하고 수신함.
    private let geocoder = CLGeocoder() // 위도/경도를 주소(도시, 구, 동) 등의 텍스트로 바꾸는 객체

    // RxSwift
    // 서브젝트로 사용한 이유: 모델 계층에서 단순 이벤트 전달만 하기 때문
    // UI - ViewModel 은 relay가 적합
    let addressSubject = PublishSubject<String>()
    let errorSubject = PublishSubject<String>()

    private override init() {
        super.init()
        locationManager.delegate = self // 위치가 업데이트되면 이 클래스가 콜백 받도록 설정
        locationManager.desiredAccuracy = kCLLocationAccuracyBest // 위치 정밀도 설정 (가장 정확한 값)
    }

    /// 위치 권한 요청 메서드
    func requestLocation() {
        let status = locationManager.authorizationStatus // 현재 권한 상태 확인
        switch status {
        case .notDetermined: // 아직 사용자에게 권한을 요청하지 않은 상태 → 권한 요청
            locationManager.requestWhenInUseAuthorization()
        case .authorizedWhenInUse, .authorizedAlways: // 권한이 있으면 requestLocation() 호출 → 현재 위치 1회 요청
            locationManager.requestLocation()
        default:
            errorSubject.onNext("위치 권한이 없습니다.")
        }
    }
}

extension LocationManager: CLLocationManagerDelegate {
    /// 위치 업데이트를 받는 delegate 메서드
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // locations.first 사용 이유 : 가장 최신 위치 하나만 사용
        guard let location = locations.first else {
            errorSubject.onNext("위치 정보를 찾을 수 없습니다.")
            return
        }
        
        print(location.coordinate.latitude, location.coordinate.longitude)

        // 위도 경도를 주소로 변환
        geocoder.reverseGeocodeLocation(location) { placemarks, error in
            if let error = error {
                self.errorSubject.onNext("주소 변환 실패: \(error.localizedDescription)")
                return
            }

            if let placemark = placemarks?.first {
                let address = [
                    placemark.administrativeArea, // 경기도
                    placemark.locality,           // 광주시
                    placemark.subLocality         // 신현동
                ].compactMap { $0 }.joined(separator: " ")
                // compactMap { $0 } : nil 값 제거
                // joined(separator: " ") : 공백으로 연결된 주소 문자열 생성

                self.addressSubject.onNext(address) // 이벤트 방출
            } else {
                self.errorSubject.onNext("주소를 찾을 수 없습니다.") // 에러 방출
            }
        }
    }

    // 위치 요청 실패 시 호출됨
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        // 에러 메시지를 ViewModel로 전달
        errorSubject.onNext("위치 가져오기 실패: \(error.localizedDescription)") // 에러 방출
    }
}
profile
iOS 개발 블로그

0개의 댓글