시리야 앱 배포해줘

시리 응답이 웰케 애잔하냐...

배경

올해 초 젠킨스 CI 서버 구축을 처음 해봤고, 그걸 계기로 자동화에 관심이 생겨 일 년 동안 틈틈이 팀 내 배포/개발 프로세스를 개선시켜왔다. 사실 관심이 생겼다는 말은 빈약한 표현이고 나는 내가 이 정도로 의미없는 반복 작업을 싫어하는 사람인줄 몰랐다. 아무 생각없이 할 수 있는 반복 작업도 가끔씩하면 정신 건강에 좋다고 생각했었다. 하지만 개발 프로세스에 녹아있던 수작업이 하기 싫어서 어떻게든 자동화할 방법을 찾아서 팀원들을 설득하고 바꿔버렸다. 그리하여 여러 사람의 손을 거쳐야했던 반복적이고 귀찮은 작업을 대부분 자동화했고, 이제는 시리도 우리 앱을 배포할 수 있을 정도로 간단해졌다.

사전 준비

일단은 당연하게도 앱 배포 단계가 자동화되어 있어야 한다. 팀마다 다르겠지만 보통 앱 배포는 최소한 버전업, 빌드, dSYM 처리(크래시 수집 툴에 업로드)를 해야하고 앱스토어용으로 스크린샷 생성하거나 사내 배포용 엔터프라이즈 앱이라면 빌드 결과물을 사내 인프라에 업로드하는 등의 일련의 작업이 있다.

배포 자동화에 큰 도움이 된 것이 fastlane이다. 가독성 좋은 API를 써서 빌드 스크립트를 쉽게 만들 수 있다. 오픈소스 플랫폼이라 여러 사람들이 만든 다양한 플러그인도 있어서 안되는 것이 거의 없다. 특히, xcconfig 파일을 수정할 수도 있고 git 작업도 쉽게 할 수 있고 필요에 따라서는 쉘 스크립트를 추가할 수도 있다. 도입하고 나면 fastlane 명령어 한 줄만으로 배포를 할 수 있게 된다.

그리고 빌드 서버가 필요하다. 요즘은 클라우드 기반 CI 서비스가 많다. 개발자 컨퍼런스를 가봐도 CI 서비스를 하는 스타트업에서 꼭 부스를 운영하고 홍보한다. travis-ci, circleci, bitrise 등이 있지만 이 글에서는 자체 서버를 구축해야하는 무료 CI 인 Jenkins를 기준으로 설명한다.

시리 숏컷 연동

사실 시리를 연동하는 단계는 가장 쉬운 단계이다. CI 서버를 통해 배포를 자동화하는 “사전 준비”에 99%의 시간과 노력이 들어간다. 실제로도 프로세스 자동화는 1년 내내 작업했고 시리 연동은 반나절 밖에 안걸렸다. 가장 이상적으로는 젠킨스 job의 Build Now 버튼만 클릭하면 앱이 배포되는 것이다.

배포 시나리오가 완성되면, 젠킨스 job의 설정에 들어가서 Build Trigger에서 Trigger builds remotely를 체크하고 토큰 값을 지정해준다.

그러면 이제부터 http://JENKINS_URL/job/JOB_NAME/build?token=TOKEN_NAME으로 POST를 날려주기만 하면 배포가 실행된다. 하지만 이대로는 http 통신할 때 젠킨스 서버 authentication을 처리해줘야 하는데 귀찮다면 Build Authorization Token Root 플러그인을 사용하면 위에 지정해준 토큰만으로 접근할 수 있다.

이렇게 젠킨스 플러그인까지 설치해줬다면 아래 명령어를 터미널에 입력하면 배포가 시작된다.

> curl -X POST http://JENKINS_URL/buildByToken/build?job=JOB_NAME&token=TOKEN_NAME

여기까지만 해줘도 배포가 훨씬 덜 귀찮은 작업이 됐다. 하지만 시리 숏컷이 처음 나왔을때부터 시리한테 배포를 시켜보는 것이 로망이었기 때문에 한 단계 더 가봤다. 아이폰에서 시리 숏컷(단축어) 앱을 실행하여 숏컷을 하나 생성한다. 아래처럼 Get Contents of URL 에다가 배포 URL을 넣고 POST를 선택해주고 내가 원하는 명령어를 입력해주면 끝이다.

