이호준 멘토님의 우당탕탕 멘토링 -3.3-

이전에 썼던 글 에서 당근마켓의 푸시 알림에 대해 조사해갔던 일에 대해 언급한 적이 있다.

당시 조사했던 글에서는 표면적으로 어떻게 푸시 알림을 보내는가에 대해 알아갔었는데, 사실 이건 내가 생각해도 누구나 조금만 조사하면 할 수 있는 기능이라 멘토님 역시 좀 더 깊게 들어가야 한다고 조언해주셨다.

그래서 이번 기회에 GPS/LBS 에 대해 조금 더 많은 공부를 해보자! 해서 애플 공식문서를 따라가면서 여러 가지 위치 기반 서비스 및 GPS 기능에 대해 알 수 있었다.

이번 글에서는 그 얘기를 해보고자 한다.

 

먼저 시도해본 것

xcode에서는 여러 가지 기능들을 제공하고 있는데, 여러 가지를 조사하면서 크게 3 가지 정도로 분류할 수 있었다. 그런데 그들을 하나씩 설명하기보다 일단 그것들을 가지고 내가 가져오고 출력한 데이터를 먼저 소개할까 한다.

 

그것을 보여주기 전에 우리가 알아야 할 것이 있다. 우리가 흔히 말하는 GPS는 무엇을 기반으로 하는지 알고 있는가? 바로 위도와 경도이다. 이것을 기준으로 내가 지도상에서 정확히 어디에 있는지 알 수 있다. 그리고 대부분의 위치기반 서비스는 모두 핵심이 되는 위도와 경도에 의존한다.

 

그렇다면 위도와 경도를 한번 가져와서 출력해보자.

그리고 해당 위도와 경도에 따라 시/군/구 데이터도 받아와보자. 애플에서는 내 위/경도 좌표에 따라 행정구역도 반환하는 기능을 가지고 있다. 또, 현재 내 위치로부터 특정 위치까지의 거리도 받아올 수 있다. 이것도 한번 출력해보자. 특정 위치로는 강남역이 좋겠다. 마지막으로 내 기기가 바라보는 방향을 방위로 나타내보자. 네이버 지도 같은 앱에서 내가 바라보는 방향을 기준으로 파란색 화살표가 생성하는 기억을 떠올릴 수 있을 것이다.

 

 

신기하다. gps를 지원하는 아이패드를 가지고 테스트했고, 내가 원하는 데이터를 모두 받아올 수 있었다.

이제 기능들에 대해 하나씩 얘기를 해볼텐데, 당근마켓과 연관이 될 법한 것부터 얘기를 해보자.

 

당근마켓은 어떤 서비스를 이용할까 ?

제목 그대로 당근마켓은 어떤 서비스를 이용해서 내 위치 주변에서 올라온 상품만 필터링하고 특정 키워드에 대해 푸시 알림을 보낼 수 있을까?

처음에는 시/군/구/동/읍/면 까지 받아올 수 있으니까 같은 행정구역을 기준으로 하지 않을까 했다.

그러나 그렇다고 하기엔 행정구역의 경계에 있는 사람들이 서비스를 제대로 받지 못할 것 같아서, 사용자들간의 정확한 거리를 기준으로 상품을 필터링하고 알림을 보낸다고 생각했다.

그런데 그런 서비스가 이상적이라고는 하더라도 어떤 게시물이 올라온 것에 대해 1000만명이 넘는 사용자 전체의 거리와 비교할 수는 없을 것 같았다.

그래서 일단 다시 들어가보고 판단하기로 했다.

 

당근마켓이 위치정보를 얻는 방식은 이러했다.

  1. 지금 내 동네를 설정한다.
  2. 현재 위치 정보를 토대로 진짜 내가 설정한 동네에 있는지 인증한다.
    • 이 때, 인증은 현재 위치로부터 얻어온 행정구역 데이터와 설정한 동네의 행정구역을 비교하여 일치하는지로 비교한다.
    • (동/읍/면 데이터를 받아올 수 있었으니까 가능한 것으로 보인다. )
    • 현재 사용자가 위치한 곳은 어디인지는 naver map api를 이용한다.

 

