AppleDeveloperAcademy@POSTECH/Retrospect

[MC2] SwiftUI로 Location, Map을 많이 만져볼 수 있었던 프로젝트

여의도사노비 2022. 12. 15. 13:05
728x90

[ 오프라인 시작 ]

MC2 프로젝트는 오프라인으로 시작했다. 첫 오프라인인지라 온라인과 많이 다를거라 생각했는데... 공통점은 새로운 사람들을 단기간에 너무 많이 만나면 나같은 I 성향이 더 강한 사람은 금방 체력을 잃는다는 것 ^^

 

 

[ 프로젝트를 시작하며 ]

MC2는 각자 관심있는 키워드로 진행되었다. 우리팀은 NFT, 사진, 캠핑, 공유옷장 등의 키워드들을 선호한 사람들이 뭉쳤다.

이 부분에선 굉장히 많은 기대가 있었다. 특히 나는 여러가지 관심있는 주제들 중 NFT에 관심이 많았다. 하지만 5주도 안되는 기간동안 성과를 낼 수 있는 앱을 만들기엔 NFT라는 주제가 너무 어려워서 팀원들끼리 여러번 고민하다가 주제를 바꿨다.

다시 아카데미로 돌아간다면 나는 MC1, MC2, MC3 프로젝트를 진행하면서 절대 그 기간안에 괜찮은 앱을 만들어야겠다는 생각은 안할것 같다. 아예 길게보고 확장성 있는 앱을 기획해놓고 사이드로 발전시킨다던가, 그 기간동안은 그냥 무리하지말고 개발 공부에 최선을 다할 것 같다.

 

[ 기획을 진행하며 ]

각자 관심있는 주제가 정해진 만큼 이야기의 방향성은 명확했다. 하지만 방향이 명확한 만큼 더 많은 이야기가 오갔다. 우리 프로젝트의 기간은 정해져있었고 단순 데스크 리서치로 끝낼 수 있는 주제들이 아니다보니 기획에 시간이 오래 걸렸다. 결국 여러가지 주제를 두고 끊임없이 마인드맵을 그려가다가 서로 타협할 수 있는 부분까지 도달하게 되었다. 다들 드라이브에 관심이 있었고, 드라이브를 하면 기분 전환에 도움이 된다는 것에 공감을 했다.(물론 너무 지쳐서 포기한걸지도 ;)...)

 

실제 드라이브를 하며 기분전환을 하는 유저들을 상대로 인터뷰도 진행했고 페인포인트도 찾아보았다.

우리가 찾아낸 페인포인트는 이 정도로 추릴 수 있었다. 

  1. 드라이브 가고 싶은데 막상 어디로 출발해야할지 모르겠다.
  2. 드라이브를 하며 들렀던 장소를 다시 가고 싶은데 어딘지 생각이 안난다.
  3. 내가 오늘 하루 드라이브 했던 경로를 다시 보고 싶다.

이에 맞는 솔루션을 고민하기 위해 MVP(최소기능제품)은 어떤 문제에 초점을 맞출지 선정해야했다.

 

첫 번째 문제는 지역 기반의 큐레이팅이 필요했다. 장소를 가보거나 리뷰들을 모아서 추천을 해줘야하는데 상당히 고된 일이라 포기했다.

두 번째와 세 번째 문제는 한 번에 엮어서 생각할 수 있었다. 우연찮게 들른 장소를 쉽게 기록할 수 있고, 드라이브가 끝난 뒤 자동으로 내가 다녀온 경로들을 나이키 런처럼 표시해주면 되겠구나라는 결론에 다달았다.

 

그래서 우리는 애플의 카플레이 기능을 이용하여 차를 운전하다가 우연찮게 마주한 장소, 건물 등을 쉽게 기록할 수 있도록 설계했다.

또한 드라이브를 시작하기 전에 버튼을 누르면 아이폰의 위치추척 기능으로 유저의 위치가 자동으로 지속적으로 기록되고, 이 기록을 모아서 지도에 하나의 선으로 표시하는 기능을 설계했다.

 

다양한 논의들이 오갔고 기능에 대한 정의를 마쳤다

 

기획을 진행하며 생각나는 일들
  • 유저 인터뷰는 잘 짜여져야 한다.
    • 우리 서비스의 긍정적인 면만 부각시키려는 답이 정해진 인터뷰는 의미가 없다.
    • Interviewee에게 던져진 애매한 질문은 애매한 답으로 돌아온다.
  • 유사 서비스가 있는데 자리를 못잡았다면, 우리는 어떻게 차별점을 둘까?
    • 결국 비슷한 기능이라면 UI/UX가 가장 중요하다. 그리고 그 다음으로는 마케팅인것 같다.
    • '크게 다른 기능이 아님에도 불구하고 우린 조금 다르다. 이 조금의 차이가 굉장히 큰 차이다'라는 것을 어필할 줄 알아야 한다.

 