느낀점

시리 숏컷 연동 자체는 엄청 쉬웠지만 하고나니 일년 동안 꾸준히 개발 프로세스를 자동화하고 개선해온 것에 대한 결실을 맺은 기분이었다. 만약 배포 프로세스가 올해 초의 상태였다면 시리 배포는 너무 멀거나 불가능하게 느껴졌을 것이다. 레츠스위프트 판교 1차 모임에서 전수열 발표자님이 ‘처음부터 의존성이 완벽하게 주입된 코드를 짜야겠다고 생각하지 말고 어제보다 조금이라도 더 나은 상태로 만들면 된다’고 했던 말이 인상 깊었다. 그런데 시리까지 연동하고 나니 그 경험을 조금이나마 해본 것 같다. 자동화하기 좋은 방식으로 프로세스를 바꾸고, 하나씩 조금씩 자동화를 해서 어제보다 조금 덜 귀찮게 만들다보니 여기까지 올 수 있었던 것 같다.

시리 숏컷도 처음 써봤는데 상상 이상으로 잘 만들어져 있어서 놀라웠다. 간단한 인터페이스로 할 수 있는 일이 무궁무진하다. 심지어 if문, 반복문, 변수 할당, 사용자 인풋 받기, 얼럿 띄우기 등등이 가능해서 흡사 앱 개발처럼 느껴진다. 친구한테 알려주니 우스갯소리로 스크래치말고 시리 숏컷으로 프로그래밍 배워야하는거 아니냐고 그랬는데 정말 불가능한 일도 아닌거 같다. 때마침 긱뉴스에서 No Code is New Programming라는 글을 읽게 되었는데, API가 정말 고도화되면 맨 마지막 단계에서는 코드 없는 인터페이스가 탄생한다는 것이고 이게 프로그래밍의 미래 모습이라는 것이다. 시리 숏컷이 이런 개념에 포함되는게 아닌가 싶다. 글이 배포 자동화로 시작해서 시리 숏컷 찬양으로 끝나게 되었는데, 시리 숏컷이 단순히 내 아이폰에 있는 앱을 실행시켜주는게 다가 아니라 여러 API(앱)를 코드 없이 조합할 수 있게 해준다는 것에 큰 영감을 얻게 되었다.

"알겠습니다.."가 너무 애잔하여 응답을 바꿨다.

Tags: siri shortcut, deploy app, siriops  

스위프트의 콤마와 &&의 차이: condition과 expression의 구분

이번 주 인터넷을 돌아다니다가 Swift의 if문에서 ‘,’와 ‘&&’의 차이라는 재밌는 글을 발견했다. 재밌었던 이유는 명료한 답변이 곧바로 떠오르지 않았기 때문이다. 가끔 이렇게 평소에는 별 생각없이 당연하게 쓰던 것에 대한 질문에 말문이 막히는 경우가 있다.

결론부터 한줄 요약 하자면, 콤마는 condition을 이어붙이는 용도로 쓰는 것이고 &&는 두개의 boolean expression을 파라미터로 받는 논리 연산자이다. 라고 결론 내릴 수 있겠다. 따라서 콤마와 &&의 차이는 conditionexpression의 차이를 이해하는데서 시작해야한다.

(1) 위에 “condition을 이어붙인다”고 했는데, 이렇게 이어붙여진 condition들을 condition-list라고 부른다. 스위프트 공식 문서에 따르면 condition-list는,

  • condition 이거나
  • condition , condition-list 이다.

정의에 재귀가 있다는 것만 유의하면 된다. 풀어서 말하면 condition-list는 condition 하나이거나 콤마로 condition을 (정의상 무한정?) 이어붙인 것이다.

  • condition
  • condition, condition
  • condition, condition, condition
  • condition, condition, condition, condition, …

(2) 그러면 condition은 뭘까? 마찬가지로 스위프트 공식 문서에 따르면 condition

  • expression
  • availability-condition
  • case-condition
  • optional-binding-condition

