Swift와 iOS 개발을 하며 배운 점
먼저 말해 두고 싶은 건, 저는 절대 전문가가 아니라는 점입니다. 프로그래밍을 한 지는 오래됐지만 Swift와 iOS 세계에는 새로 왔고, 모바일 앱을 만들어 본 적도 없었습니다 :) 이것은 Pillion의 첫 iOS 버전을 만들면서 우연히 마주치고 배운 모든 내용입니다!
Pillion은 오토바이 주차 공간을 찾는 데 도움을 주는 앱입니다. 전 세계에 주차 위치를 아마 10,000곳 정도 등록해 두었는데, 일부는 OpenStreetMaps에서 가져왔고 일부는 온라인 데이터셋에서 가져왔으며 나머지는 사용자들이 추가했습니다. 주차 외에도 라이딩 추적 기능을 제공합니다.
아직 여러 가지를 알아가는 중에 발견한 내용이 많아서, 상당수의 경우 처음에는 "올바른" 방식으로 하지 않았습니다. 더 나은 방법을 알게 된 부분은 두 접근법을 모두 넣었습니다.
반응이 좋다면 나중에 각 섹션, 특히 MapBox를 더 자세히 다루는 글을 쓸 수도 있습니다.
실수를 발견하시면 알려 주세요 😊
SwiftUI
Apple이 iOS 13과 함께 출시한 새 UI 시스템인 SwiftUI를 바로 사용하기 시작했습니다. 여기서 주의할 점은 iOS >= 13을 실행하는 기기만 지원한다는 것입니다. 그래도 iPhone 6S 이후의 모든 기기가 해당합니다.
React를 몇 년간 사용해 와서 그런지 SwiftUI는 아주 좋게 느껴집니다. 차이점은 있지만 컴포넌트/자식/state가 자연스럽게 느껴지는 방식으로 구성돼 있습니다. 사실 state 바인딩은 React의 state 처리 방식보다 더 마음에 들고, 코드도 꽤 더 깔끔하게 느껴집니다.
UIKit은 써 본 적이 없어서 비교할 건 별로 없지만, 지금까지 본 SwiftUI는 훨씬 깔끔합니다.
SwiftUI의 미성숙함과 주로 관련 있어 보이는 문제를 몇 개 겪었는데, 아래에서 다룹니다.
짜증 나게도 GitHub에서 찾을 수 있던 멋진 UI 컴포넌트 대부분은 UIKit 전용이었습니다. 언젠가 래핑해 볼 수도 있겠지만 SwiftUI 생태계는 조금 빈약해 보이고, 그 미성숙함을 아주 잘 보여 줍니다. 염두에 둘 점입니다.
디버깅
XCode 디버거는 기분 좋은 놀라움이었습니다! Python에서 ipdb 세션을 쓰는 데 익숙하지만, 코드가 휴대폰에서 컴파일된 상태로 실행되는 걸 생각하면 이렇게 유연할 거라고는 예상하지 못했습니다.
디버거는 값을 문제없이 검사하고 Swift 표현식을 평가하는 등, 제가 원하던 모든 일을 할 수 있었습니다! 탭 자동 완성도 아주 잘 작동했습니다. 도구 만세 :)
가끔 조금 느리긴 하지만, 그 정도는 예상할 만하다고 생각합니다.
UI 스레드 관련
제 앱의 상당 부분은 본질적으로 API에 요청을 보내고 그 결과를 렌더링하는 일입니다. 백그라운드 스레드에서 UI를 업데이트하려고 하면(HTTP 응답의 비동기 핸들러 같은 경우) 오류가 발생한다는 걸 알게 됐습니다. 대신 다음 패턴을 사용해야 합니다:
DispatchQueue.main.async {
// do ui
}이렇게 하면 UI 스레드를 블로킹하지 않기가 아주 쉽습니다 😁
사실 이건 Swift를 사용하기 시작한 아주 초기에 읽은 내용입니다. 이후에는 가능하다면 몇몇 Published 프로퍼티가 있는 ObservableObjects를 써서 데이터를 가져오고 업데이트하는 편이 더 일반적인 방법이라는 걸 배웠습니다! 그래도 필요할 경우를 대비해 남겨 두었습니다. 제 프로젝트에서 HTTP 요청을 처리하는 방식은 아직도 그다지 만족스럽지 않아서, 아마 앞으로 다시 검토하게 될 부분입니다.
키보드 숨기기
작은 내용이지만, 특정 상호작용에서 키보드를 숨기고 싶었습니다. 예를 들어 검색창에 포커스가 잡힌 상태에서 목록의 항목을 터치하면 키보드를 숨기는 식입니다.
extension UIApplication {
func hideKeyboard() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}UIApplication.shared.hideKeyboard()Just
Swift에서 HTTP 요청을 만드는 일은 그리 나쁘지 않지만, 저는 Python의 requests에 익숙해서 비슷한 것을 찾았습니다.
Just가 딱 그런 라이브러리로 보였습니다! 작고 빠르며 좋고, 다음처럼 작성할 수 있습니다:
Just.get("https://example.com") { r in
print(r.text)
}...정말 좋습니다!
헤더가 미리 설정된 세션도 구성할 수 있어서, 인증이 필요한 HTTP 요청에 유용합니다.
Mapbox
이 프로젝트의 상당 부분에서 iOS용 MapBox Maps SDK를 사용하고 있습니다. JavaScript에서 Mapbox를 다루는 건 즐거웠지만, 솔직히 iOS 쪽은 조금 부족합니다. 커스터마이즈하기가 어색할 수 있고 문서도 썩 좋지 않습니다. 무언가를 하는 방법은 여러 개 찾았지만, 왜 그런지에 대한 설명은 거의 없었습니다. 그래도 Android 쪽은 조금 더 나은 것 같습니다!
나쁘다는 말은 아닙니다. JS 솔루션과 비교하면, 또 지금까지 본 Android 버전과 비교해도 약간 덜 완성됐거나 개발이 덜 된 느낌이라는 뜻입니다. Android 앱을 만든 뒤에 비교해 보겠습니다.
또 한 가지는 문서 일부가 UIKit에 더 맞춰져 있다는 점입니다. SwiftUI가 아주 새로우니 당연합니다! 그냥 염두에 둘 점입니다. SwiftUI용으로 지도를 래핑하는 방법을 보여 주는 문서도 있긴 합니다.
Swift 타입 검사
Rust를 쓰다가 오니 Swift 타입 체커는 조금... 부족하게 느껴집니다. 얼핏 명백해 보이는 타입을 아예 추론하지 못하거나, 너무 오래 걸린 끝에 개발자에게 코드를 좀 더 쪼개라고 요구하는 경우가 있습니다. 아주 큰 문제는 아니지만 조금 짜증 납니다. 그래도 계속 개선되고 있다고 합니다!
어떤 경우에는 문제가 전혀 없는 위치에 오류를 표시합니다. 코드를 지우면 실제 근본 문제가 드러납니다. 이건 SwiftUI DSL과 더 관련 있어 보입니다. DSL 안에서 타입 오류가 났을 때 타입을 추론하지 못하면 그냥 약간 무너져 버립니다. 지금은 좋은 예시를 찾지 못했지만, 찾으면 업데이트하겠습니다.
Catalina와 최신 XCode로 업데이트한 뒤에는 업그레이드 전에는 전혀 없던 이상한 "ambiguous member" 오류가 잔뜩 발생했습니다. Swift는 오해를 부르는 오류 메시지를 내놓는 경향이 있는 듯한데, 이건... 이상합니다. 특히 이렇게 큰 회사가 지원하는 언어 중에서는, 이런 식으로 동작하는 다른 현대 언어를 써 본 적이 없습니다.
전에 작성한 코드도 제가 아는 한 의미상 올바른 코드였지만, 결국에는 Swift가 이해할 수 있게 코드를 작성하는 데 더 능숙해졌습니다.
SwiftFormat
다른 프로젝트에서는 포매터를 사용하는 데 익숙해서 여기서도 찾아봤습니다. SwiftFormat은 잘 작동하는 것 같습니다.
SwiftUI DSL 문제
SwiftUI에서는 View 본문을 작은 DSL 비슷한 것으로 선언합니다.
var body: some View {
ComponentOne {
AnotherOne {
// so much nesting :O
}
}
}...그 자체로는 괜찮습니다.
하지만 조건부 렌더링을 하려고 하면 DSL은 아직 충분히 준비되지 않은 듯합니다.
if thing.ouch {
RenderMe(thing.ouch.thing)
}그런데 작성 시점에는 if let 같은 것은 작동하지 않습니다.
if let o = thing.ouch {
RenderMe(o.thing)
}필수는 아니지만, 있으면 좋겠습니다!
DSL에는 여기저기 아직 정리할 작은 문제가 몇 가지 더 있지만, SwiftUI는 아주 새롭고 베타 버전이니 놀랍지는 않습니다. 문제를 제외하면 작업하기 정말 즐겁습니다. 작동할 때는 아주 잘 작동합니다! 작동하지 않을 때가 문제일 뿐이죠 ;)
Auth0
현재는 Google + Apple oauth 로그인을 지원하는 Auth0 lock을 사용하고 있습니다. 사실 지금은 제 자체 솔루션을 대신 사용하고 있지만, 여전히 관련 있는 내용이라 남겨 두었습니다.
Apple 쪽은 정말 멋집니다! 사용자는 이메일을 숨기기 위해 Apple 전달용 주소를 선택할 수 있고, FaceID로 로그인할 수 있습니다.
Lock은 SwiftUI용으로 설정돼 있지 않았지만, 래핑하기는 꽤 쉽습니다:
struct Auth0Lock: UIViewControllerRepresentable {
typealias UIViewControllerType = LockViewController
var lock: Lock = Lock.passwordless().withOptions {
$0.passwordlessMethod = .magicLink
}
.onAuth { credentials in
print(credentials)
// save access token and use it to do stuff :)
}
.onError{ error in
print(error)
// you should probably do something other than just print errors
}
func makeUIViewController(context: UIViewControllerRepresentableContext<Auth0Lock>) -> LockViewController {
return lock.controller
}
func updateUIViewController(_ uiViewController: LockViewController, context: UIViewControllerRepresentableContext<Auth0Lock>) {
// there isn't really much updating I need to do atm
}
}Auth0에는 리프레시 토큰 처리, 새 액세스 토큰 요청, 토큰 저장 및 검증 등을 담당하는 credential manager가 들어 있습니다. 그래서 사용자를 로그인 상태로 유지하는 데 많은 노력을 들일 필요가 없습니다!
Auth0를 그만둔 이유를 조금 더 설명하자면, 인증 처리 방식에 더 많은 유연성이 필요했고 결국 백엔드를 Django 앱으로 다시 작성했습니다. 그래서 Django에 이미 있는 기능과 통합하기가 아주 쉬웠습니다. 제품을 더 빨리 만들고 재미있는 아키텍처 문제를 고민하는 데 시간을 쓰지 않기 위해 Django로 전환했습니다.
Apple 개발자
제 회사(Pillion Software Ltd)용 Apple Developer 계정에 가입하는 데는 아마 1~2주가 걸렸습니다. 온라인 양식 작성은 빨랐지만 승인까지 조금 시간이 걸렸고, Apple에서 저에게 전화도 해야 했습니다. 그 뒤로는 모두 괜찮았습니다!
스크린샷을 찍기 위해 Fastlane을 사용해 봤지만, 시뮬레이터/테스트가 잘 작동하지 않았습니다(UI 테스트에서 mapbox가 충돌했습니다). 그래서 실제로 가능한 유일한 방법은 몇 장을 직접 찍는 것이었습니다. 좋지는 않고, 해결하고 싶습니다. 언젠가 하겠죠. 제 노트북이 아주 느리고 조금 오래됐다는 점도 도움이 안 됩니다. 주로 RAM이 부족해서 시뮬레이터가 정말 엉망입니다.
처음으로 App Store에 제출했을 때, 제 앱에는 기능이 충분하지 않다는 말을 들었습니다. 아야. 좀 찾아보니 웹 앱이 될 수 있는 모든 것은 Apple이 승인하는 것을 그다지 좋아하지 않는 듯합니다. 그래서 로드맵의 일부를 앞당겼습니다.
실제로 제 앱의 한 버전은 전체 심사 과정, 즉 제가 "submit"을 누른 시점부터 출시될 때까지 약 7분 만에 통과했는데 정말 인상적이었습니다.
결론
돌이켜 보면, 예전 웹 버전에서 사용한 것과 같은 기술을 바로 쓰기 전에 다른 솔루션을 더 알아봤을 것 같습니다. 어쩌면 MapKit만으로도 충분했고 MapBox를 쓰지 않았을 수도 있습니다.
그래도 이 과정에서 많은 것을 배웠으니 크게 바꾸고 싶지는 않습니다. 곧 Android용으로도 같은 것을 만들기 시작할 예정인데(Android 개발도 아직 전혀 모르지만요 ;P), 어떻게 될지 보겠습니다! 이와 비슷한 글을 기대해 주세요.
아무튼, Pillion은 이제 App Store에서 이용할 수 있습니다!
오토바이를 타시고 한번 써 보신다면, 어떻게 생각하시는지 정말 알고 싶습니다. 주저하지 말고 연락해 주세요!
글을 무작위로 읽기