그러니까 당근 마켓에서는 비용을 줄이기 위해 내 동네를 직접 설정하고 인증받아 자신의 '동'을 기준으로 주변의 '동'에서 올라온 게시물의 정보를 받아온다고 추측된다. 또한 어느 정도의 반경에 대한 데이터를 받을지 설정할 수 있는 것으로 보아 어떤 동의 주변 데이터를 미리 저장해놓고 내가 설정한 동네 범위 안에 게시물을 올린 사람의 동네가 있는지만 파악해서 서비스 로직을 처리하는 것 같았다.

 

아하. back-end를 정확히 어떻게 구성하는지 알 수는 없어도 어느 정도 예상은 가능했다. 그리고 백엔드가 엄청난 수고를 해줘야 서비스를 운영하는 데에 드는 비용이 장기적으로 많이 줄어들 것 같다는 생각도 들었다.

 

당근마켓에서 어떻게 위치기반서비스를 제공하는지에 대해 어느 정도 예측을 해보았으니 본격적으로 애플에서 제공하는 위치기반 서비스는 무엇이 있는지 알아보자.

 

애플이 제공하는 위치기반 서비스

애플의 공식문서를 읽어가면서 대략적인 LocationService 다이어그램을 그려보았다.

MapKit에 대해서는 조사가 덜 됐지만, 크게 상관은 없다고 본다.

(경험상 애플이 제공하는 MapKit은 현지화가 최대로 이루어져있는 네이버에 '크게' 뒤떨어졌기 때문에.)

(구글의 draw.io를 이용했다.)

 

스크린샷 2021-04-07 오후 2 21 40

내가 조사한 다이어그램을 좀더 보고 싶다면 여기 링크에서 확인이 가능하다.

(참고로 링크를 클릭했을 때 뜨는 draw.io로 열기 버튼을 눌러야 제대로 확인이 가능할 것이다.)

 

CLLocation

CLLocation은 위도 경도 정보를 담고 있는 Coordinate를 제공하고,

특정 location에서 다른 location 까지의 거리를 제공하는 distance() 메서드를 제공한다.

그 외에도 사용자가 위치한 건물의 바닥의 위치를 제공하는 floor,

기기의 고도 정보를 제공하는 altitude, 기기가 움직이는 속도인 speed

위치정보 데이터를 받아온 시간 정보인 timestamp를 제공한다.

 

Location 정보를 받아오는 방법에 대한 예제는 다음 코드를 확인하면 된다.

Location 정보는 사실 LocationManager가 받아오는데, 받아오는 location 정보는 계속 업데이트되어 그 양이 많아질 수 있기 때문에 배열로 저장한다. 이 때 배열의 요소 하나하나가 CLLocation이 되는 것이다.

 

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        self.locationManager.startUpdatingHeading()

        // the most recent location update is at the end of the array.
        let location: CLLocation = locations[locations.count - 1]
        let longtitude: CLLocationDegrees = location.coordinate.longitude
        let latitude:CLLocationDegrees = location.coordinate.latitude

      ...

 

CLLocationManager

CLLocationManager는 실시간으로 위치 정보를 업데이트하고 그 정보를 받아올 수 있다.

뿐만 아니라 AcitivityType에 따른 정보, heading 그리고 beacon 관련 정보들을 받아온다.

 

Activity Type

CoreMotion 서비스의 CMMotionActivity 에서는 현재 사용자가 걷고 있는지, 자전거를 타고 있는지, 자동차를 타고 있는지 등에 대한 정보를 제공한다. 그런데 CLLocationManager의 ActivityType은 그것과는 살짝 다른 정보를 제공한다.

ActivityType은 현재 사용자의 활동을 크게 네 가지로 묶어 어떤 상태인지 그 정보를 제공한다.

그룹별로 보면

  1. fitness - 걷기, 달리기, 사이클링
  2. airbone - 비행기
  3. automotiveNavigation - 자동차
  4. otherNavigation - 자동차가 아닌 차량

 

heading

heading은 현재 디바이스가 바라보는 방위 정보를 나타낸다. 북쪽을 0이라는 값으로 주고, 남쪽을 180이라는 값으로 줘서 각도별로 방위를 계산할 수 있게 만든다. (gyroscope에서도 비슷한 기능을 제공한다.)

이 때, trueHeading과 magneticHeading 이라는 정보 두 가지를 제공하는데, 그 이면에 깔린 정확한 원리는 파악을 못했지만 서로 다른 축을 기준으로 하기 때문에 차이가 나타나는 것 같다. 좀 더 자세한 정보를 원하면 이곳에서 확인하자

 