이렇게 네 개 중에 하나를 나타낸다. 아래 세 개는 이름만으로도 뭘 의미하는 꽤 명확해보인다.

(3) 그럼 condition의 한 종류인 expression은 과연 뭘까? expression도 위와 같이 계속 파고 들어갈 수 있지만 주제의 범위를 벗어나기 때문에 이쯤에서 그만하기로 하고, 최대한 간단하게 말해서 ‘연산자를 하나라도 쓴 스위프트 코드 단위’라고 볼 수 있다.

(4) 결국 &&는 하나의 expression을 만들 수 있는 수많은 연산자 중에 하나인 것이고, &&를 쓴 expression은 (condition의 정의에 따라) condition이기도 한 것이다.

그렇다면 원 질문의 예제로 돌아와서, 왜 첫번째는 되고 두번째는 안되는 것일까?

if let a = someOpt, let b = someOtherOpt {  } // works

if let a = someOpt && let b = someOtherOpt {  } // error

첫번째는 optional-binding-condition 두 개를 콤마로 붙인 condition-list 이기 때문에 잘 동작을 한다. 반면에 아래 코드는 && 논리 연산자를 썼는데, 논리 연산자는 두 개의 bool expression을 인자로 받는 연산자이다. 그런데 let a = someOptlet b = someOtherOpt는 bool expression이 아니다. 연산자를 잘못 썼기 때문에 당연히 에러가 난다. 비유하자면 "hello" && "world"가 안되는 것과 같은 이유이다.

또 다른 예제를 보게되면,

if 1 == 1, 2 == 2 {  } //works

if 1 == 1 && 2 == 2 {  } //works

둘다 기능적으로는 동일하게 동작하지만 컴파일러의 눈에는 조금 다르다. 첫번째는 condition(== 연산자를 쓴 expression)이 두 개이고, 두번째는 condition(&& 연산자를 쓴 expression)이 하나인 것이다.

결론으로 돌아오면, 콤마는 condition-list를 만드는 스위프트 문법의 종류이고, &&는 논리 연산자이다. 다시 말해 콤마 앞뒤에는 condition이 와야하고 && 앞뒤에는 boolean expression이 나와야 하는 것이다. 그리고 둘의 차이를 결정 짓는 핵심은 expression은 condition이지만 condition은 expression이 아니다라는 명제이다.

보너스

Q. condition과 condition-list를 굳이 구분해야하나?

A. 그렇다. 왜냐면 condition과 condition-list를 쓸 수 있는 곳이 다르다. while, if, guard 문에는 condition-list를 쓰지만 repeat-while 문에서는 condition만 쓸 수 있다. 콤마로 여러 condition을 이어붙이는 것이 허용된 곳과 아닌 곳이 있는 것이다.

Tags: swift comma, condition-list, AND operator  

iOS 13 앱 업데이트 후기

Background

iOS 13에 다크 모드가 추가돼서 개인 앱 보안카드 위젯을 오랜만에 업데이트했다. 이왕 하는 김에 9000줄 정도의 옵젝씨 코드도 전부 스위프트 전환을 했고 기존에 쓰던 C++로 된 SMTP 라이브러리도 더 가벼운 스위프트 라이브러리로 바꿨다. Pod도 전부 제거하고 이제는 카르타고로 파이어베이스만 외부에서 가져와 빌드를 하고 있다. 앱 용량도 15메가에서 6메가 정도로 줄어서 기분이 좋다. 오래된 집을 깔끔하게 리모델링한 기분이다.

1️⃣ .pageSheet 모달

개발자 입장에서 가장 충격적인 변화는 modal presentation의 기본 값이 바뀌었다는 점이다. 기존에는 fullScreen이 기본이었고 화면 전체를 가렸다. iOS 13에서는 pageSheet가 기본이 되었고 이 프레젠테이션은 화면 전체를 가리지 않고 상단에 살짝 걸쳐 있다. 그리고 화면을 드래그해서 뷰를 내릴 수 있다. iOS 13이 나오기 전부터 페이스북 앱에서는 외부 링크용 웹뷰가 이런식으로 되어있었고 지도나 내비게이션 앱, AR 앱 등에서 최근 급격히 많이 보이기 시작했다. 이와 관련된 오픈소스도 굉장히 많이 등장했다.

