Swift와 iOS 개발에서 배운 것들
원문은 Ellie Huxtable님이 에 게재했습니다. 이 블로그 구독하기
먼저 말씀드리고 싶은 건 저는 전혀 전문가가 아니라는 점입니다. 프로그래밍은 오래 해왔지만 Swift와 iOS 세계는 처음이고, 모바일 앱을 만들어본 적도 없습니다 :) Pillion의 첫 iOS 버전을 만들면서 마주치고 배운 모든 것을 정리해 봤습니다!
Pillion은 오토바이 주차 공간을 찾는 것을 돕는 앱입니다 - 전 세계에 약 10,000개의 주차 위치가 등록되어 있는데, 일부는 OpenStreetMaps에서, 일부는 온라인 데이터셋에서, 나머지는 사용자들로부터 수집한 것입니다 주차 외에도 라이딩 기록 기능도 제공합니다
이 중 상당수는 아직 이것저것 알아가던 중에 발견한 것들이라, 많은 경우 ‘올바른’ 방식으로 하고 있지 않았습니다. 더 나은 방법을 알게 된 부분은 두 가지 접근 방식을 함께 적어 두었습니다.
반응이 좋다면 나중에 각 섹션(특히 MapBox)에 대해 더 자세히 다룬 글을 쓸 수도 있습니다.
혹시 틀린 부분이 보이면 알려주세요 😊
SwiftUI
저는 Apple이 iOS 13과 함께 출시한 새로운 UI 시스템인 SwiftUI를 바로 도입했습니다. 단, iOS 13 이상을 실행하는 기기만 지원된다는 제약이 있습니다 - 다만 이는 iPhone 6S 이후의 모든 기기에 해당합니다.
수년간 React를 써왔기 때문에 SwiftUI가 아주 편하게 느껴졌습니다. 차이점은 있지만, 컴포넌트/자식 요소/state를 다루는 방식이 자연스럽게 느껴집니다. 사실 React의 state 처리 방식보다 state 바인딩이 더 마음에 들고, 코드도 훨씬 깔끔하게 느껴집니다.
UIKit은 써본 적이 없어서 비교할 건 많지 않지만, 제가 본 SwiftUI는 훨씬 깔끔합니다.
SwiftUI를 쓰면서 몇 가지 문제를 겪었는데, 대부분 아직 미성숙해서 생긴 문제들로 보입니다 - 아래에서 다루겠습니다.
짜증나게도 GitHub에서 찾을 수 있는 괜찮은 UI 컴포넌트들은 대부분 UIKit 전용이었습니다. 언젠가 래핑을 시도해 볼 수도 있겠지만, SwiftUI 생태계는 아직 다소 빈약한 편입니다 - 그 미성숙함을 잘 보여줍니다. 염두에 둘 점입니다.
디버깅
XCode 디버거는 꽤 반가운 놀라움이었습니다! Python에서는 ipdb 세션에 익숙해져 있었는데, 코드가 휴대폰에서 컴파일되어 실행되는 주제에 이렇게 유연할 줄은 기대하지 못했습니다.
디버거는 값을 들여다보고, Swift 표현식을 평가하고, 제가 원하는 모든 걸 문제없이 해냈습니다! 탭 자동완성도 아주 잘 동작했습니다. 툴링 만세 :)
가끔 조금 느릴 때도 있지만, 뭐 그 정도는 예상할 수 있는 일이죠.
UI 스레드 관련
제 앱의 상당 부분은 본질적으로 API에 요청을 보내고 그 결과를 렌더링하는 것입니다. 백그라운드 스레드(예를 들어 HTTP 응답의 비동기 핸들러)에서 UI를 업데이트하려고 하면 오류가 발생한다는 걸 알게 됐습니다. 대신 다음과 같은 패턴을 사용해야 합니다:
DispatchQueue.main.async {
// do ui
}이렇게 하면 UI 스레드를 블로킹하지 않기가 아주 쉬워집니다 😁
사실 이건 Swift를 쓰기 시작한 아주 초기에 읽었던 내용입니다. 그 후로는 데이터 페칭과 업데이트에는 ObservableObjects와 Published 속성을 쓰는 게 더 정석이라는 걸 알게 됐습니다, 가능한 경우라면요! 그래도 혹시 필요할까 싶어 남겨둡니다. 아직 제 프로젝트에서 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
이 프로젝트의 상당 부분에서 MapBox Maps SDK for iOS를 사용했습니다. Mapbox는 JavaScript에서는 작업하기가 즐거웠지만, 솔직히 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의 body를 일종의 작은 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가 아주 새롭고/beta 단계이니 놀랍지도 않습니다. 이런 문제들을 제외하면 작업하는 게 정말 즐겁습니다. 될 때는 정말 잘 됩니다! 안 될 때가 문제죠 ;)
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 계정에 가입하는 데는 일주일에서 이주 정도 걸렸습니다 - 온라인 양식을 작성하는 건 빨랐지만, 승인까지 시간이 좀 걸렸고 전화까지 받아야 했습니다. 그 이후로는 다 순조로웠습니다!
Fastlane으로 스크린샷을 찍어보려 했지만, 시뮬레이터/테스트가 잘 동작하지 않아서(Mapbox가 UI 테스트에서 크래시) 결국 직접 몇 장을 수동으로 찍는 방법밖에 없었습니다. 별로 좋지 않고, 개선하고 싶긴 합니다. 언젠가는 해결하겠죠. 제 노트북이 너무 느리고 좀 오래된 것도(주로 RAM 부족) 도움이 안 돼서, 시뮬레이터가 정말 형편없습니다.
첫 App Store 제출 후에 앱 기능이 충분하지 않다는 통보를 받았습니다. 아야. 조사해 보니 웹 앱으로도 만들 수 있는 것들은 Apple이 승인을 그다지 내켜하지 않는 것 같습니다. 그래서 로드맵에 있던 몇 가지를 앞당겼습니다.
실제로 제 앱의 한 버전은 전체 심사 과정(제가 "제출"을 누른 순간부터 라이브가 되기까지)이 약 7분 만에 끝난 적도 있는데, 정말 인상적이었습니다.
결론
돌이켜보면 예전 웹 버전과 같은 기술을 그대로 쓰기 전에 다른 솔루션들을 좀 더 알아봤을 것 같습니다. 어쩌면 MapKit으로도 충분했을 텐데 MapBox를 쓰지 않았을 수도 있죠.
하지만 이 과정에서 정말 많이 배웠기 때문에 크게 바꾸고 싶진 않습니다. 곧 같은 걸 Android용으로도 만들기 시작할 예정인데(그리고 Android 개발도 아직 잘 모릅니다 ;P) 어떻게 될지 지켜봐 주세요! 이번과 비슷한 글을 기대해 주세요.
어쨌든, Pillion이 이제 App Store에 올라왔습니다!
바이크를 타신다면 한번 써보시고 어떠셨는지 알려주시면 좋겠습니다 - 언제든 편하게 연락 주세요!
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기