[ 디자인을 진행하며 ]

우리팀의 디자이너는 3명이었다. 근데 사실 난 3명이라는 이야기를 듣고 걱정이 앞섰다. 기획하는 사람들이 3명만 뭉쳐도 자기 생각에 고착화되어 프로젝트를 진행하기가 상당히 힘든데, 디자이너라면 자신만의 확고한 디자인 주관이 있을테고... 분명 마찰이 있지 않을까 싶었다. 실제로 크진 않지만 작은 마찰들이 존재했다. 사실 당연히 존재할만한 마찰이었지만, 문제는 나에게도 있었다. 모든 디자이너의 의견을 조금씩이라도 프로젝트에 담고 싶었던 것이다. 이는 나뿐 아니라 디자인 분야가 아닌 다른 분들에게도 적용되는 이야기였다. 결국 그렇게 누구하나 기분 나쁘지 않기 위해 최선을 다해서 의견들을 모아 디자인하다보니... 약간은 복합적인(?) 디자인이 나온 것 같다 ;)

 

디자인을 진행할 때 가장 고민한 것은 '유저가 운전할 때 어떻게 기록 버튼을 누를 것인가'였다. 이 부분에 대해서 사용자가 쉽게 버튼에 접근할 수 있어야하고, 운전하면서도 신경쓰이지 않도록 간편하게 작동해야했다. 그래서 우리는 버튼을 최대한 크게 보이고, 기록 버튼 외에 다른 버튼은 과감히 보이지 않게 디자인했다. 카플레이 기능을 이용할 때도 현재 위치가 뜬다던지 부가적인 기능을 제외하고 버튼만 떠서 운전을 방해하지 않도록 디자인했다.

또한 앱 이용시 사용자의 화면 depth에도 신경을 썼다. 특정 기능까지 도달하는데 필요한 터치 횟수를 최소한하기 위해 노력하고 한 화면 내에 직관적으로 특정 기능으로 넘어 갈 수 있도록 디자인 했다.

 

프로토 타입 디자인: 카플레이 디자인은 정말 최대한 심플하게 가려고 했다.

 

디자인을 진행하며 생각나는 일들
  • 디자인 시스템이 있으면 편리하다는 것을 처음 알았다
    • 결국 공통으로 사용되고, 재사용되는 컴포넌트들의 경우 공통으로 만들어두고 조금씩 변형을 주는게 편하다.
    • 글씨체부터 색감까지 전부 컨트롤이 되고 있어야 디자인이 산으로 가지 않는 다는 것을 깨달았다.
  • 디자이너와 소통하는 것이 생각보다 더 어렵다.
    • 제일 어려운 것은 내 마음에 들지 않는 디자인을 상대방이 정말 열심히 준비해주었을 때이다.
    • 심지어 마음에 들지는 않지만 누구나 인정하는 근거로 상대방을 설득할 수도 없을 때이다.
    • 이런 경우 결국 이 디자인을 유저가 마음에 들어하느냐에 대해 검증해야하는데 시간은 한정적이고... 정말 여전히 어려운 문제인 것 같다.

 

[ 개발을 진행하며 ]

이제 UIKit이 뭔지 SwiftUI가 뭔지 구분이 되는 시기쯤이었다. 물론 아직 앱의 생명주기라던지, 데이터를 전달하는 방식이라던지... 이런 부분에 대해서는 하나도 몰랐다. 그럼에도 불구하고 나름 열심히 코드를 뜯어보고 Swift 공부를 한 시기인거 같다.

 

우리 팀의 개발자는 총 3명이었다. 심지어 단 한 명의 전공자나 Swift 경험자가 없었기에... 우린 험난한 개발 여정이 될 것임을 인지했다.

애플 카플레이는 너무 생소한 개념이라 제일 잘하는 코더분이 역할을 맡았고 도메인 분야에 오랜 업무를 맡았던 팀원 분이 데이터 저장을 위한 CoreData 기능을 맡으셨다. 그리고 내가 Map, Location과 같은 기능을 맡았다. 부담감이 상당했지만 한 편으로는 내가 앱으로 사용하던 TMap이나 네이버 지도 같은 서비스에 들어가는 기능을 다뤄볼 수 있다는 점에서 굉장히 재밌어보였다.

 