아이폰의 크기가 많이 커진 상황에서 기존의 모달을 닫으려면 스크린 맨 위에 있는 버튼에 닿기 위해 위태롭게 그립을 바꾸거나 다른 손을 써야 했었다. 하지만 손가락을 멀리 뻗지 않고도 드래그 제스처로 뷰를 내릴 수 있다는 점에서 이런 형태의 모달이 개인적으로 매우 유용했고 앞으로 아이폰 앱에서 쓸 일이 많을 것이라고 생각했었다. 그래서 네이티브로 이런 UI를 사용할 수 있게 돼서 기쁜 한편, 이렇게 하위 호환성을 깨뜨리는 방식으로 급격히 변화를 가져 올 줄은 몰랐다. Modernizing Your UI for iOS 13에서 변경점에 대해 아래처럼 발표를 한다.

‘여러분, 새로운 모달 프레젠테이션 스타일입니다! 사용자에게 더 많은 비주얼 컨텍스트를 제공합니다. 인터랙티브하게 dismiss 할 수도 있습니다. 멋지죠? 어떻게 쓰면 되나고요? 그냥 아무것도 안하면 됩니다. 왜냐면 저희가 기본 값을 이걸로 바꿔버렸기 때문이죠. 예전처럼 동작하게 만들고 싶으시다고요? 코드를 한 줄만 추가해주면 됩니다!’

예전처럼 동작하려면 코드를 추가해야 한다니! 그리고 이걸 이렇게 기쁜 표정으로 자랑하듯이 발표하다니. 이런 태도, 뻔뻔함이 애플의 상징처럼 느껴져서 황당하면서 엄청 재밌었다. Breaking change 까지는 아니지만 이번 모달의 변화는 거의 에러 못지 않게 기존 UI와의 호환성을 깨뜨리거나 유저 플로우에 큰 영향을 준다. 특히 모달을 벗어나는 경로가 새로 생긴 것에 대한 추가 작업이 필수이다.

예전에는 모달 화면을 나갈 수 있는 유일한 방법은 앱 개발자가 추가한 코드를 통해서 밖에 없었다. 하지만 pageSheet 모달을 사용하면 드래그 제스처가 자동으로 추가된다. 여기에 개발자가 관여할 수 있는 방법은 두 가지가 있다. UIViewController의 isModalInPresentation을 true로 하면 아예 유저가 드래그로 dismiss 할 수 없게 된다. 또는 UIAdaptivePresentationControllerDelegate에 새롭게 추가된

func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool

에서 지정할 수도 있다.

마지막으로 유용한 델리게이트 하나는

func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController)

isModalInPresentation이 true거나 presentationControllerShouldDismiss 메서드가 false 를 리턴하면 유저의 드래그 dismiss가 실패하면서 이 메서드가 불린다. 사용자가 작업해 둔 내용물이 있을 때 실수로 날리지 않게 이때 정말 화면을 나갈 것이냐를 확인하는 얼럿이나 액션 시트를 띄워주면 된다.

2️⃣ @propertyWrapper

Swift 5.1에 추가된 프로퍼티 래퍼가 매우 유용했다. 프로퍼티 래퍼는 변수의 저장소, 캐싱 전략, 바운딩 로직 등등 변수의 특성과 관련된 로직을 분리하여 재사용할 수 있게 해주는 기능인데 총 4가지의 프로퍼티 래퍼를 만들어서 썼다.

  • SharedDefaults: UserDefaults에서 값을 가져오는 래퍼.
  • SharedKeychain: Keychain에서 값을 불러오는 래퍼.
  • LateInitialized: 변수를 사용하기 전에 무조건 값을 세팅해줘야 하는 래퍼. IUO를 제거할 수 있다. 전부터 unspecified()로 만들어서 쓰던 것을 대체했다. 굳이 lazy 키워드를 써주지 않아도 돼서 실수 유발 가능성도 없고, 원하면 immutable 하게도 만들 수 있어서 훨씬 사용성이 좋다.
  • ClampedString: 지정해둔 max 길이를 넘어갈 수 없는 String 래퍼

