Swift와 Realm으로 산타 위치 추적 앱 만들기 튜토리얼: Part 1

안녕하세요? 연말은 즐겁게 보내고 계신가요? 당신처럼 멋진 모바일 개발자들을 위해 저희가 어떤 선물을 드려야 할 지 고민을 좀 했습니다. 음. 근데 진짜 받고 싶은 선물은 뭐죠? 🤔

역시, 뛰어난 앱을 만드는 것이겠죠? 🙌

저희가 여러분에게 도움을 드릴 수 있는 최선의 방법은 훌륭한 제품을 제공하는 것이라고 생각합니다. 그래서 이번 12월달에는, 아직 저희 제품을 사용해 본 적이 없거나 내년에 좀 더 생산적인 개발자가 되길 희망하는 분들을 위해 새로운 자습서를 제공하기로 했습니다. 지금 바로 시작해 볼까요?

Swift 자습서는 총 4 개의 포스팅으로 구성될 예정이며, 크리스마스 이브에 전 세계를 돌아다니는 산타 클로스 위치��� 추적하는 간단한 앱을 작성할 것입니다. 이 자습서에서는 저희가 만든 2 가지 제품이 사용되는데 그것은 바로 데이터 저장과 처리를 위한 Realm 모바일 데이터베이스, 동기화를 위한 Realm 모바일 플랫폼입니다. Realm 을 이용해 앱을 작성한 경험이 있는 분들도 이 자습서를 통해 새로운 정보를 얻을 수 있을 것입니다.

이 자습서에서는 아래와 같은 앱을 만들 예정입니다.:

산타 추적 앱 최종 결과물

첫 번째 포스팅에서는 앱을 작성하기 위해 필요한 환경설정과 기본적인 파일 설정을 다루며, 최종적으로 산타의 현재 위치가 지도화면에 제대로 나타나는지 확인해 볼 것입니다. 앱의 기본설정을 쉽게 따라할 수 있도록 8 가지 단계로 나눠서 구성하였습니다.

다음번 포스팅에서는 화면에 산타의 경로를 보여주고, 다른 데이터들을 업데이트 하는 방법을 다룰 것입니다. 포스팅 중에는 Realm Object Server 를 다루는 부분도 있습니다. 자, 간략히 내용을 살펴봤으니, 이제 시작해봅시다.

1. 새로운 Xcode 프로젝트 생성하기

  1. “Single View Application” 템플릿을 선택하고 “Santa Tracker” 라는 이름으로 새로운 프로젝트를 생성합니다. (“Santa Tracker” 외의 다른 이름을 사용해도 상관없습니다.)
  2. “Language” 는 “Swift”, “Devices” 는 “iPhone” 을 선택하고, Core Data 는 사용하지 않을 예정이기 때문에 체크하지 않습니다.

2. storyboard 설정

  1. 이 앱은 하나의 view controller 만 사용하는데요, 가장 쉬운 구성방법은 제가 미리 작성해 놓은 storyboard 를 사용하는 것입니다. 다운로드 받은 storyboard 파일로 Main.storyboard 를 교체하거나 Interface Builder 에서 view controller 를 복사하여 사용할 수 있습니다.
  2. IB 안에서 복사한 경우라면, 추가적으로 view controller 를 “Is Initial View Controller” 로 설정해야 합니다. 설정 후 IB 에서 view controller 왼쪽에 화살표가 나타나는지 확인하기 바랍니다.
  3. UI 는 자유롭게 변경하셔도 됩니다. 좋은 디자인 아이디어가 있다면 적용해 보세요. 🎨 (Auto Layout 디버깅에 익숙하지 않다면 제가 설정한 layout 을 그대로 사용하셔도 좋습니다.)
  4. 프로젝트에 이미지를 추가하기 전까지는 하단의 이미지들이 제대로 표시되지 않을 것입니다. 이미지들은 여기에서 다운로드 받을 수 있습니다. Assets.xcassets 파일에 다운로드 받은 이미지들을 추가하고 이미지 뷰에서 해당 이미지를 적절하게 설정해 주세요.
  5. 만약 storyboard 를 복사한 경우라면, view controller 의 클래스 이름을 변경해야 합니다. ViewController.swift 파일을 선택하고 클래스 이름을 ViewController 에서 SantaTrackerViewController 로 변경합니다. 필수 사항은 아니지만 파일 이름도 SantaTrackerViewController.swift 로 변경할 수 있습니다. (저는 클래스 이름과 파일 이름을 동일하게 설정하기 위해 파일 이름도 변경했습니다.)