나는 이 정보를 이용해 디바이스의 방위를 다음과 같이 나타내보았다.

 

// 바라보는 방향(방위)
// binade는 우리가 원하는 자료형 데이터(double 형).
guard let heading = self.locationManager.heading?.trueHeading.binade
else {return}

if(heading > 23 && heading <= 67) {
    self.compassLabel?.text = "북동" + "쪽";
} else if(heading > 68 && heading <= 112){
    self.compassLabel?.text = "동" + "쪽";
} else if(heading > 113 && heading <= 167){
    self.compassLabel?.text = "남동" + "쪽";
} else if(heading > 168 && heading <= 202){
    self.compassLabel?.text = "남" + "쪽";
} else if(heading > 203 && heading <= 247){
    self.compassLabel?.text = "남서" + "쪽";
} else if(heading > 248 && heading <= 293){
    self.compassLabel?.text = "서" + "쪽";
} else if(heading > 294 && heading <= 337){
    self.compassLabel?.text = "북서" + "쪽";
} else if(heading >= 338 || heading <= 22){
    self.compassLabel?.text = "북" + "쪽";
}

 

CLGeoCoder와 CLPlaceMark

CLGeoCoder는 특정 위, 경도 정보를 해석하여 행정구역 정보 등으로 만들어주는

geoCoder.reverseGeocodeLocation(location, preferredLocale: local)

메서드를 제공한다.

이때 preferredLocale에는 나라마다 부여되어 있는 identifier를 적용하면 되고, 대한민국의 경우 Ko-kr이 되겠다.

 

이 때 받아온 정보는 CLPlaceMark라는 데이터 뭉치 타입으로 저장되는데,

해당 필드들을 하나하나 나열하면 다음과 같다.

 

timeZone: Asia/Seoul (current)

name:"망월동 141-2"

isoCountryCode: "KR" 

country: "대한민국"

postalCode: 12191 

administrativeArea: "경기도" 

locality: "하남시”

subLocality: "망월동" 

thoroughfare: "망월동" 

subThoroughfare: "141-2" 

region:CLCircularRegion (identifier:'<+37.56432430,+127.19813000> radius 70.65', center:<+37.56432430,+127.19813000>, radius:70.65m) 

timeZone: Asia/Seoul (current) 

 

이것을 어떻게 받아오는지 궁금해 하시는 분들을 위해 맨 아래에 전체 코드를 올려두겠다.

 

후기

ios의 많은 센서 정보, 디바이스 정보 등을 연구하면서 가장 흥미가 가는 건 이번 CLLocation이었다. 이것을 가지고 활용할 수 있는 분야가 많겠다는 생각이 들었기 때문이다.

당장에는 이걸 가지고 전에 만들다가 멘토링 때문에 잠깐 중단했던 42place 서비스를 좀더 발전시켜 내 위치를 기준으로 100m, 300m, 500m 이내에 있는 가게들을 필터할 수 있는 기능을 넣어보고 싶다.

멘토님께서 조사해서 갖고 오라고 하셨던 기간보다 1주일을 더 받아서 여기까지 진행했는데, 시간이 그래도 조금은 더 넉넉해서 많은 것들을 조사할 수 있었다. 이제는 눈도 아프지 않아서 다음 과제는 기한을 꼭 맞춰야겠다.

 

전체코드

 

//
//  GPSViewController.swift
//  HoTechCourse
//
//  Created by 최강훈 on 2021/04/03.
//

import UIKit
import CoreLocation

class GPSViewController: UIViewController  {

    @IBOutlet weak var latitudeLabel: UILabel!
    @IBOutlet weak var longtitudeLabel: UILabel!
    @IBOutlet weak var administrativeAreaLabel: UILabel!
    @IBOutlet weak var localityLabel: UILabel!
    @IBOutlet weak var subLocalityLabel: UILabel!
    @IBOutlet weak var distanceFromGangnamStationLabel: UILabel!
    @IBOutlet weak var compassLabel: UILabel!

    lazy var locationManager: CLLocationManager = {
        let manager = CLLocationManager()
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.distanceFilter = kCLHeadingFilterNone
        manager.delegate = self
        return manager
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.startUpdatingLocation()

        requestGPSPermission()
    }