프로퍼티 래퍼는 스위프트의 API 디자인 가이드라인에서 가장 중요시 하는 원칙 Clarity at the point of use에 딱 부합하는 기능인 것 같다.

3️⃣ SF Symbols

정-말 좋다. 특히 사이드 프로젝트 앱 개발을 할 때 무료 아이콘을 쓰겠다고 이곳 저곳에서 원하는 걸 가져오다 보면 통일성도 떨어지고 무엇보다 퀄리티가 너무 아쉽다. SF Symbol은 정말 많고 다양해서 용도가 무궁무진하다. 게다가 이미 시스템 앱들이 쓰고 있다. 보안카드 위젯 앱에서도 기존에 쓰던 아이콘들을 전부 대체했고 텍스트로 쓰던 부분도 적절한 아이콘으로 교체할 수 있었다.

SF Symbol은 두 가지 방법으로 사용할 수 있다. 첫번째는 이미지로 쓰는 것이다. UIImage(systemName:) 생성자를 사용해서 이미지를 만들 수 있다. 두번째는 텍스트로 쓰는 것이다. 원리가 뭔지는 모르겠지만 심볼은 SF(샌프란시스코) 폰트에 포함되어있다. 그래서 SF 폰트로 설정된 UILabel이나 UITextView 등에서 이모티콘처럼 텍스트와 함께 사용할 수 있다. 심지어 폰트 두께와 크기에 따라서 아이콘도 바뀐다. 이미지로 쓸 때도 두께와 크기를 변경할 수 있다.

관련 링크

4️⃣ 다크 모드


사용자에게 이번 업데이트의 최대 하이라이트는 다크 모드겠지만 개발 관점에서 딱히 특별하거나 재밌는 것은 없었다. 속된 말로 노가다지만, 하고 나서 결과물을 보면 뿌듯하고 개운한 느낌이랄까. 어두운 배경의 새로운 색을 조합하고, xcasset에 named color를 만들어서 적용하는 것 뿐이다. 네임드 컬러는 Xcode 9부터 등장했고 iOS 11부터 지원한다. 네임드 컬러를 만들어 놓고 UIColor(named:) 로 생성하거나 스토리보드 컬러 피커에서 고를 수도 있어서 사용이 편리하다.

또한 다크 모드 대응의 일환으로 애플에서 시스템 컬러와 다이나믹 시스템 컬러라는 것을 새로 만들었다. ‘system’ 이라는 접두어가 쓰인 색을 쓰면 자동으로 다크모드와 라이트모드에서 각각 적절한 색이 사용된다. 다이나믹 시스템 컬러라는 것은 배경, 컨텐츠용 텍스트, separator 등등 색의 사용처 기준으로 색의 이름을 지은 것이다. 색 이름이 label, secondaryLabel, tertiaryLabel, separator, link 등등 처럼 되어있어서 의미에 맞춰 사용하면 된다. 또한 다크모드 대응된 회색도 6가지 종류나 생겼다. HIG 설명에 따르면 투명도가 잘 안 먹히는 상황에서 드물게 쓰라고 하는데 이건 디자인쪽 언어인거 같아서 무슨 말인지 잘 이해가 안간다.

개발하다가 공식 문서를 읽지 않아서 디버깅이 오래 걸렸던 이슈 하나는 다크모드 / 라이트모드 전환을 할때 어떤 뷰들만 색이 전환이 안되는 이슈였다. 알고보니 UIColor 객체를 바로 사용하면 자동으로 전환이 되지만 CGColor 값으로 사용하면 개발자가 수동으로 다크 모드 전환에 대응했어야 하는 것이다. (참고: Standard Colors). 그래서 커스텀 뷰의 CAShapeLayer에 적용해놓은 borderColor와 fillColor는 다크 모드 전환이 안되고 있었다. 따라서 다크모드 전환이 될 때 불리는 적절한 메서드 내에서 CGColor 값을 지정해주어야 한다.

종합적으로 느낀 점은 언제나 그랬듯 애플이 기본으로 제공해주는 것들만 적절히 잘 써도 디자이너의 도움 없이도 그럴듯하게 다크모드를 적용할 수 있다.

Tags: iOS 13, dark mode, propertyWrapper, SF symbols