3. MapKit 링크하기

  1. 앱에서 지도를 사용해 본 경험이 있다면, MapKit 을 링크하지 않으면 앱이 크래쉬된다는 사실을 알고 계시겠죠?
  2. 프로젝트 설정을 열고 “General” 탭을 선택한 다음, 스크롤을 내려서 “Linked Frameworks and Libraries” 섹션을 찾으세요. 여기에 MapKit.framework 을 추가합니다.

4. 프로젝트에 Realm 추가하기

  1. CocoaPods 를 이용해 Realm Swift 를 추가할 것입니다. CocoaPods 가 설치되어 있지 않다면 CocoaPods 를 먼저 설치하세요. 다른 설치 방법을 원한다면 이곳을 참조하세요. 하지만 CocoaPods 를 이용한 설치가 가장 쉽습니다.
  2. Xcode 를 종료하세요. 이제 새로운 워크 스페이스를 만들 차례입니다.
  3. 터미널을 열고 프로젝트 디렉토리로 이동하세요. 해당 디렉토리에는 .xcodeproj 파일이 포함되어 있습니다.
  4. pod init 명령어를 실행하여 Podfile 을 생성한 뒤, Podfile 을 열어 # Pods for Santa Tracker 다음 라인에 pod 'RealmSwift', '2.1.0' 을 추가합니다. 저장 후 닫습니다.
  5. 터미널로 돌아와서 pod install 을 실행한다.
    • CocoaPods 를 처음 사용하는 경우라면 상당한 시간이 걸릴 것입니다. 차 한잔 하시면서 기다리세요. 🍵
    • Unable to satisfy the following requirements 와 같은 문구가 나타난다면, Podspec repo 가 오래된 것이므로, pod repo update 명령어를 먼저 실행해주세요.
    • Pod installation complete! 메시지가 나타나면 설치가 완료된 것입니다.
  6. CocoaPods 안내문에도 나타나듯이, 이제부터는 .xcodeproj 대신 .xcworkspace 파일을 사용할 것입니다. .xcodeproj 을 사용하면 Pods 에 추가한 라이브러리를 사용할 수 없습니다.

5. 앱 실행하기(제대로 설정되었는지 확인하기 위한 단계임).

  1. .xcworkspace 파일을 열고 Run 을 실행합니다. (플레이버튼을 눌러도 동일한 동작을 합니다.) ▶️
  2. 시뮬레이터가 앱 실행에 성공했다면 모든 설정을 제대로 했다는 뜻입니다. 🎉
  3. 실행되지 않는다면 디버그할 시간입니다. 위에서 설명한 모든 단계를 정확하게 실행했는지 다시 한번 확인하기 바랍니다. 확인을 위해 위의 절차들을 다시 한번 꼼꼼하게 읽어보길 바랍니다. 도움이 필요하다면 저희에게 알려주세요.

6. outlets 설정

SantaTrackerViewController 파일에 아래와 같은 outlets 을 추가합니다:

이런 개발 뉴스를 더 만나보세요

@IBOutlet private weak var timeRemainingLabel: UILabel!
@IBOutlet private weak var mapView: MKMapView!
@IBOutlet private weak var activityLabel: UILabel!
@IBOutlet private weak var temperatureLabel: UILabel!
@IBOutlet private weak var presentsRemainingLabel: UILabel!