이때 정말 유튜브와 구글을 많이 사용했다. SwiftUI는 다행히 최신 기술인만큼 여러 유튜버가 본격적으로 다루고 있어 Core Locaiton, MapKit 등에 대해 레퍼런스를 참고하기 좋았다. 하지만 레퍼런스는 레퍼런스 일뿐... 정작 그 코드를 내가 온전히 이해하고 다시 변형해서 우리가 원하는 형태의 기능으로 재정의하는 과정이 너무나 어려웠다.

 

특히 MapKit을 활용할 때 UIKit 레거시를 불러와야하고 NS라는 개념이 타입 앞에 붙는 경우도 종종 있어서 도대체 이게 무슨 뜻인가... 싶은 코드들이 많았다. 하지만 구글은 정말 모든 정보를 알려주었고, 다행히 자신이 현재 있는 위치의 위도와 경도를 불러오는 MKCoordinateRegion()이나 Location 정보를 불러올 때 필요한 권한 설정 등의 기능을 세팅할 수 있었다. 그리고 데이터를 주고 받는 @State, @ObservableObject 등의 개념에 대해서도 알 수 있었다.(근데 아직도 너무 어렵다...)

 

CoreData의 경우 결국 프로젝트 막바지에 다달아 연결을 성공시켰고 CarPlay의 경우 애플로부터 승인을 받았다. 특히 CarPlay의 경우 처음 알게된 사실이, 차를 사용하며 써야하는 위험한 기능이기 때문에 애플에 어떤 이유로 이 프레임워크를 사용할 것인지에 대해서 설명해야하고 이를 확인받아야한다. 우리는 엄청 단순하게 "버튼 클릭 시 현재 위치 저장"을 이유로 신청했고 프로젝트가 끝나기 몇일 전 겨우 승인이 떨어졌다.(결국 앱에 카플레이 연동을 시키진 못했다.)

 

이때 주변에 코딩을 잘하는 분들의 도움도 많이 받았었는데... 아무리 생각해도 비개발전공자에게 프로젝트 + 방목형은 쉽지 않은 학습 방법이다. 어느정도 기반이 있는 분들은 프로젝트에서 나온 기능을 만들기 위해 이 UI를 구현하기 위해 UIViewController를 쓰고, ScrollView를 쓰고 등등 화면별로 필요한 기술 스택을 정리하면서 시작했다. 이 모습을 보니 주먹구구식으로 당장 기능 구현만을 목표로 최대한 입맛에 맞는 코드를 찾아다니는 과정이 긍정적으로 보이진 않았다.

 

그럼에도 불구하고 SwiftUI 관련하여 많은 개발 경험을 쌓을 수 있었던 시간이었다. 

 

import SwiftUI
import MapKit

struct LocationDetailView: View {
    @EnvironmentObject private var vm: LocationsViewModel
    let location: Location
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 8) {
                basicInfo
                Divider()
                mapLayer
                Divider()
                memoField
            }
        }
        .background(.ultraThinMaterial)
        .overlay(backButton, alignment: .topLeading)
    }
}
    
extension LocationDetailView {
    private var basicInfo: some View {
        HStack {
        VStack {
            Image("restaurant")
            .resizable()
            .frame(width: 100, height: 100)
            .padding()
            Text("식당")
                .padding()
        }
            VStack {
                Spacer()
                Text("제목을 입력하세요")
                    .padding()
                Spacer()
                Text("2022-06-13")
                    .padding()
            }
            Image(systemName: "star")
                .padding()
                .offset(y: -30)
        }
    }
    
    private var mapLayer: some View {
        Map(coordinateRegion: .constant(MKCoordinateRegion(center: location.coordinates, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))),
            annotationItems: [location]) { location in MapAnnotation(coordinate: location.coordinates) {
            LocationMapAnnotationView()
                .shadow(radius: 10)
            }
        }
            .allowsHitTesting(false)
            .aspectRatio(1, contentMode: .fit)
            .cornerRadius(30)
            .padding()
    }
    
    private var memoField: some View {
        Rectangle()
            .frame(width: 330, height: 300)
            .foregroundColor(.white)
            .border(.black, width: 1)
            .offset(x: 15)
            .padding()
            .textFieldStyle(.plain)
    }
    
    private var backButton: some View {
        Button {
            vm.sheetLocation = nil
        } label: {
            Image(systemName: "xmark")
                .font(.headline)
                .padding(16)
                .foregroundColor(.accentColor)
                .background(.thickMaterial)
                .cornerRadius(10)
                .shadow(radius: 4)
                .padding()
        }
    }
}

struct LocationDetailView_Previews: PreviewProvider {
    static var previews: some View {
        LocationDetailView(location: LocationsDataService.locations.first!)
            .environmentObject(LocationsViewModel())
    }
}