    func requestGPSPermission(){
        locationManager.requestWhenInUseAuthorization()

        switch CLLocationManager.authorizationStatus() {
        case .authorizedAlways, .authorizedWhenInUse:
            guard let coordinate = locationManager.location?.coordinate
            else {return}
            latitudeLabel?.text = "\(coordinate.latitude)"
            longtitudeLabel?.text = "\(coordinate.longitude)"
        case .restricted, .notDetermined:
            DispatchQueue.main.async {
                self.requestGPSPermission()
            }
        case .denied:
            print("GPS: 권한 없음")
        default:
            print("GPS: Default")
        }
    }
}

extension GPSViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        self.locationManager.startUpdatingHeading()


        // the most recent location update is at the end of the array.
        let location: CLLocation = locations[locations.count - 1]
        let longtitude: CLLocationDegrees = location.coordinate.longitude
        let latitude:CLLocationDegrees = location.coordinate.latitude

        let geoCoder: CLGeocoder = CLGeocoder()
        let local: Locale = Locale(identifier: "Ko-kr") //korea

        geoCoder.reverseGeocodeLocation(location, preferredLocale: local) {
            (place, error) in
            if let address: [CLPlacemark] = place {
                self.latitudeLabel?.text = "\(latitude)"
                self.longtitudeLabel?.text = "\(longtitude)"
                self.administrativeAreaLabel?.text =
                    address.last?.administrativeArea
                self.localityLabel?.text =
                    address.last?.locality
                self.subLocalityLabel?.text
                    = address.last?.subLocality

                // 바라보는 방향(방위)
                guard let heading = self.locationManager.heading?.trueHeading.binade
                else {return}

                if(heading > 23 && heading <= 67) {
                    self.compassLabel?.text = "북동" + "쪽";
                } else if(heading > 68 && heading <= 112){
                    self.compassLabel?.text = "동" + "쪽";
                } else if(heading > 113 && heading <= 167){
                    self.compassLabel?.text = "남동" + "쪽";
                } else if(heading > 168 && heading <= 202){
                    self.compassLabel?.text = "남" + "쪽";
                } else if(heading > 203 && heading <= 247){
                    self.compassLabel?.text = "남서" + "쪽";
                } else if(heading > 248 && heading <= 293){
                    self.compassLabel?.text = "서" + "쪽";
                } else if(heading > 294 && heading <= 337){
                    self.compassLabel?.text = "북서" + "쪽";
                } else if(heading >= 338 || heading <= 22){
                    self.compassLabel?.text = "북" + "쪽";
                }

//                print("name:\(address.last?.name)")
//                print("isoCountryCode: \(address.last?.isoCountryCode)")
//                print("country: \(address.last?.country)")
//                print("postalCode: \(address.last?.postalCode)")
//                print("administrativeArea: \(address.last?.administrativeArea)")
//                print("subAdministrativeArea:\(address.last?.subAdministrativeArea)")
//                print("locality: \(address.last?.locality)")
//                print("subLocality: \(address.last?.subLocality)")
//                print("thoroughfare: \(address.last?.thoroughfare)")
//                print("subThoroughfare: \(address.last?.subThoroughfare)")
//                print("region:\(address.last?.region)")
//                print("timeZone: \(address.last?.timeZone)")
//
//
                let locationGangnam = CLLocation(latitude: 37.4968985, longitude: 127.0298547)
                let distanceFromGangnamStation = locationGangnam.distance(from: location)
                self.distanceFromGangnamStationLabel?.text 
                      = "\(distanceFromGangnamStation / 1000)"

            }
        }
    }
}

 

 

토링 3-1,2,3편 refs

https://developer.apple.com/documentation/coremotion/getting_raw_gyroscope_events

https://blog.naver.com/horajjan/220572460973

https://faith-developer.tistory.com/34

https://hyperline.tistory.com/5

https://jinnify.tistory.com/35

https://ichi.pro/ko/ios-geunjeob-senseoleul-choedaehan-gandanhage-180817578442185

https://stackoverflow.com/questions/10432023/how-to-measure-the-distance-in-meters-between-two-cllocations#comment13465359_10432069

NFC:https://betterprogramming.pub/working-with-nfc-tags-in-ios-13-d08c7d183981

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기

댓글을 달아 주세요

">