이들 outlets 은 storybord 를 열어 적절하게 연결합니다.

SantaTrackerViewController 파일에 import MapKit 을 추가해야한다는 사실도 잊지 마세요.

7. 기본적인 데이터 모델 설정

모델 클래스에 대해 설명하기에 앞서 주의사항을 알려드립니다. property 와 model 이름은 반드시 제가 설정한 이름으로 설정하기 바랍니다. 만약 다른 이름으로 설정할 경우 이어지는 포스팅에서 소개될 Realm Object Server 와의 연결과정에서 앱이 크래쉬될 것입니다.

이러한 상황을 방지하기 위해 property 이름을 변경없이 그대로 사용해 줄 것을 부탁드립니다.

  1. Santa 를 위한 데이터 모델부터 작성하도록 하겠습니다. 이야기를 진행하면서 모델 구성은 확장해 나갈 것입니다. 우선, 위치를 보여주기 위해 필요한 것들을 추가해보겠습니다. Santa.swift 라는 Swift 파일을 생성하세요. Santa 객체는 하나만 사용하지만 데이터 동기화를 위해서 클래스로 만들 필요가 있습니다.

    class Santa: Object {
        dynamic var currentLocation: Location?
    }
    

    (모델 파일을 작성할 때 import RealmSwift 구문을 추가하는 것을 잊지마세요.) Location 타입으로 선언된 currentLocation 속성(property, 이하 속성으로 표기함)은 산타가 현재 어디에 있는지 알려줄 것 입니다. Location 타입은 무엇일까요?

  2. Location.swift 라는 Swift 파일을 추가로 작성하겠습니다.

    class Location: Object {
        dynamic var latitude: Double = 0.0
        dynamic var longitude: Double = 0.0
    
        convenience init(latitude: Double, longitude: Double) {
            self.init()
            self.latitude = latitude
            self.longitude = longitude
        }
    }
    

    위치를 표현하기 위해 latitudelongitude 를 선언하였고, 편의를 위해 convenience initializer 를 추가했습니다.

  3. Santa.swift 은 이제 컴파일 가능한 상태가 되었습니다. 좀 더 진행하기 전에 Santa 모델을 약간 수정하도록 하겠습니다. currentLocation 을 optional 로 선언했었죠? 코드에서 currentLocation 을 사용할 때마다 언래핑(unwrapping)하고 싶지 않네요. 특히 산타의 기본 위치를 알고 있는 상태라면 더더욱 그렇습니다. Santa.swift 를 다음과 같이 수정하도록 하겠습니다.

    class Santa: Object {
        private dynamic var _currentLocation: Location?
        var currentLocation: Location {
            get {
            	// 산타가 어디있는지 알지 못한다면, 산타는 여전히 그의 집에 있을 것으로 추정됩니다.
                return _currentLocation ?? Location(latitude: 90, longitude: 180)
            }
            set {
                _currentLocation = newValue
            }
        }
           
        override static func ignoredProperties() -> [String] {
            return ["currentLocation"]
        }
    }
    

    currentLocation 속성이 non-optional 로 변한 것 외에는 Santa 의 API 는 변동사항이 없습니다. Realm Swift 문서에 따르면, Object 속성(여기서는 Location 을 말함)은 “반드시 optional” 이어야 합니다. 때문에 Swift 표준 방식으로는 기본값을 설정할 수도 없고 non-optional 로 만들 수도 없습니다. 이러한 문제점을 해결하기 위해 private 변수 하나를 추가로 선언하고 public 변수에서 우리가 원하는 동작을 처리하는 방식을 선택했습니다. 또한, Realm 이 currentLocation 속성을 저장하지 않도록 하기 위해 ignoredProperties 를 사용했습니다. (Realm이 read-only 속성의 경우 자동으로 저장하지 않지만, 우리 예제의 경우 setter가 있으므로 ignoredProperties 를 사용했습니다.)

  4. 테스트를 위해 아래의 데모 데이터를 추가하겠습니다.:

    extension Santa {
        static func test() -> Santa {
            let santa = Santa()
            // 지도상에서 빠른 확인을 위해 서울 좌표로 설정했습니다.
            santa.currentLocation = Location(latitude: 37.566535, longitude: 126.977969)
            return santa
        }
    }
    

    자습서에서는 Santa 가 샌프란시스코에 있다고 설정했는데 여러분이 살고 있는 곳의 좌표를 확인하여 설정 위치를 변경해도 됩니다.

    Sidebar: 위도와 경도를 헷갈리지 마세요. 여기에 도움이 될만한 다이어그램이 있습니다! 만약 지도상에서 여러분의 예상했던 위치에 pin 이 표시되지 않는다면 위도 경도를 먼저 확인하길 바랍니다.

