티스토리 뷰
지오코딩과 역지오코딩
지오코딩(Geocoding)은 주소를 위치 정보(일반적으로 위도, 경도 쌍으로 이뤄진 좌표)로 변환하는 것을 말한다. 역지오코딩(Reverse Geocoding)은 반대로 위치 정보를 주소로 변환하는 것을 말한다. iOS에서는 CoreLocation과 MapKit의 CLGeocoder를 이용해 두 작업 모두 수행할 수 있다.
현재 사이드 프로젝트에 유저 현재 정보를 토대로 짧은 주소를 표시해야하는 요구사항이 있다. 예를 들어서, 디바이스가 서울특별시 마포구 신수동의 어딘가에 있다면 "마포구 신수동"과 같이 당근마켓 식으로 표시해야 한다. 이를 위해 역지오코딩이 필요했다.
현재 위치 가져오기
CoreLocation을 통해 현재 위치를 가져올 수 있다. Info.plist에 "Privacy - Location When In Use Usage Description"이나 "Privacy - Location Always Usage Description"이 추가되어 있어야 한다.
아래 코드에서는 위치 정보와 관련된 권한이 변경되었을 떄 현재 정보를 가져오도록 했다.
// MARK: - ViewController
class ViewController: UIViewController {
private var locationManager: CLLocationManager!
override func viewDidLoad() {
// ...
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
// 또는 locationManager.requestAlwaysAuthorization()
// ...
}
// ...
}
// MARK: - CLLocationManager Delegate 구현
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse,
let currentLocation = manager.location {
reverseGeocoding(with: currentLocation)
}
}
private func reverseGeocoding(with location: CLLocation) {
// TODO: 역지오코딩
}
}
역지오코딩 해보기
CLGeocoder의 reverseGeocodeLocation 메서드를 통해 역지오코딩을 할 수 있다.
private func reverseGeocoding(with location: CLLocation) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, preferredLocale: Locale(identifier: "ko-KR")) { places, error in
guard error == nil else {
print("reverseGeocodeLocation error - \(error?.localizedDescription ?? "N/A")")
return
}
if let places = places {
guard let place = places.first else { return }
print("country: \(place.country ?? "")")
print("thoroughfare: \(place.thoroughfare ?? "")")
print("subThoroughfare: \(place.subThoroughfare ?? "")")
print("locality: \(place.locality ?? "")")
print("subLocality: \(place.subLocality ?? "")")
print("administrativeArea: \(place.administrativeArea ?? "")")
print("subAdministrativeArea: \(place.subAdministrativeArea ?? "")")
print("postalCode: \(place.postalCode ?? "")")
}
}
}
reverseGeocodeLocation은 비동기로 수행되며 위와 같이 completionHandler로 클로져를 넘겨 이후 작업을 진행하면 된다. 또는 Swift Concurrency에 대응하므로 await을 통해 처리할 수도 있다.
completionHandler에는 [CLPlacemark]?와 Error? 인자가 전달된다. 첫 번쨰 인자를 통해 역지오코딩된 장소 정보를 얻을 수 있고, 두 번째 인자를 nil 검사해 오류 여부를 확인할 수 있다.
주의사항은 이 작업이 인터넷 연결을 필요로 한다는 점이다. 네트워크를 끊으면 오류가 발생하는 것을 볼 수 있다. (The operation couldn’t be completed. (kCLErrorDomain error 2.) 또한 너무 빈번하게 요청하는 경우에도 같은 오류가 발생하며, 여기에 더해 쓰로틀링되었다는 로그가 출력된다.
중요한 문제가 있다
위의 예제를 실행하면 다음과 같이 출력된다.
thoroughfare: 용강동
subThoroughfare: 112-12
locality: 서울특별시
subLocality: 용강동
administrativeArea: 서울특별시
subAdministrativeArea:
postalCode: 04166
정말로 위치 좌표만으로 주소를 알 수 있다. 그런데 "서울특별시"와 "용강동"은 있는데, 마포구가 없다. 아마도 CLPlacemark가 우리나라 행정구역 체계를 제대로 표현해내지 못하는 것 같다. 위에서 이야기했듯 이번 프로젝트에서는 "마포구 용강동"처럼 표시해야하기 때문에 곤란한 상황이었다.
다행히도 카카오페이에서 동일한 문제와 관련된 아티클을 공유한 적이 있고 몇 가지 해결책이 제시되어 있다. 결론적으로 CLPlacemark의 addressDictionary 프로퍼티를 활용하거나 debugDescription 프로퍼티를 활용하면 된다.
addressDictionary
let formattedAddressLines = place.addressDictionary?["FormattedAddressLines"] as? [String]
// ["대한민국 서울특별시 마포구 용강동 112-12", "04166"]
addressDictionary의 FormattedAddressLines 키를 참조해 전체 주소를 얻을 수 있다. 다만 이 프로퍼티는 Deprecated 되어 주의를 필요로 한다. Xcode 경고에는 CLPlacemark의 속성들을 활용하라고 하는데 이 속성이 온전하지 않은게 문제이니... 다만 다행히도 다른 방법이 있다.
debugDescription
let debugDescription = place.debugDescription
// 용강동 112-12, 대한민국 서울특별시 마포구 용강동 112-12, 04166 @ <+37.54129517,+126.94210786> +/- 100.00m, region CLCircularRegion (identifier:'<+37.54123010,+126.94214170> radius 70.65', center:<+37.54123010,+126.94214170>, radius:70.65m)
CLPlacemark는 NSObject의 debugDescription()을 오버라이딩하고 있어서 프로퍼티로 얻을 수 있는 정보 외에 다양한 정보를 문자열로 가져올 수 있다.
간단하게 아래와 같이 정규표현식을 사용하여 원하는 포맷으로 변환해볼 수 있다.
let debugDescription = place.debugDescription
// 용강동 112-12, 대한민국 서울특별시 마포구 용강동 112-12, 04166 @ <+37.54129517,+126.94210786> +/- 100.00m, region CLCircularRegion (identifier:'<+37.54123010,+126.94214170> radius 70.65', center:<+37.54123010,+126.94214170>, radius:70.65m)
do {
let regex = /대한민국.*?,/
if let match = try regex.firstMatch(in: debugDescription) {
let matchedString = match.output // "대한민국 서울특별시 마포구 용강동 112-12,"
let addressComponents = matchedString.split(separator: " ")
if addressComponents.count >= 4 {
let shortAddress = "\(addressComponents[2]) \(addressComponents[3])"
print(shortAddress) // "마포구 용강동"
// TODO: Use address
}
}
} catch {
print("faild to parse debugDescription... error: \(error.localizedDescription)")
}
정규표현식 /대한민국.*?,/은 "대한민국"으로 시작해 첫번째 ","로 종료되는 부분에 매칭된다. 즉, "용강동 112-12, 대한민국 서울특별시 마포구 용강동 112-12, 04166 @ <+37.54129517,+126.94210786> ..."와 같은 문자열에서 매칭되는 부분은 "대한민국 서울특별시 마포구 용강동 112-12,"이다. 이를 다시 공백문자를 이용해 나누었다.
(위의 정규표현식에서 ".*?" 대신에 ".*"를 쓰면 대한민국으로 시작해 마지막 ","까지 매칭되므로 의도한 바와는 다르게 동작한다. 그러나 이 경우에서는 그렇게 매칭되더라도 구와 동을 추출하는데는 문제가 없다.)
완벽하게 해결되지는 않는다
위에서 addressDictionary와 debugDescription을 통해 구와 동 정보를 얻어내는 방법을 살펴보았지만 아직 문제가 완벽히 해결되었다고 할 수는 없다. addressDictionary는 Depreacted 상태이기 때문에 언제든 새로운 iOS가 나오면 사라질 수 있다.
debugDescription은 말 그대로 디버거 출력 용도로 제공되는 것이므로, 위와 같이 가공해서 사용하는 것은 최초 의도와 다르기 때문에 항상 동일한 형식의 문자열이 반환될 것이라고 보장할 수 없다. 애플이 CLPlacemark의 debugDescription 형식을 변경할테니 대비하라고 메일을 보내주지는 않을 것이다.
게다가 시-구-동으로 행정구역을 나누는 것은 서울특별시와 일부 광역시일 뿐, 다른 지역의 경우 행정구역 체계가 다를 수 있다. 또한 행정동/법정동을 구분하라는 요구사항이 추가된다든지 세부 요건이 생긴다면 CLGeocoder만으로는 대응할 수 없게 된다.
따라서 역지오코딩의 신뢰도가 보장되어야 하는 경우나 요구사항이 까다로운 경우라면 CLGeocoder를 사용하지 않고 써드파티 서비스를 사용하거나 직접 시스템을 구현해야할 것 같다. 카카오나 네이버가 관련 기능을 HTTP API로 제공하며 depth별 행정구역 명칭, 행정동/법정동, 지번 주소/도로명 주소 등 보다 다양한 데이터를 반환해준다.
[추가]
이후에 동네를 산책하면서 테스트해보니 다음과 같은 경우가 있었다. 위의 두 가지 방법으로 전체 주소를 얻으면 도로명 주소가 나오게 되면서 동에 대한 정보는 얻을 수 없었다. 다만 subLocality에는 동이 제대로 찍힌다.
선택한 방법
사이드프로젝트이기 때문에 심각하게 고민하지는 않고 debugDescription을 활용했다. 다만 위에서 봤던 것처럼 debugDescription에 도로명 주소가 들어오는 경우가 있기 떄문에, 구(두번째 행정구역)는 debugDescription에서 가져오고, 동(세번째 행정구역)은 subLocality로 가져오도록 처리했다.
let debugDescription = place.debugDescription
do {
let regex = /대한민국.*?,/
if let match = try regex.firstMatch(in: debugDescription) {
let matchedString = match.output // "대한민국 서울특별시 마포구 용강동 112-12,"
let addressComponents = matchedString.split(separator: " ")
if addressComponents.count >= 4 {
let districtName = "\(addressComponents[2])"
let localityNameFallback = "\(addressComponents[3])"
let shortAddress = "\(districtName) \(firstPlace.subLocality ?? localityNameFallback)"
print("result: \(shortAddress)")
// TODO: Use address
}
}
} catch {
print("faild to parse debugDescription... error: \(error.localizedDescription)")
}
여러 군데 넣어서 테스트해봤는데 문제없이 잘 되는 것 같다.
참고자료
https://www.hackingwithswift.com/read/22/2/requesting-location-core-location
https://tech.kakaopay.com/post/ios-mapkit/#원인-2-한국의-행정구역-체계에-완벽히-대응되지-않는-clplacemark
'컴퓨터 > iOS' 카테고리의 다른 글
스위프트 테스팅 소개 (Meet Swift Testing, WWDC24) (0) | 2025.02.02 |
---|---|
Xcode 에디터 수평분할, 수직분할 (0) | 2025.02.02 |
[iOS] 날짜/시간 치트시트 (0) | 2025.01.15 |
[iOS] CoreData 사용해보기 (0) | 2024.12.24 |