MKCoordinateRegion, @EnvironmentObject 등을 활용...

 

import Foundation
import MapKit
import SwiftUI

class LocationsViewModel: ObservableObject {
    @Published var locations: [Location]
    @Published var mapLocation: Location {
        didSet {
            updateMapRegion(location: mapLocation)
        }
    }
    
    @Published var mapRegion: MKCoordinateRegion = MKCoordinateRegion()
    @Published var showLocationsList: Bool = false
    @Published var sheetLocation: Location? = nil
    let mapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    
    init() {
        let locations = LocationsDataService.locations
        self.locations = locations
        self.mapLocation = locations.first!
        self.updateMapRegion(location: locations.first!)
    }
    
    private func updateMapRegion(location: Location) {
        withAnimation(.easeInOut) {
            mapRegion = MKCoordinateRegion(
                center: location.coordinates,
                span: mapSpan)
        }
    }
    
    func toggleLocationsList() {
        withAnimation(.easeInOut) {
            showLocationsList = !showLocationsList
        }
    }
    
    func showNextLocation(location: Location) {
        withAnimation(.easeInOut) {
            mapLocation = location
            showLocationsList = false
        }
    }
    
    func nextButtonPressed() {
        guard let currentIndex = locations.firstIndex(where: { $0 == mapLocation }) else {
            print("Could not find current index in locations array! Should never happen.")
            return
        }
        
        let nextIndex = currentIndex + 1
        guard locations.indices.contains(nextIndex) else {
            guard let firstLocation = locations.first else { return }
            showNextLocation(location: firstLocation)
            return
        }
        
        let nextLocation = locations[nextIndex]
        showNextLocation(location: nextLocation)
    }
}

ViewModel을 만들때가 가장 힘들었던것 같다... 이해가 잘 안되고 코드간의 유기성을 따져봐야하는데 정말 하루종일 비슷한 내용만 내내 찾아본듯

 

개발을 진행하며 생각나는 일들
  • 개발 시간이 부족했다
    • 여전히 개발 시간이 부족했다.
    • 어쩔 수 없다는 생각이 들면서도 항상 아쉬웠던 부분
  • 기초가 너무 없다
    • 다른 사람이 만든 코드를 보고 View와 ViewModel의 차이를 인지하고, Struct에는 init을 선언할 필요가 없고 Class는 init을 선언해야한다는 사실에 대해서 알게 됐다.
    • 이렇게 코드를 파고 분석하다보니 어느새 5일 뒤 앱을 선보여야한다.
    • 결국 코드를 복사하고 최대한 디자인에 맞게 변형만 시킨다.
    • 이 과정이 반복되다 보니 만들어놓고 나서도 100% 이해가 안간다.
    • 아카데미 수료 후 더 깊게 공부해서 나만의 것을 만들고자 다짐했던 시기다.
  • 협업이 아닌 개인 작업을 했다.
    • Github을 썼지만 그냥 나 혼자 로컬로 코드 짜서 커밋하고 푸쉬만 한 수준이었다.
    • 개발자는 결국 협업을 잘해야하는데 그 부분에 대해서 전혀 고민하지 못했던 시기다.
    • 머지 개념도 부족하고 푸쉬&풀의 개념도 많이 부족했다.

 

[ Mini Challange 2를 마치며 ]

개발자로의 전환을 맞이한 시기이다.

 

그래도 내가 이거 개발했어. 라고 이야기 할 수 있었다. 내가 100% 코드를 이해하고 짠 것은 아니지만, 일정 기능은 돌아가게 만들었고 화면간 연결도 했다. 누군가에겐 별거 아니지만 나에게는 새로운 시작과 같았다. 하지만 한 편으로는 벌써 두개의 프로젝트가 끝났는데 아직 개발 실력이 너무나 부족하다는 것을 깨달았기 때문에 이 이후에는 더 많은 공부가 필요하다는 것을 깨닫기도 했다. 

또한 열심히 배우고 있는 SwiftUI가 최신 기술이다보니 많은 기업들은 아직 UIKit과 Objc를 기준으로 개발하고 있었고, 인력도 이 둘을 더 잘다루는 사람을 선호했다. 그래서 이 프레임워크를 계속 공부해나가야할지 지금이라도 UIKit을 빠르게 공부해야할지도 많은 고민이 들었다. 그리고 MC3 프로젝트를 진행하는 동안 UIKit을 사용해보아야겠다는 결론에 다달았다.

 

SwiftUI or UIKit

 

깃허브(Github): https://github.com/DeveloperAcademy-POSTECH/chagokchagok