8. 지도상에 산타 위치 표시하기!

  1. 지도 표기와 관련된 기능들을 처리하기 위해 이들을 다루는 함수를 작성하도록 하겠습니다. MapManager.swift 라는 Swift 파일을 작성합니다:

    class MapManager: NSObject {
        private let santaAnnotation = MKPointAnnotation()
    
        init(mapView: MKMapView) {
            santaAnnotation.title = "🎅"
            super.init()
            mapView.addAnnotation(self.santaAnnotation)
        }
    
        func update(with santa: Santa) {
            // 산타의 새로운 위치를 지도상에 나타내기 위해 위치를 업데이트 합니다.
        }
    }
    

    map 관련 컴파일러 에러를 방지하기 위해 import MapKit 구문을 추가해야 합니다. MapManager 은 익숙한 pin UI 로 지도 상에 특정 위치를 표현하는 MKPointAnnotation 을 다룰 것입니다. MapManager 를 생성할 때 annotation 을 생성할 것이며, 이것을 지도에 추가할 것입니다.

  2. pin 이 어디에 위치해야 하는지 어떻게 알려주고, 어떻게 위치를 업데이트 해야할까요? 언제 업데이트해야 하는지 아는 것은 MapManager 의 역할이 아닙니다. 다른 것이 언제 업데이트를 해야하는지 알려주고, 이 정보를 Santa 가 활용할 수 있게 넘겨주는 역할을 담당해야 합니다. 마지막으로 새로운 위치에 대해 annotation 의 coordinate 속성을 업데이트해야 합니다.

    하지만 여기서 몇 가지 문제가 발생합니다. Santa 의 위치는 Location 을 사용하고 MKPointAnnotationCLLocationCoordinate2D 를 사용한다는 점입니다. 또한, update(with:) 가 메인 쓰레드가 아닌 다른 쓰레드에서 호출될 경우 크래쉬되는 문제가 있습니다. 위치 변경은 UIKit 에서 호출해야 하며 UI 업데이트는 메인 쓰레드에서만 처리할 수 있기 때문입니다. (이 문제와 관련된 이슈는 다음 번 포스팅에서 좀 더 상세히 다룰 예정입니다.)

    가장 먼저 할 일은 LocationCLLocationCoordinate2D 로 변환하는 것입니다. Location 에 다음과 같이 extension 을 추가하겠습니다:

    private extension Location {
        var clLocationCoordinate2D: CLLocationCoordinate2D {
            return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
        }
    }
    

    함수의 이름은 UIColor.cgColor 과 같은 형태를 따랐으며, 이 함수는 두 개의 숫자를 복사하는 역할을 합니다. MapManager.swift 에서만 이 extension 이 필요하기 때문에 MapManager.swift 파일에 이 extension 을 추가하도록 하겠습니다. Location.swift 파일에 import CoreLocation 구문을 추가하지 않아도 된다는 뜻입니다.

    쓰레드 문제도 coordinateDispatch.main.async 안으로 이동시키면 쉽게 해결할 수 있습니다.

    update(with:) 를 아래와 같이 수정해봅시다:

    func update(with santa: Santa) {
        let santaLocation = santa.currentLocation.clLocationCoordinate2D
        DispatchQueue.main.async {
            self.santaAnnotation.coordinate = santaLocation
        }
    }
    

    문서에 따르면, Santa 를 생성한 쓰레드 외의 쓰레드에서는 Santa 에 접근할 수 없기 때문에 Santa 를 직접 메인 쓰레드로 넘겨줄 수는 없습니다. 그래서 우리가 필요한 데이터만 얻은 다음 그것을 넘겨주는 방식을 사용했습니다.

  3. 이제 manager 가 Santa 를 지도에 표시할 수 있게 되었습니다. SantaTrackerViewController 를 수정해봅시다:

    // Has to be implicitly unwrapped
    // Needs the reference to the map view
    private var mapManager: MapManager!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        // Set up the map manager
        mapManager = MapManager(mapView: mapView)
    
        // Find the Santa data in Realm
        let realm = try! Realm()
        let santas = realm.objects(Santa.self)
    
        // Set up the test Santa if he's not already there
        if santas.isEmpty {
            try? realm.write {
                realm.add(Santa.test())
            }
        }
    
        // Be responsible in unwrapping!
        if let santa = santas.first {
            // Update the map
            mapManager.update(with: santa)
        }
    }
    

    Realm 모델을 사용하고 있으므로 파일 상단에 import RealmSwift 구문을 추가해야 합니다. map view 에서 참조하기 위해 mapManager 를 implicitly unwrapped 로 선언하였습니다. (다른 방법도 있지만 이어지는 포스팅에서 이같은 설정이 필요하기 때문에 설정을 그대로 유지하길 바랍니다.)

    그 다음 Realm 을 열어 데이터가 설정되어 있지 않은 상태라면 테스트 데모 데이터를 설정하고 지도 업데이트를 위해 첫번째 Santa 를 넘겨줍니다. 다음 번 포스팅에서는 Realm 과 관련된 에러 처리 부분을 개선할 예정이니 여기에서는 이 부분에 너무 신경쓰지 않아도 됩니다.

    이 시점에서 앱이 크래쉬 되는 원인은 다음과 같습니다.:

    • 정확한 프레임워크를 import 구문을 사용하여 알맞은 파일에 추가하였나요?
    • @IBOutlet 을 적절히 연결하셨나요?
    • 위에서 사용된 코드를 정확히 입력하셨나요? 복사하기가 아니라 코드를 참고하여 프로그램 코드를 작성한 경우, 코드를 정확히 입력하였는지 다시 한번 확인해주세요.

    앱을 실행하여 원하는 위치에 pin 이 나타났다면 축하드립니다! 첫번째 포스팅을 잘 마쳤습니다! 맛있는 음식을 즐기며 다음 주를 기대해주세요. 다음 주에는 산타의 전체 경로를 화면에 보여줄 것입니다.


두 번째 포스팅의 번역이 진행되고 있습니다. 궁금하신 분은 영어 원문 을 확인해 주세요!

번역: 문상준 iOS 개발자. Reactive, Functional programming 과 Swift 관련 글쓰기에 관심이 많습니다. 현재는 Vandad Nahavandipoor 가 진행하는 crowdsource swift book 프로젝트 에서 열심히 활동하고 있습니다. 관심있는 분들의 참여는 언제나 환영입니다.

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.


Michael Helmbrecht

Michael은 주간에는 디자이너, 야간에는 iOS 개발자로 활동합니다. 현재 Realm에서 최고의 디자인을 만들기 위해 디자인과 iOS 개발, 두 분야에서 공헌하고 있습니다. 또한 세계에서 가장 큰 Swift 모임인 SLUG의 저명한 주최자이기도 합니다.

4 design patterns for a RESTless mobile integration »

close