'Swift와 iOS/기술'에 해당하는 글 35건

목차

  • libxlsxwriter 소개

  • Swift 프로젝트에서 c라이브러리 pod 세팅하기

  • libxlsxwriter 사용법

  • 마치며


libxlsxwriter 소개

libxlsxwriter는 C라이브러리입니다. (벌써부터 머리가 지끈..) Objective-C를 사용할때는 편하게 썼었는데 요즘은 Swift만 쓰다보니 C를 쓰는게 여간 부담스러운 일이 아닐 수 없네요. 여러가지 라이브러리를 써봤지만 실패를 하곤 결국 C 라이브러리까지 찾게 되었습니다. 그래도 한가지 다행인 소식은 이 라이브러리가 CocoaPods을 지원한다는 것입니다.

아래는 여러가지 엑셀 편집 라이브러리입니다. 저는 결국 libxlsxwriter를 사용하기로 했습니다.

Swift 프로젝트에서 c라이브러리 pod 세팅하기

libxlsxwriter를 사용하려면 pod으로 워크스페이스를 만들어줘야하는데요. 그밖에도 몇가지 세팅이 더 필요합니다. C라이브러리라그런지 프레임워크로 인식을 못하더라구요. 그래서 bridge-header.h를 만들고 #include로 라이브러리의 파일을 불러오면 프로젝트의 Swift에서 사용할 수 있습니다.

Podfile 만들기

Podfile을 만듭니다. 확장자가 따로 없는 텍스트파일입니다. Podfile.xcodeproj 와 같은 경로(프로젝트 경로라 부르겠습니다)에 저장해둡니다.

libxlsx_구조.png

Podfile 의 내용물입니다. CocoaPods은 Objective-C나 Swift 언어의 프로젝트에서 사용하는 의존성 관리 툴인데, 사용하는 방법은 따로 찾아보시길 바랍니다.

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

def my_pods
  pod 'libxlsxwriter', '~> 0.7.7'
end

target 'excel-example' do
  my_pods
end
target 'excel-exampleUITests' do
  my_pods
end

여기서 중요한 부분은 pod 'libxlsxwriter', '~> 0.7.7'입니다. 0.7.7버전의 libxlsxwriter 라이브러리를 설치하라는 부분이죠. (참고로 excel-example는 제 프로젝트 이름입니다.)

pod 설치

cd ~/{프로젝트경로}
pod install
excel-example-Bridging-Header.h 만들어서 설정

저는 use_frameworks!를 사용했었는데, libxlsxwriter는 C 라이브러리라 그런지 읽어오질 못했습니다. 그래서 따로 excel-example-Bridging-Header.h를 만들고 프로젝트에 명시해주었습니다(Build Setting에 들어가서 Swift Compiler - Code Generation > Objective-C Bridging headerexcel-example/excel-example-Bridging-Header.h으로 설정해주시면 됩니다).

C 라이브러리 불러올 수 있게 Pod 경로 설정

이번에는 헤더파일을 불러올 수 있게 Pod 경로를 명시해줍니다(마찬가지로 Build Setting에 들어가서 Search Paths > User Header Search PathsPods를 추가하고 recursive를 선택해줍니다).

여기까지 잘 따라오셨다면 이제 excel-example-Bridging-Header.h를 통해 C라이브러리를 불러와서 사용할 수 있습니다!

libxlsxwriter 사용법

excel-example-Bridging-Header.h에서 xlsxwriter.h 불러오기

#include "xlsxwriter.h"
저장할 경로 정하기

let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let url = documentsURL.appendingPathComponent("filename").appendingPathExtension("xlsx")
let path = NSString(string: url.path).fileSystemRepresentation
print("path:", url.path)
초기 설정

/* 새 워크북을 만들고 워크시트를 하나 추가하기 */
let workbook = new_workbook(path)
let worksheet = workbook_add_worksheet(workbook, nil);

아래같은 새 엑셀 파일을 하나 만들고 Sheet1 시트를 하나 만든 것입니다. 시트 이름을 바꾸려면 nil대신에 문자열을 넣어줍니다.

new_xlsx.png

초기 설정

/* 0컬럼부터 5컬럼까지 13.7 너비 적용 */
worksheet_set_column(worksheet, 0, 5, 13.7, nil);
셀 생성

/* 0 row, 4 column에 "이름" 문자열 넣기 */
worksheet_write_string(worksheet, 0, 4, "이름", nil);
/* 1 row, 2 column에 1.25123 숫자 넣기 */
worksheet_write_number(worksheet, 1, 2, 1.25123, nil);


/* 포멧 생성 */
let my_format = workbook_add_format(workbook);
format_set_align(my_format, UInt8(LXW_ALIGN_RIGHT.rawValue));
format_set_align(my_format, UInt8(LXW_ALIGN_VERTICAL_CENTER.rawValue));
format_set_bold(my_format);
format_set_font_size(my_format, 20)
format_set_font_name(my_format, "Arial")
/* 포멧이 적용된 셀 생성 */
worksheet_write_string(worksheet, 0, 1, "타이틀", my_format);
셀 합치기

worksheet_merge_range(worksheet, 3, 0, 3, 5, "합쳐진 셀", my_format);
파일 닫기

workbook_close(workbook);

모든 작업이 끝났으면 파일을 닫아주셔야합니다. 그리고 닫은 파일을 사용하려하면 에러가 발생합니다.

전체코드

https://github.com/tucan9389/excel-example-ios

위 코드로 만들어진 엑셀파일 예제코드 실행 ê²°ê³¼ 엑셀파일.png

마치며

코드 분할

C 인터페이스를 사용하다보니 Swift 코드와 안 어울리는것 같습니다. 저는 ExcelMaker.swift 파일로 빼내서 ExcelMaker 클래스를 만든 다음 클래스 메소드 static func createXLSX(filename: String, info: MyInfo) { ... }를 만들어서 사용했습니다.

Excel 포멧의 한계

엑셀파일 만들기가 csv파일 만들기와 비슷할 줄 알고 쉽게봤다가 고생 좀 하면서 느낀점은, MS는 엑셀파일을 자기네 프로그램 안에서만 쓰게 하려고 이렇게 사용하기 어렵게 만들었나 싶습니다. 아마도 엑셀파일 용량 줄이고 최적화시킨다고 이렇게 된게 아닐까 생각이 드는데, 라이브러리를 쓰실때도 오피셜 라이브러리가 따로 없기때문에 문제가 생겼을때 처리하기도 매우 곤란한 상황입니다. 여하튼 제3자 개발자가 엑셀파일을 직접 건드리는 작업은 피하는걸 권장하고싶습니다.

엑셀파일 만들기가 csv파일 만들기와 비슷할거라 생각하고 덤볐다가 고생했습니다. 그리고 고생한 이유를 찾아보았습니다.

  1. 엑셀파일 포멧이 표준을 지키지 않습니다.마이크로소프트 자신만의 포멧을 만들어 엑셀을 지원하고 있는데, 아마도 용량을 줄이거나 최적화시키려고 그랬겠지요..

  2. 공식라이브러리는 당연히 없고, 그러다보니 엑셀파일을 만들 수 있게 지원해주는 비공식(이지만 거의 공식처럼 쓰이는)라이브러리도 약한 편입니다.물론 Swift 라이브러리를 찾아보려다보니 범위가 좁긴 했지만, 엑셀 정도로 자주쓰이는 포멧은 Swift용 라이브러리가 있을법도한데 Star가 100개를 넘는 저장소를 찾기 힘들었습니다.

참고



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문: https://medium.com/developerinsider/best-ios-development-tips-and-tricks-6c42c1d208c1

이 글은 먼저 DeveloperInsider에 발행됩니다. 여기(원문)서 확인할 수 있습니다.

1. Xcode에서 빌드시간 확인하기

프로젝트의 정확한 빌드 시간을 모른다면 Xcode에서 아래 옵션을 켜자.

defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES

2. Swift 프로젝트 빌드시간 단축시키기

Xcode 9.2 릴리즈 노트에서는 BuildSystemScheduleInherentlyParallelCommandsExclusively 사용자 디폴트를 켜서 스위프트 빌드시간을 단축시켜주는 실험단계의 기능을 언급했습니다.

defaults write com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively -bool NO

주의: 릴리즈 노트에 따르면 "빌드시간동안 메모리 사용을 증가시킬 수 있는 실험적 기능"이라고 했습니다.

3. Xcode에서 전체화면 모드로 시뮬레이터 사용하기

나는 Xcode 9 기능중에 전체화면으로 iOS 시뮬레이터와 Xcode를 실행시키는 것을 좋아한다. 이 기능을 사용하려면 그냥 터미널을 켜서 아래 명령을 실행시키면 된다.

defaults write com.apple.iphonesimulator AllowFullscreenMode -bool YES

시뮬레이터에 숨겨진 더 많은 기능을 사용하고싶으면 애플의 숨겨진 Internals 메뉴를 활성시키면 된다. 이렇게 하기 위해서는 루트 폴더에 AppleInternal이라는 빈 폴더를 만들자. 아래 명령을 실행시키고 시뮬레이터를 재시작하면 된다.

sudo mkdir /AppleInternal

4. iOS 시뮬레이터 화면 녹화하기

xcrun 명령어 유틸리티를 이용하면 시뮬레이터 창을 스크린샷 찍거나 비디오로 저장할 수 있습니다. 비디오를 찍기 위해서는 아래 명령을 실행시키세요.

xcrun simctl io booted recordVideo <filename>.<file extension>

예시:

xcrun simctl io booted recordVideo appvideo.mov

녹화를 멈추려면 control + c를 누르십시오. 파일이 만들어지면 현재 폴더에 저장됩니다.

5. 파인더에서 시뮬레이터에 파일 공유하기

Xcode 9부터는 시뮬레이터가 파인더 확장을 가지고 있습니다. 이것은 파이더 창에서 바로 파일을 공유할 수 있게 해줍니다. 그래도 파일을 시뮬레이터로 드레그&드롭해 주는게 더 빠른거 같네요.

아니면 아래 simctl 명령으로 이미지/비디오 파일을 전송할 수 있습니다.

xcrun simctl addmedia booted <PATH TO FILE>

6. sudo 인증시 지문 사용하기

맥북프로의 지문을 sudo 인증의 비밀번호로 사용하고 싶다면 /etc/pam.d/sudo를 수정하고 상단에 아래줄을 넣습니다.

auth sufficient pam_tid.so

이제 sudo로 지문을 사용할 수 있습니다.

7. 소리 알림과 함께 AutoLayout 컨스트레인트 디버깅하기

이번 방법은 AutoLayout 컨스트레인트를 디버깅하는데 훌륭한 벙법입니다. 그냥 실행시 UIConstraintBasedLayoutPlaySoundOnUnsatisfiable 인자를 넘겨주면 런타임시 컨스트레인트가 어긋날을 경우 소리가 납니다.

-_UIConstraintBasedLayoutPlaySoundOnUnsatisfiable YES

8. Xcode에서 사용할 수 없는 시뮬레이터 제거하기

이 조그만 명령어는 Xcode에서 사용할 수 없는 모든 시뮬레이터를 제거해 줍니다. 여기서 "unavailable"은 Xcode의 xcode-select 버전에서 사용불가능한 것을 의미합니다.

xcrun simctl delete unavailable

재밌었나요? 아래에 댓글을 달아주시면 고맙겠습니다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 - 블로그 댓글 - 페이스북 페이지(@나는한다번역-ios) - 트위터(@tucan9389) 으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

출처: [애플 공식 문서] Integrating a Core ML Model into Your App(원문)

앱에 간단한 모델을 추가하고, 모델에 입력 데이터를 넣은 뒤, 모델 예측을 실행시키는 예제입니다.

프로젝트 다운로드

SDKs

iOS 11.0+

Xcode 9.2+

Framework

Core ML

On This Page

개요

함께 보기

개요

이번 간단한 앱은 학습된 간단한 모델 (MarsHabitatPricer.mlmodel)을 사용하여 화성거주지 가격을 예측할 것이다.

여러분의 Xcode 프로젝트에 모델 넣기

모델을 Xcode 프로젝트에 넣기 위해서는 모델을 드레그해서 프로젝트 네비게이터에 넣어야한다.

Xcode에서 모델을 열면 모델 타입, 예상되는 입출력등 모델에대한 정보를 확인할 수 있다. 여기서 모델 입력값은 태양열 전지판과 온실 갯수, 그리고 거주지 크기(에이커 단위)이다.

코드에서 모델 생성하기

Xcode는 모델의 입출력 정보를 이용하여 자동으로 모델의 인터페이스를 생성하는데, 이 인터페이스는 여러분의 코드상에서 모델과 상호작용하는데 쓰일 것이다. MarsHabitatPricer.mlmodel의 경우 Xcode는 MarsHabitatPricer모델을 표현하는 인터페이스와 모델의 입력(MarsHabitatPricerInput), 모델의 출력(MarsHabitatPricerOutput)을 생성한다.

모델을 생성하려면 만들어진 MarsHabitatPricer 클래스 생성자를 사용하자.


let model = MarsHabitatPricer()

모델에 넣을 입력값 가져오기

이번의 간단한 앱에서는 사용자로부터 모델의 입력값을 가져오기위해 UIPickerView를 사용한다.


func selectedRow(for feature: Feature) -> Int {
   return pickerView.selectedRow(inComponent: feature.rawValue)
}

let solarPanels = pickerDataSource.value(for: selectedRow(for: .solarPanels), feature: .solarPanels)
let greenhouses = pickerDataSource.value(for: selectedRow(for: .greenhouses), feature: .greenhouses)
let size = pickerDataSource.value(for: selectedRow(for: .size), feature: .size)

예측을위해 모델 사용하기

MarsHabitatPricer 클래스는 자동으로 생성된 prediction(solarPanels:greenhouses:size:) 메소드를 가지는데, 이 메소드는 모델 입력값으로부터 가격을 예측하는데 쓰인다. 이번의경우 태양열 전지판과 온실 갯수, 그리고 거주지 크기(에이커 단위)를 넣는다. 이 메소드는 MarsHabitatPricerOutput 인스턴스를 결과로 내뱉는다.


guard let marsHabitatPricerOutput = try? model.prediction(solarPanels: solarPanels, greenhouses: greenhouses, size: size) else {
   fatalError("Unexpected runtime error.")
}

예측된 가격을 가져오기위해 marsHabitatPricerOutputprice프로퍼티에 접근하여 앱 UI에 표시하자.


let price = marsHabitatPricerOutput.price
priceLabel.text = priceFormatter.string(for: price)
주의

자동으로 생성된 prediction(solarPanels:greenhouses:size:) 메소드에서 에러를 던질수 있다. Core ML으로 작업하면서 만나게될 가장 일반적인 에러 종류는 입력 데이터의 내용과 모델이 기대하는 내용이 서로 일치하지 않는 경우입니다(가령 잘못된 이미지 포멧이라던지).

Core ML 앱을 빌드하고 실행하기

Xcode는 Core ML 모델을 디바이스에 최적화시켜 컴파일한 후 리소스 안에 넣어둘 것입니다. 최적화된 모델은 앱번들안에 들어가며, 이 모델은 앱이 디바이스에서 실행되는동안 예측을 만드는데 쓰입니다.

함께 보기

첫번째 단계



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

1. Tensorflow mobile for iOS(Deprecated)

  • pod 'TensorFlow-experimental'

  • C++ 인터페이스

  • pod(의존성 관리 툴)으로 프로젝트 환경을 구축하면 450MB가 넘음. 여러 플랫폼을 묶다보니 용량이 큼. 예제를 앱으로 말면 약 25MB정도로 만들어짐.

  • Tensorflow Lite로 대체됨.

2. Tensorflow Lite

iOS 기기에서 TensorFlowLite 로 .tflite 모델을 사용할 수 있음

  • TensorFlow Lite를 바로 사용할 시 C++ 인터페이스

  • TensorFlow Mobile의 다음 버전

  • 더 작은 바이너리 크기

  • 적은 의존성, 더 나은 퍼포먼스

Tensorflow Lite for iOSiOS 기기에서 .tflite 모델을 실행시킨 결과



3. Core ML

iOS 기기에서 Core ML 로 .mlmodel 모델을 사용할 수 있음

  • Swift/Objective-C 인터페이스(Core ML)

  • 텐서플로우(.pb)를 CoreML(.mlmodel)로 변환하여 사용.

  • 애플이나 구글의 공식 프로젝트는 아니지만 스타가 많은 프로젝트.

4. MLKit with Firebase

Firebase 프레임워크에 들어있는 MLKit 로 .tflite 모델을 Swift 인터페이스로 사용할 수 있음

  • Swift, Objective-C 인터페이스

  • 18.05.09 구글I/O에서 발표

  • 기본 ML 기능 제공

    • Text Recognition (OCR)

    • Face Detection

    • Label Detection

    • Cloud Landmark Detection

    • Cloud Text Recognition

    • Cloud Label Detection

    • Custom Model Object Detection(.tflite 모델 사용)

MLKit for iOSiOS 기기에서 MLKit으로 .tflite 모델을 실행시킨 결과


+. tensorflow + swift

기계학습 모델을 만드는 새로운 방법.18년5월에 처음 공개되었고 개발중..



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: iOS11: Machine Learning for everyone

WWDC 2017에서 한가지는 확실하게 만들었다. 애플은 기기에서의 기계학습을 확실히 밀고 있다.

그리고 앱 개발자들이 여기에 조인할 수 있도록 가능한 쉽게 만들고 싶어한다.

작년 애플은 기본 컨볼루셔널 신경망(basic convolutional networks)을 위한 Metal CNN and BNNS frameworks를 발표했다. 올해는 Metal, 새로운 컴퓨터 비젼 프레임워크, Core ML의 수많은 수가사항이 있다. 툴킷은 ML 모델을 여러분의 앱에 아주 쉽게 넣을 수 있게 해준다.


이 블로그 포스팅에서는, iOS11와 macOS10.13에서 새로운 기계학습에대한 내 생각(과 내 경험)을 공유하려고 한다.

Core ML
Core ML은 WWDC에서 많은 주목을 받았고, 그 이유는 알기 쉽다. 바로 많은 개발자들이 그들의 앱에 넣으려고 했던 프레임워크이기 때문이다.

이 API는 꽤 간단하다. 당신이 할 수 있는 것은 다음과 같다.
  1. 학습된 모델을 불러오기
  2. 예측 만들기
  3. 개이득!!!
이 말이 제한적으로 보일 수 있지만, 실제로 모델을 불러와서 예측을 하는 것은 보통 여러분의 앱에서 하고 싶었던 모든 것일 수 있다.

예전에는 학습된 모델을 불러오는 것이 굉장히 힘들었다(사실 library to take away some of the pain에대한 글을 썼었다). 그러니 이제 단지 두단계로 간단해져버려 굉장히 행복하다.

모델은 .mlmodel 파일안에 담겨있다. 이것은 새로나온 공개형 파일 포멧(open file format)으로 여러분의 모델, 입력과 출력, 클래스 레이블 그리고 데이터에 일어나는데 필요한 전처리에서 그 레이어를 표현한다. 또한 학습된 모든 파라미터들을 담고있다(가중치와 기저).

모델을 사용하는데 필요한 모든것이 이 한 파일 안에 들어 있는 것이다.

여러분은 간단하게 여러분의 프로젝트에다 이 mlmodel 파일을 떨어뜨리면 Xcode가 자동으로 스위프트(혹은 Objective-C)를 감싸는 클래스로 생성하며, 이런 과정이 모델을 사용하기 굉장히 쉽게 만들어준다.

예를들어, 여러분의 Xcode 프로젝트에 ResNet50.mlmodel 파일을 추가했으면, 이렇게 작성할 수 있겠다.
let model = ResNet50()
모델을 인스턴트화한다. 그리고 아래에는 예측을 한다.
let pixelBuffer: CVPixelBuffer = /* your image */

if let prediction = try? model.prediction(image: pixelBuffer) {
  print(prediction.classLabel)
}
그리고 이게 전부이다. 모델을 불러오거나 스위프트에서 그 결과물을 변환하기위해 다른 코드를 짤 필요가 없다. 이 모든것을 Core ML과 Xcode가 관리해준다. 멋지다!
주의: 화면뒤에서 무슨일이 일어나는지 알고싶으면, Project Navigator에 mlmodel 파일을 선택하여 버튼을 클릭하면 생성된 헬퍼 코드로 파일을 볼 수 있다.
Core ML은 모델을 CPU에서 돌릴지 GPU에서 돌릴지 스스로 결정할 것이다. 그리하여 가용의 리소스 사용을 최적화할 수 있게 해준다. 또한 Core ML은 특정 부분만 GPU에서 동작하고(많은 계산이 많이 필요한 작업) 다른 부분은 CPU(메모리가 많이 필요한 작업)에서 동작하도록 쪼갤 수 있다.

Core ML이 CPU를 사용할 수 있는 능력은 개발자들에게 또다른 큰 이점을 준다. iOS 시뮬레이터에서 돌려볼 수 있다는 점이다(어떤것은 Metal에서 불가능한데, 이것 또한 유닛테스트로 잘 돌아가지 않는다).

Core ML이 지원하는 모델은 무엇인가?
위의 ResNet50 예제는 이미지 분류기이지만, Core ML은 여러 타입의 모델을 다룰 수 있다.
  • support vector machines(SVM)
  • tree ensembles such as forests and boosted trees
  • linear regression and locgistic regression
  • neural networks: feed-forward, convolutional, recurrent
이 모든 것들은 회기(regression)뿐만 아니라 분류(classification)에도 사용될 수 있다. 추가로 여러분의 모델은 one-hot encoding, feature scaling, imputation of missing values등과같은 전형적인 ML 전처리 과정을 담고 있을 수 있다.

애플은 학습시킨 여러 모델을 다운로드 받을 수 있게 해놓았다. Inception v3, ResNet50, VGG16같은 것이 있는데, 파이썬 라이브러리 Core ML Tools로 자신만의 모델을 변환할 수도 있다.

현재 여러분은 Keras, Caffe, scikit-learn, XGBoost, libSVM로 학습한 데이터를 변환할 수 있다. 이 변환 툴은 지원하는 버전에대한 약간의 명세이다. 예를들어 Keras 1.2.2는 동작하지만 2.0은 아닌. 다행인점은 이 도구가 오픈소스여서 미래에는 더 많은 학습 툴킷을 지원할 부분에는 확신할 수 있다.

그리고 다른 모든것이 실패하면 언제나 여러분만의 변환기를 만들 수 있다. mlmodel 파일 포멧은 공개형이고 꽤 직관적으로 사용할 수 있다(photobuf 포멧 내에 있고, 스팩은 애플이 명세해놓음).

한계
Core ML은 모델을 빨리 불러와서 여러분의 앱에서 실행하기에는 훌륭하다. 그러나 이런 간단한 API로는 그 한계를 가지고 있다.
  • 지원하는 모델 타입들은 감독 기계학습(supervised machine learning)이다. 비감독 학습 알고리즘이나 강화 학습은 안된다. (비록 "제네릭" 뉴럴 네트워크 타입을 지원하는데, 이것을 이용할 수는 있을것 같다)
  • 기기에서 학습은 지원하지 않는다. 당신은 오프라인 툴킷을 사용하여 모델을 학습시키고 모델을 Core ML 포멧으로 변환해야한다.
  • Core ML이 특정 레이어 타입을 지원하지 않는다면, 사용할 수 없다. 이 점에서 여러분이 직접 계산한 커널로 Core ML을 확장시키는게 불가능하다. TensorFlow같은 툴들이 질반 목적 컴퓨테이셔널 그래프를 만들기위해 사용되는 곳은, mlmodel 파일 포멧은 유연성과는 좀 멀다.
  • Core ML 변환 툴은 제한된 수의 학습 툴과 명세한 버전만 지원한다. 예를들어 TensorFlow에서 모델을 학습했으면, 이 툴을 사용할 수 없고 여러분의 변환 스크립트를 작성해야할 것이다. 그리고 내가 덫붙이자면, 여러분의 TensorFlow 모델이 mlmodel에서 지원하지 않는 것을 하고 있다면, Core ML에서 모델을 사용할 수 없다.
  • 중간 레이어(intermediate layer)에서 만들어진 아웃풋을 볼 수 없다. 네트워크의 마지막 레이어에서 나온 예측만 볼 수 있다.
  • 100% 확신이 되진 않지만, 모델 업데이트를 다운받는 것이 문제가 있어 보인다. 여러분이 다시 학습시키고 싶은데, 바뀐 모델바다 앱의 새로운 버전을 내고 싶지 않으면 Core ML은 여러분에게 적절하지 않을 것이다.
  • Core ML은 (편의를위해) CPU에서 실행하는지 GPU에서 실행하는지 숨기는데, 이것이 앱을 위해 잘 동작할거라 믿어야만 한다. 당신이 아무리 아무리 원한다고 해도 Core ML에게 강제로 GPU에서 실행하라고 할 수 없다.
당신이 이런 한계들과 공존할 수 있다면, Core ML은 당신에게 맞는 프레임워크이다.

그렇지않거나, 전체를 컨트롤 하고 싶다면, Metal Performance Shaders나 Accelerate 프레임워크로 여러분만의 것을 만들어야할 것이다.
물론 실제 마법이 Core ML에 있진 않지만, 여러분의 모델에 들어있다. 여러분이 시작할때 적절한 모델을 가지고 있지 않다면 Core ML은 소용없을 것이다. 그리고 모델을 설계하고 학습시키는것은 기계학습에서 어려운 부분이다.

간단한 데모 앱
Core ML을 가지고 놀아보려고 간단한 데모를 만들었다. 언제나처럼 깃헙에서 소스를 확인할 수 있다.


이 데모 앱은 고양이 사진을 분류하기위해 MobileNet architecture을 사용했다.

원래 이 모델은 Caffe에서 학습되었다. 이것이 mlmodel 파일로 변환하는데 약간의 노력이 들게 만들었으나, 한번 변환한 모델로 만들고나니 앱에서 빌드하기 굉장히 쉬었다. (변환 스크립트는 깃헙 저장소에 들어있다)

앱은 아직 멋지지 않지만(단순히 정적인 이미지에대해 상위 5가지 예측의 아웃풋을 낸다) Core ML을 어떻게 쉽게 사용했는지 보여준다. 그냥 코드 몇줄만이 필요할 뿐이었다.
주의: 데모앱이 시뮬레이터에서는 잘 동작하지만 기기에서는 크래쉬가 난다. 읽어내려가서 왜 그런지 찾아내보자 ;-)
물론 안에서 무슨일이 일어나는지 알고싶었다. 실제로 mlmodel 파일이 mlmodelc 폴더에 컴파일되었으며 이 폴더는 여러분의 앱 번들 안에 있다. 이 폴더는 여러 다른 파일들, 몇몇 바이너리, 몇몇 JSON들을 가진다. 따라서 여러분의 앱에 실제로 넣기전에 Core ML이 어떻게 mlmodel을 변환하는지 볼 수 있다.

예를들어 MobileNet Caffe 모델은 소위 Batch Normalization 레이어라 불리는 것을 사용하며, 이것들이 변환된 mlmodel 파일안에 들어있다는 것을 확인했다. 그러나 컴파일된 mlmodelc에서는 Batch Normalization 레이어가 보이지 않았다. 이것은 Core ML이 모델을 최적화시켰다는 좋은 소식이다.

mlmodelc는 여전히 스케일링 레이어(scaling layer)를 포함하며 반드시 필요하지 않아 보이므로 좀 더 모델의 구조를 최적화할 수 있을것 같아 보인다.

물론 아직 iOS11 베타1버전이고 Core ML은 아마 더 개선될 것이다. 그 말은, Core ML에 넘겨주기전에 여러분의 모델을 최적화할 필요가 있다는 뜻이다. (“folding” the Batch Normaliza) 그러나 그렇게하면 특정 모델을위해 측정하고 비교해야할 것이다.


여러분의 모델이 CPU에서 실행될때와 GPU에서 실행될때 같은지도 확인해야할 것이다. 내가 언급한것처럼 Core ML은 모델을 CPU에서 돌릴지(Accelerate 프레임워크를 사용하여) GPU에서 돌릴지(Metal을 사용하여) 정한다. 결국 이 두 구현은 다르게 동작할 것이다. 그러니 둘 다 테스트해봐야한다!

예를들어 MobileNet은 "depthwise" 컨볼루션 레이어라 불리는 것을 사용한다. 이 원래 모델은 Caffe에서 학습되었고, 정규 컨볼루션의 groups 프로퍼티와 아웃풋 채널 수를 같게 만들어 depthwise 컨볼루션을 지원한다. 결과로나온 MobileNet.mlmodel 파일은 동일하지 않다. 이것이 iOS 시뮬레이터 안에서는 잘 동작하겠지만 실제 디바이스 위에서는 크래쉬가 난다!

시뮬레이터는 Accelerate 프레임워크를 사용하지만 실제 디바이스는 Metal Performance Shaders를 사용하여 생긴 문제이다. 그리고 Metal이 데이터를 인코딩하는 방법때문에 MPSCNNConvolution 커널은 제한되어 그룹의 수와 아웃풋 채널의 수를 같게 만들 수 없게 되었다. 아이고!

나는 애플에 버그리포팅을 제출했지만 내 요점은 이렇다. 시뮬레이터에서 모델이 잘 동작한다는게 디바이스에서 잘 동작할거라는 의미는 아니다. 테스트를 해보아라!

얼마나 빠른가?
나의 새로운 10.5" iPad Pro가 다음주까지 도착하지 않기 때문에(역자: 부럽습니다) Core ML의 속도를 시험해보지 못했다

나는 특별히 MobileNets을 실행시키는데 나의 Forge library를 이용할때와 Core ML을 이용할때 그 속도 차이가 어떻게 되는지 관심이 갔다(아직 초기 베타 단계에 있다).

채널을 고정하라! 공유할 데이터가 생기면 이 섹션을 업데이트 할 것이다.

Vision
다음으로 이야기할것은 새로나온 Vision 프레임워크이다.

여러분도 이름에서 추측했을지 모르겠지만, Vision은 컴퓨터비젼(computer vision) 테스크를 실행시켜준다. 과거에는 이를위해 OpenCV를 사용해야했었는데, 이제 iOS는 자신만의 API를 가지게 되었다.

Vision이 수행할 수 있는 일의 종류들이다
  • 이미지안에서 얼굴 찾아내기. 각 얼굴에대해 사각형을 만들어준다.
  • 안면의 세부적인 특징 찾아내기. 눈이나 입의 위치나, 머리의 모양 등.
  • 이미지에서 사각형모양으로된 것을 찾아내기. 표지판같은것들.
  • 비디오에서 움직이는 물체 추적하기.
  • 수평성 각도 알아내기
  • 두 이미지를 변형하여 그 내용을 정렬하기. 사진들을 합성할때 유용하다.
  • 이미지안에 포함된 텍스트 영역 감지하기.
  • 바코드 감지와 인지.
이 작업들중 몇몇은 이미 Core Image와 AVFoundation으로 가능하지만 이제 일관된 API로 한 프레임워크안에 들어왔다.

여러분의 앱이 이 컴퓨터비젼 작업중 하나가 필요할때, 더이상 여러분이 직접 구현하거나 다른 라이브러리를 사용할 필요가 없다. 그냥 Vision 프레임워크를 사용하면 된다. 더욱 강력한 이미지 처리를위해 Core Image 프레임워크와 합쳐서 사용할 수도 있다.

더 나은 것은, Core ML을 작동시키기위해 Vision을 사용할 수도 있다는 점이다. 그리하여 뉴럴 네트워크를 위한 전처리 단계로서 이런 컴퓨터 비젼 기술을 사용할 수 있게 해준다. 예를들어, 사람 얼굴의 위치와 크기를 감지하는데, 그 영역에 비디오 프레임을 자르는데, 이미지에서 얼굴이 있는 부분에 뉴럴럴 네트워크를 실행시키는데 Vision을 사용할 수 있다.

사실 이미지나 비디오와 함께 Core ML을 사용하면 항상 Vision을통해 가는것이 알맞은 방법이다. 가공되지않은 Core ML로는 여러분의 입력 이미지는 모델이 예상하는 포멧안에 있도록 해야하지만, Vision으로는 프레임워크가 이미지 리사이징 등에 주의해야한다. 이것으로 여러분의 추가 노력을 조금 절약해줄 것이다.

코드에서 Core ML을 작동시키기위한 Vision 사용은 다음과같이 생겼다.
// the Core ML machine learning model
let modelCoreML = ResNet50()

// link the Core ML model to Vision
let visionModel = try? VNCoreMLModel(for: modelCoreML.model)

let classificationRequest = VNCoreMLRequest(model: visionModel) {
  request, error in
  if let observations = request.results as? [VNClassificationObservation] {
   /* do something with the prediction */
  }
}

let handler = VNImageRequestHandler(cgImage: yourImage)
try? handler.perform([classificationRequest])
VNImageReuestHandler가 요청하는 오브젝트의 배열을 받아서, 아래처럼 여러 컴퓨터 비젼 작업을 함께 연결하여 할 수 있음을 인지하자.
try? handler.perform([faceDetectionRequest, classificationRequest])
Vision은 컴퓨터 비젼을 사용하기 아주 쉽게 만들어준다. 그러나 기계학습 사람들에게 멋진 일은 컴퓨터 비젼 작업의 아웃풋을 받아서 Core ML 모델에 넣을 수 있다는 점이다. Core Image의 파워와 합쳐지면 이미지 처리 파이프라인을 하나로 만들게된다!

Metal Performance Shaders
내가 말하고싶은 마지막 주제는 Metal이다. 이것은 애플의 GPU 프로그래밍 API이다.

올해 클라이언트를위한 많은 내 일거리는 Metal Performance Shaders (MPS)로 뉴럴 네트워크 구축과 최적화된 퍼포먼스로 맞추는 작업이 포함되있었다. 그러나 iOS10은 컨볼루션 네트워크를 생성하기위한 기본적인 몇 커널만을 제공했었다. 종종 이 갭을 채우기위해 커스터마이징한 커널을 짜야했다.

그러니 나는 iOS11에서 여러 이용가능한 커널 수가 늘었을때 행복했고, 그 이상의 기분이었다. 이제 그래프를 구축하는(building graphs) API를 가진다.

주의: 왜 Core ML 대신에 MPS를 사용할까? 좋은 질문이다! 가장 큰 이유는 Core ML이 여러분이 원하는 것을 지원하지 않거나, 프로세스 전체를 컨트롤 하고싶고 가능한 최대의 속도를 짜내고 싶을때 사용한다.

MPS에서 기계학습을위한 큰 변화들은 다음과 같다.
  • Recurrent neural networks. 이제 RNN, LSTM, GRU, MGU 레이어를 생성할 수 있다. 이것들이 MPSImage 오브젝트의 시퀀스에도 동작하고, MPSMatrix 오브젝트 시퀀스에도 동작한다. 이것이 흥미로운 이유는, 다른 모든 MPS 레이어들이 이미지만 다른다는 것이다(그러나 확실히 텍스트나 이미지가아닌 데이터와 작업할때는 매우 불편하다).
  • 더 많은 데이터 타입들. 이전의 가중치는 32비트 부동소수라고 가정했었는데, 이제 16비트 소수, 8비트 정수, 심지어 바이너리까지 될 수 있다. 컨볼루션과 완전히 연결된 레이어들은 바이너리 가중치와 바이너리화된 인풋으로 할 수 있다.
  • 더 많은 레이어들. 지금까지 우리는 plain-pld convolution과 max/average pooling으로 만들어야 했었다. 그러나 iOS11 MPS는 dilated convolution, subpixel convolution, transposed convolution, upsampling과 resampling, L2-norm pooling, dilated max pooling, 게다가 몇몇 새로운 활성 함수들(activation functions)을 가능하게 했다. 아직 MPS는 Keras나 Caffe 레이어 타입처럼 모든 타입을 가지진 않지만, 그 갭은 줄어들고 있다...
  • 더욱 편리함. Metal은 항번에 채널 4개의 분할로 데이터를 구성하는데(이미지가 MTLTextrue 오브젝트에의해 돌아오기 때문에),  그것때문에 MPSImage으로 작업하는것은 항상 좀 이상하다. 그러나 이제 MPSImage는 데이터를 읽고 쓰는 메소드를 가지므로 한결 편해질것이다.또다른 편리함은 레이어에 batch normalization 파라미터를 설정하게 해주는 새로운 메소드를 가지는 것이다. 이 말은 더이상 여러분이 컨볼루션 레이어 가중치에 batch normalization을 접지 않아도 MPS가 알아서 다 해줄것이라는 의미다. 매우 편리하다!


  • 성능 개선. 기존에 있던 커널들이 더 빨라졌다. 이 소식은 항상 좋다.
  • 그래프 API. 내 생각에는 이것이 큰 소식이다. 모든 레이어와 (임시의) 이미지를 직접 생성하는것은 항상 성가신다. 이제 Keras에서처럼 그래프를 표현할 수 있다. MPS는 이미지가 얼마나 커져야하는지, 패딩을 어떻게 다뤄야하는지, MPS 커널의 offset을 어떻게 설정할지 등을 자동으로 계산한다. 뒷편에서는 퓨징 레이어(fusing layers)로 그래프까지도 최적화시킬 수 있다.
이제 모든 커널들이 NSSecureCoding으로 시리얼라이즈 가능해보이는데, 이 의미는 그래프를 파일로 저장하여 나중에 복구시킬 수 있다는 의미이다. 그리고 이 그래프로 인터페이스를 사용하면 이제 그냥 한 메소드 호출만 하면 된다. 아직 Core ML만큼 쉽지는 않지만, MPS 사용이 확실히 이전보다 작업이 많이 줄었다.

내가 생각하기에 아직 분명하지 않은 것은 자신만의 컴퓨트 커널을 작성할 수 있는지, 그래프에 이것을 붙일 수 있는지이다. 내 클라이언트 작업에서 나는 전처리 과정에서 종종 필요했었고, Metal Shading Language로 작성된 커스텀 shader를 필요로 했다. 내가 말할 수 있는 부분은, "MPSNNCustomKerneNode" 클래스가 될것 같진 않다. 더 조사할 필요가 있어보인다!

결론: 기계학습을 위한 Metal Performance Shaders은 iOS 11과함께 더 강력해졌지만, 아마 많은 개발자들이 (내부적으로 MPS를 사용해가며) Core ML와 붙여 사용할 수 있다.
주의: 여러분의 앱이 계속 iOS 10을 지원하지 않는한, 새로나온 그래프 API는 내 Forge library를 쓸모없게 만들었다. 곧 예제 앱을 새로나온 그래프 API로 포팅할 것이고, 그것에대한 세부적인 내용을 블로그에 포스팅할 예정이다.

남은것들
발표된 것 중에 다른 부분이다.
  • Accelerate: Accelerate 프레임워크에서 BNNS는 기능적 업데이트가 크게 일어나진 않았다. 결국 Softmax 레이어가 나왔지만, MPS가 얻은 새로운 레이어 타입은 없었다. 아마 맞을것이다. 딥 뉴럴 네트워크를위한 CPU 사용은 어쨌든 좋은 아이디어가 아닌것같다. 이 말은, 나는 Accelerate를 사랑하고, 이것으로 많은 즐거움이 있었다. 그리고 이번년도에 나는 스파스 메트릭스를 더 지원했었는데, 꽤 멋졌다.
  • 자연어 처리(Natural Language Processing): Core ML은 이미지만을 위한게 아니라 텍스트를 포함한 수많은 종류의 데이터를 다룰 수 있다. 이 API에는 NSLinguisticTagger 클래스를 사용하는데, 얼마간 사용해봤지만 iOS 11이 나오면서 더욱 효과적게 되었다. 이제 NSLinguisticTagger는 언어 식별, 토큰화, part-of-speech tagging, lemmatization, Named Entity Recognition을 한다.
나는 NLP에 많은 경험이 없으므로 다른 NLP 프레임워크에대해 어떤식으로 stack up 되었는지 말할순 없을것 같지만, NSLinguisticTagger는 보기에 꽤 강력해 보인다. 여러분의 앱에 NLP를 넣고 싶으면 이 API로 시작하기에 좋아보인다.

이 모든게 좋은 소식인가?
애플이 개발자를위해 이 모든 새로운 툴을 제공하는것은 훌륭한 일이지만, 애플의 많은 API에는 중요한 "문제"가 있다.
  1. 오픈소스가 아니다
  2. 제한을 가진다
  3. 새로운 OS가 배포될때만 업데이트를 한다
이 세가지가 함게 있으면 애플의 API는 항상 다른 툴들에비해 뒤떨어질것이다. 만약 Keras가 멋진 새로운 레이어 타입을 추가하면 애플이 그 프레임워크와 OS를 업데이트 하기 전까지 Core ML로는 사용할 수 없을 것이다.

그리고 API의 어떤 부분은 여러분이 원하는대로 동작하지 않을때, 내부로 들어가서 고칠 수 없다. 여러분은 이것으로 작업해야하거나(항상 가능하진 않다) 다음 OS 배포까지 기다려야한다(모든 사용자들이 업그레이드하도록 해야한다).

물론 나는 애플이 비밀 소스를 줄거라 생각하진 않지만, 많은 다른 기계학습 툴킷들이 오픈소스니 Core ML도 오픈소스로 만드는 건 어떨까?🙏

애플이 이것을 아는것은 아마 빠른 시일안에 일어나진 않을 것이지만, 여러분의 앱에 기계학습을 넣기로 했을때는 적어도 위의 내용들을 마음에 담아주자.

더 읽을거리...



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Catching Leaky View Controllers Without Instruments

레테인 사이클(retain cycles)에의해 생긴 메모리 누수를 찾아내는 방법 중 잘 알려진 방법은, 더이상 화면에 있지 않을때 모든 뷰컨트롤러들이 디얼록되었는지 확인하는 것이다. 이 과정은 각각이 할당해제되기 전에 직접 반복해서 시행해보아야하는데, 재밌지도 않을 뿐더러 여기서 에러를 만들어내기도 한다. 만약 좀 더 일찍 UIViewController 누수에대한 
과정을 알 수 있었다면, 매일 개발하는동안 훨씬 낫지 않겠는가?

UIViewController의 잘 알려지지 않은 두개 프로퍼티에대해 감사하게 될 수도 있다.
  • isBeingDismissed: 모달로 나타난 뷰컨트롤러가 dismiss될 때, 이 프로퍼티는 true이다.
  • isMovingFromParentViewController: 부모 뷰컨트롤러로부터 이 뷰컨트롤러가 제거될 때, true가 된다. 이것은 UINavigationController 스택에서 뷰컨트롤러가 pop될 때와 같은, 시스템 컨테이너에서 제거되는 것도 포함된다.
이 프로퍼티 중 하나가 true면, 곧 뷰컨트롤러가 디얼록될 수 있다는 것을 알 수 있다. 정확히 얼마나 있어야 내부적으로 정리된 상태를 만들고 ARC가 디얼록할지는 모른다. 간단하게 생각해서 2초보다 더 길지는 않을 것이라 가정하자.

우리가 생각한 것을 코드로 만들어보자.
extension UIViewController {
   public func dch_checkDeallocation(afterDelay delay: TimeInterval = 2.0) {
       let rootParentViewController = dch_rootParentViewController

       // We don’t check isBeingDismissed simply on this view controller because it’s common

       // to wrap a view controller in another view controller (e.g. in UINavigationController)

       // and present the wrapping view controller instead.

       if isMovingFromParentViewController || rootParentViewController.isBeingDismissed {
           let type = type(of: self)
           let disappearanceSource: String = isMovingFromParentViewController ? "removed from its parent" : "dismissed"

           DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { [weak self] in

               assert(self == nil, "\(type) not deallocated after being \(disappearanceSource)")
           })
       }
   }

   private var dch_rootParentViewController: UIViewController {
       var root = self

       while let parent = root.parent {
           root = parent
       }

       return root
   }
}
흥미로운 부분은 asyncAfter(deadline:execute:) 호출에서 일어난다. 먼저 sefl를 weak로 만들면([weak self]), 나중에 호출할 클로저에의해 리테인되지 않는다. 그리고 self가 nil이면 assert를 건다(여기서 self는 UIViewController). 뷰컨트롤러가 살아있게 잡아두는 리테인 사이클을 가질때만 nil이 아니다.

이제 마지막으로 해야할 일은 모든 뷰컨트롤러의 viewDidDisappear(_:)에서 dch_checkDeallocation()을 호출하는 것이다(부모로부터 제거되거나 dismiss된 후에도 살아있도록 잡아두는 것을 제외하고)
override func viewDidDisappear(_ animated: Bool) {
   super.viewDidDisappear(animated)

   dch_checkDeallocation()
}
만약 누수가 일어나고 있으면, assert에서 실패나 나올 것이다(-Onone 빌드에서만 가능).

여기서 우리는 그냥 메모리 그래프 디버거(Memory Graph Debugger)를 열어 사이클의 원인을 찾고 고치면 된다.

이 방법을 통해 새로 소개된 리테인 사이클을 빠르게 배우는데 도움이 될거라 생각한다. 여러분도 이 방법이 즐거웠길 바란다! production-ready 코드(더 많은 주석과 if DEBUG 확인으로)는 GitHub의 DeallocationChecker에서 확인해볼 수 있다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: iOS — Identifying Memory Leaks using the Xcode Memory Graph Debugger

이 짧은 포스팅에서 내가 설명할 것은 아래와 같다
  • Xcode의 메모리 그래프 디버거란
  • 이것을 어떻게 사용하고, 몇가지 팁들
  • 장/단점들

이것이 무엇인가
짧게말해 메모리 그래프 디버거는 다음 질문의 답변에 도움을 준다. 한 오브젝트가 왜 메모리에 남아있는가?

Xcode의 메모리 그래프 디버거는 레티인 사이클과 메모리 누수를 찾아내고 고치는데 도움을 준다. 그 일이 발생하면, 앱 실행이 일시정지되고 현재 힙에서 오브젝트를 보여주는데, 이 오브젝트가 살아있게 하는 참조들이 무엇인지 그 관계와 함께 나타난다.


어떻게 사용하는가
리테인 사이클과 메모리 누수를 식별하는데 3가지 간략한 단계가 있다.
  • 1. 아래처럼 Xcode scheme editor로 stack logging에 체크한다.

live allocations을 위해 Malloc stack logging을 켜기live allocations을 위해 Malloc stack logging을 켜기


'Live Allocation'의 logging만 켰다. 이것은 디버깅할때 'All Allocations'를 선택하는것보다 오버헤드가 적고, 레테인 사이클과 누수를 식별하는데 필요한 것이다.
  • 2. 분석하고 싶은 앱을 실행시키고(리테인 사이클이나 누수가 의심되는 행동), 그 디버그 바 버튼을 선택하여 메모리 그래프 디버깅 모드로 들어가자.

메모리 그래프 디버깅 버튼메모리 그래프 디버깅 버튼

  • 3 .메모리 그래프 디버거는 앱 실행을 일시정지하고 아래를 보여준다.

Xcode의 메모리 그래프 디버거 모드Xcode의 메모리 그래프 디버거 모드

왼편에 디버그 네비게이터가 앱의 힙 내용을 보여준다.

디버그 네비게이터에 type/instance를 선택하면 가운데 패널에서 인스턴스 참조들을 보여준다.

가운데 참조 패널에 인스턴스를 선택하면 오른편의 인스팩터 페널에서 일반적인 오브젝트 메모리 정보와 allocation backtrace를 보여준다.

누수는 아래처럼 디버그 네비게이터에서 볼 수 있다.

디버그 네비게이터에 나타난 누수디버그 네비게이터에 나타난 누수


  • 1. 메모리 누수를 식별하는데 도움이 되기위해서, 아래처럼 누수만 보이도록 힙 내용을 필터링할 수 있다.

메모리 누수를위한 필터링메모리 누수를위한 필터링

  • 2. 런타임 이슈 네비게이터도 유용한데, 식별된 모든 누수의 숫자를 표시한다.

수많은 메모리 누수들!수많은 메모리 누수들!


좋은점과 나쁜점
  • 좋은점: 운좋게 누수를 쉽게 찾아낼 수 있다(간단한 리테인 사이클). 예를들어 한 오브젝트가 클로저 프로퍼티 안에서 자신을 붙잡고 있을때. 이건 붙잡고 있는 참조를 weak로하여 쉽게 고쳐진다.
  • 나쁜점: 잘못 알려주는 경우. 예를들어 UIButton 오브젝트를 만들고 UIToolBars 아이템 배열에 추가했는데, 메모리 누수로 나왔지만 그 이유는 볼 수가 없었다.


유용한 링크들

이게다다! 📱🚀👍🏽


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Vapor and its callback and throwing stacks

이 글은 스위프트로 쓰여진 서버 프레임워크, Vapor에 관한 이야기이다. 나는 iOS 개발자이며, 스위프트를 사랑하기때문에 내 개인 블로그를 이것으로 바꾸려고 하였다. 그렇게 하면서 내가 발견한 몇몇가지 특징들(미들웨어, 콜백체인, 에러핸들링)을 소개해주고 싶었다. 우리는 계층을 만들고 기능을 확장하고 프로젝트를 더 구조화하기위해 프로토콜과 익스텐션 사용을 할 것이다.

다소 간단한 서버로 시작해보자.
let drop = Droplet()  // 1



try? addProvider(VaporPostgreSQL.Provider.self) // 2

drop.preparations += Model.self // 3


drop.middleware.insert(RedirectMiddleWare(), at: 0) // 4

drop.middleware.append(GzipMiddleWare()) // 5

drop.middleware.append(HeadersMiddleware())

drop.get("/test", handler: {request in

    print("4")
    return try JSON(node: "This is a test page.")
})
Droplet 설정

droplet을 만듦으로서 시작하고(1), 데이터베이스 프로바이더를 추가하며(2) 함께 사용될 Model 엔티티를 추가한다. 그런다음 우리의 미들웨어를 추가한다. 미들웨어의 순서는 보통 상관으므로 append하면 된다. 그러나 가끔 구체적인 요구나 미들웨어중 하나를 가장 먼저 실행해야하는 경우가 있을 수 있다. 이 경우는 insert하여 스택의 제일 첫번째에 넣을 수 있다(4). 그리하여 내부에 다른 것들 이전에 첫번째것이 호출된다.

이제 미들웨어로가서, 이것이 어떻게 동작하는지 그리고 왜 이렇게 추가했는지 보자. Vapor는 Middleware라는 프로토콜을 제공하는데, 이것은 그냥 하나의 메소드를 필요로 한다.
func respond(to request: Request, chainingTo next: Responder) throws -> Response
미들웨어의 유일한 메소드


이 메소드를 통해 미들웨어는 요청을 받고, 적절하게 수정하여(혹은 그대로두어), 그것을 다음 응답자(next responder)에 보낸다. 이 응답자는 또다른 미들웨어일 수도 있고 엔드포인트의 핸들러일 수도 있다. 이 응답자는 응답을 반환하고, 적절하게 수정하여(혹은 그대로두어), 직접 응답을 생성하고 반환한다. 아래 예제에서 그것에대한 각각의 상황을 확인할 수 있다.

먼저 스택에서 RedirectMiddleware이다.
struct RedirectMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        print("1")
        guard request.uri.scheme != "https" else { // 1

            let response = try next.respond(to:request) // 2

            print("7")
            return response // 3

        }

        let uri = uriWithSecureScheme

        print("alternate")
        return Response(redirect:uri.description) // 4

    }
}
RedirectMiddleware

이 미들웨어의 유일한 목표는 현재 요청이 안전한지 확인하는 것이다(1). 안전하지 않으면 요청 URIhttps 스킴을 넣어 새로운 응답을 만들어 반환한다. 만약 안전하면 다음 응답자에게 보내고(2), 끝난다. 우리에게 반환된것이 어떤것이든 반환한다(3). 모든 요청이 안전하고싶기 때문에 모든 다른 미들웨어를 통해 요청이 통과될 필요가 없다. 그러므로 0번째 인덱스에 위치시킨다.

다음 미들웨어는 HeaderMiddleware이다.
struct HeadersMiddleware: Middleware {

    funcrespond(torequest:Request,chainingTonext:Responder)throws->Response{
        print("2")
        let response=try next.respond(to:request) // 1

        print("6")

        response.headers["Cache-Control"] = "public, max-age=86400" // 2


        // Disable the embedding of the site in an external one.

        response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
        response.headers["X-Frame-Options"]  ="DENY"

        return response// 3

    }

}
HeadersMiddleware

이것은 요청을 바로 다음 응답자에게 보내고(1), 응답이 리턴된 후에 여기에 몇 헤더를 설정하고(2), 반환한다(3).

이 스택의 다음으로 다음 응답자인 GzipMiddleware이다.
struct GzipMiddleware: Middleware {
    func respond(torequest: Request, chainingTo next: Responder) throws->Response {
        print("3")
        let response = try next.respond(to: request) // 1

        print("5")

        response.body = gzippedBody // 2

        response.headers["Content-Encoding"] = "gzip"// 3

        response.headers["Content-Length"] = gzippedBody.length

        returnresponse // 4

    }

}
GZipMiddleware

여기서도 바로 다음 리스폰더에게 응답을 보내고(1), 몇몇 헤더를 추가한다(3). 그러나 다른점은 이것을 리턴하기 전에(4) 바디를 바꾼다.

내부적으로 미들웨어는 어떻게 동작할까? 기본적으로 우리가 이것을 사용한 것처럼 된다. DropletResoonder 프로토콜을 따르는데, 이것은 그냥 한 메소드를 가지고 있다.
func respond(to request: Request) throws -> Response
Droplet의 응답 메소드

Middleware 프로토콜과 비교해보면, 이것은 아무거나 연결(chain)하여 호출될 수 없고 그 구현에서 모든 미들웨어를 연결한다.
extension Droplet: Responder {

    public func respond(to request: Request) throws -> Response {

        [...]

        print("0")
        let mainResponder = middleware.chain(to: routerResponder) // 1

        var response: Response


        do {
            response = try mainResponder.respond(to: request) // 2

        }
        catch {
            return Response(status: .internalServerError, headers: [:], body: "Error message".bytes)// 3

        }

        print("10")
        returnresponse// 4

    }

}
Droplet의 respond 메소드 구현

모든 미들웨어를 연결하여 만들어진 응답자(1)를 볼 수 있다(아직 호출되진 않았다). 그리고 요청의 바깥에 응답을 만드는데 사용되며(2), 모든게 괜찮으면 리턴될 것이다(4). 만약 실패하면 새로운 응답이 만들어지고 반환된다(3). 여기에는 에러메시지와 상태코드가 담겨있다. 미들웨어는 어떻게 연결되고(1) 호출될까? Collection을 익스텐션하고 Responder를 상속하여 이런 한 목표는 클로저를 잡아두고 호출하는 것이다.
extension Request {
    public struct Handler: Responder {
        public typealias Closure = (Request) throws -> Response


        private let closure: Closure


        public init(_c losure: @escaping Closure) {
            self.closure = closure // 1

        }

        public func respond(to request: Request) throws -> Response {
            return try closure(request) // 2

        }
    }
}


extension Collection where Iterator.Element == Middleware {

    func chain(to responder: Responder) -> Responder {
        return reversed().reduce(responder) { nextResponder, nextMiddleware in // 3

            return Request.Handler { request in

                return try nextMiddleware.respond(to: request, chainingTo: nextResponder) // 4

            }
        }
    }

}
Request와 Collection을 익스텐션하기

chain 메소드는 모든 미들웨어를 뒤집고(3) 각 단계에 새로운 Handler를 생성하고 반환한다. 이것은 요청과 현재 응답을 보냄으로서 앞에서 말한 Middleware 프로토콜의 메소드를 호출한다. 이렇게하여 우리의 미들웨어 스택(쉽게 설명하기위해 줄인말)을 기억한다면 [Redirect, Headers, Gzip]는 아래처럼 chain메소드를 통해 갈것이다.
reverse ->
[Gzip, Headers, Redirect] ->
create and return a Handler, that in its closure calls Gzip's respond(to: request, chainingTo: mainResponder) ->
create and return a Handler, that in its closure calls Headers' respond(to: request, chainingTo: gzipResponder) ->
create and return a Handler, that in its closure calls Redirect's respond(to: request, chainingTo: headersResponder) ->
return the Redirect Handler
미들웨어의 클로저를 연결하기

Dropletrespond 메소드는 반환된 Handlerclosure가 호출된 곳에 위치한다(2). 이것은 다음 그리고 다음을 호출할 것이다. 마지막으로 호출될 것은 get 메소드에서 전달된 Handler 클로저 이고, 이것은 응답/에러를 다시 연결한 처음의 것이다.
drop.get("/test", handler: { request in

    print("4")
    return try JSON(node: "This is a test page.")
})
get의 핸들러

마지막으로 모두 합치고 그 chain을 따라가보자. 안전한 요청은 이런식으로 보내진다.
create the main responder ("0") ->
RequestMiddleware ("1") ->
Other, internal middleware ->
HeadersMiddleware ("2") ->
GZipMiddleware ("3") ->
MiscMiddleware1 ->
MiscMiddleware2
안전한 요청 chain

그리고 응답은 이런식으로 보내진다.
get's handler ("4") ->
GZipMiddleware ("5") ->
HeadersMiddleware ("6") ->
Other, internal middleware ->
RequestMiddleware ("7") ->
response is sent to client ("10")
응답 chain

안전하지않은 요청은 아래처럼 약간 긴 경로를 따라간다.
get("/test") ->
RequestMiddleware ("1") ->
RequestMiddleware ("alternate") ->
start over ->
RequestMiddleware ("1") ->
[continue with the rest of the secure path]
안전하지 않은 요청 chain

여러분도 보았듯, 미들웨어의 주된 장점은 특화된 클래스로 더 작게 쪼개어 코드를 모듈화 시킬 수 있다는 점이다. 각각이 보통 하나의 작은 목적을 제공하는 덕분에, 더 쉬워지고 더 표현력있고 테스트/변경/제고하기 더 쉬워질 수 있다. 이것은 서로 전혀 모르며 며,이것들은 모두 체인의 위 아래에서 무슨일이 일어나는지 모른체 독립적으로 요청/응답이 동작한다.

체인을 넘기는 것은 정확히 같은 경로를 따르며, 굉장히 직관적이다. 만약 무엇이든 에러를 던졌는데 처리하지 않았다면 헨들러가 발견될때까지 요청/응답 체인에 돌려 보내거나 제네릭 에러 메시지와 함께 만들어진 Response에 내부 캐치(catch) 블럭(extension Droplet: Responder 예제코드에 있는 3)에 도달하게 될 것이다.

어떻게 에럴를 처리할 수 있는지 한번 보자. 먼저, 가장 직관적인 그 자리에서 처리하는 방법이다.
extension String: Error { } // 1


drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        returnJSON(answer) // 2

    }
    catch {
       return JSON("Answer couldn't be computed.") // 3

    }
})

drop.get("/pass-along", handler: { request in

   let answer = try findAnswer(in: request) // 4

   return JSON(answer) // 5

})


func findAnswer(for request: Request) throws->String {
   guard let answer = request.extract("answer") as? String else {
       throw "Answer parameter is not present." // 6

   }

  guard computingFinishedFast else {
       throw "Computing took too long to finish."
   }

   return answer
}
문자열 던지기

StringError를 따르게 하여(1) , 연관 값으로 만든 열거형을 만들어서 Error를 따르게 할 수 있으면 문자열을 던질 수 있다.

handle 엔드포인트 경우에, findAnswer에서 잘 처리했다면 answer로 만들어낸 JSON을 반환한다(2). 뭔가 문제가 있다면 에러를 캐치하고 에러로 만들어낸 JSON을 반환한다.

pass-along 엔드포인트의 경우,  모든것이 잘 처리되었다면 같은 시나리오대로 (5)를 적용하고, 에러가 던져졌다면 여기서 처리하지 않으므로 앞에서 언급한대로 콜백체인으로 보내질 것이다.

앞에서 본 것 처럼, findAnswer 메소드의 사용할 다른 엔드포인트를 추가하면 계속계속 에러를 다뤄야 할 것이다.

Vapor의 문서에서 추천해주는 다른 방법으로는, 모든 에러를 처리해주는 에러 처리 미들웨어를 만드는 것이다. AppError 엔티티를 만들고 findAnswer 메소드를 바꿈으로서 시작해보자.
enum AppError: Error {
    case argumentNotPresent(name: String)
    case computingTimedOut
}

drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        return JSON(answer) // 1

    }
    catch {
        return JSON("Answer couldn't be computed.") // 2

    }
})

drop.get("/pass-along", handler: { request in

    let answer = try findAnswer(in: request) // 3

    return JSON(answer) // 4

})


func findAnswer(for request: Request) throws -> String {
    guard let answer = request.extract("answer") as? String else {
        throw AppError.argumentNotPresent(name:"answer") // 5

    }

    guard computingFinishedFast else {
        throw AppError.computingTimedOut // 6

    }

    return answer
}
커스텀 에러 던지기

여전히 그 자리에서 에러를 다룰 수 있지만(1)(2), 에러를 다루기위한 미들웨어를 추가할 것이기 때문에 이것들을 함께 던져(3)(4) 어디로 갈지 정할 수 있다. 이제 findAnswer메소드는 기대하는 파라미터를 위해 연관 값과함께 AppError를 던지며(5), 타임아웃을 위한 에러도 던진다(6).

이미 알고 있듯, 한 메소드만 구현하면 되는데, 아래에서 어떻게 하는지 확인해보자.
struct ErrorHandlingMiddleware: Middleware {

    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        do {
            return try next.respond(to: request) // 1

        }
        catch AppError.argumentNotPresent(let name) { // 2

            throw Abort.custom( // 3

                status: .badRequest,
                message: "Argument \(name) was not found." // 4

            )
        }
        catch AppError.computingTimedOut { // 5

              throw Abort.custom(
                status: .requestTimeout, // 6

                message: "Computing an answer has timed out."
            )
         }
         catch { // 7

                return Response( // 8

                    status: .serverError,
                    message: "Something unexpected happened."
                )
          }
    }
}
ErrorHandlingMiddleware

우리가 해야할 일은 do-catch 블럭 안에서 다음 응답자에 요청을 보내주는 일만 하면 된다. 만약 모든 처리가 잘 되었다면(1), 요청은 get 헨들러에 도달할 것이고 응답은 우리에게 돌아온다(1). 그리고 체인을 따라 간 것을 반환할 수 있다(1).

findAnswer 메소드에서 뭔가 잘못되었다면 우리 스스로 캐치할 수 있게 만들수 있고 특정의 Abort 에러들을 만들어서 던질 수 있다(3). Abort는 Vapor에서 제공하는 열거형일 수도 있고 Response를 생성해서 반환할 수도 있다(8).

연관 값과함께 에러를 캐치하는 것은 우리에게 어떤 유연함을 제공해 주는데, 보이지 않는 파라미터를 추출해내거나(4) 다른 파라미터에 같은 에러를 던질 수 있는 가능성을 열어준다.
func findMeaningOfLife(for request: Request) throws -> Int {
    guard let answer = request.extract("meaningOfLife") as? Int else {
        throw AppError.argumentNotPresent(name: "meaningOfLife")
    }

    return 42
}
연관 값과 함께 에러 던지기

이제 적절한 상태와 함께(6) 개별적인 타임아웃 에러를 다룰 수 있게 되었다(5). 그리고 serverError를 반환하는 곳에 나머지 모든 것을 위한 catch 콜백도 가질 수 있다. 이 serverError는 제네릭 메시지이다.

마지막으로 droplet에  우리의 새 미들웨어를 추가한다.
// [...]
drop.middleware.append(ErrorHandlingMiddleware())
// [...]
에러 처리 미들웨어 추가하기

한가지만 더, 미들웨어의 괜찮은 기능을 소개하고 싶다. 서버당 설정(per-server configuration)이다. ErrorHandlingMiddleware를 제품판(production)에서만 동작하게 하고 싶다고 가정하자. 어떤 이유로 우리앱에서 부분적으로 크래쉬를 내고 싶다고 하자. 우리가 지금까지 봐온 방식대로 미들웨어를 붙이는 것 대신에, droplets은 설정파일을 제공하며, 아래와같이 사용할 수 있다.

먼저, 미들웨어(middleware)로 붙이는 것 대신 컨피겨레이블(configurable)로 미들에웨를 추가하자.
// [...]
// drop.middleware.append(ErrorHandlingMiddleware()) -> replaced with:
drop.addConfigurable(middleware: ErrorHandlingMiddleware(), name: "error-handling")
// [...]
컨피겨레이블 미들웨어 추가하기

Config/production/droplet.jsonConfig/staging/droplet.json 파일서 적절한 키를 추가하자.
// production/droplet.json
{
   ...
    "middleware": {
        "server": [
            ...
            "error-handling", // 1
            ...
        ],
        "client": [
            ...
        ]
    },
    ...
}

// staging/droplet.json
{
    ...
    "middleware": {
        "server": [
            ... // 2
        ],
        "client": [
            ...
        ]
    },
    ...
}
droplet.json

이제 앱 실행때 ErrorHandlingMiddleware는 제품판 서버의 미들웨어에 추가될 것이지만(1), (2) 단계에서는 아니다. 서버와 클라이언트 두가지 다에 미들웨어를 추가할 수 있고, Config/server-type/droplet.json에도 추가할 수 있다. 또한 그 배열의 순서에 따라 된다.
이 글의 끝에 도달하고 있는 것처럼 스위프트는 서버 세팅을 위한 실용적인 솔루션임을 볼 수 있다. 우리의 요청/응답을 수정하기 쉽게 하기 위해 프로토콜은 Middleware를 정의하게 해주고, 체인된 스택에 핸들러를 추가하는 다양한 방법을 제공한다. 또한 기본 에러 처리를 위해 문자열을 던지는 용도로 익스텐션 하는 것 뿐만 아니라, 프로젝트의 더 나은 구조를 만들기 위해 메인 스트럭쳐에서 Request.Handler를 분리해내는 것도 하였다.

서버와 클라이언트에서 둘 다 스위프트를 사용하면, 중복을 피하면서 모델, 기능, 핼퍼/유틸리티를 공유할 수 있고,  서버와 클라이언트를 전체적인 하나로 봄으로서 모든것을 더 쉽게 만들 수 있다.(역자: 이 문장은 조금 더 의논해볼 필요가 있는 것 같습니다.)

Vapor 그 자체로서, 프레임워크로서 선택인가?(As for Vapor itself, as the framework of choice?) 이 에 따르면 최고도 아니고 최악도 아니다. Vapor는 Express.js와 Sinatra와 비슷한 성격을 제공하며(이것들은 이전에 내 블로그에 쓰인 두 방법이었다.), 내 요구에 잘 들어 맞았다(블로그는 너무 많은 요청이 없으며, API를 필요로 하지 않는다).


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Optimizing your Swift Codebase with Attirbutes

편집자의 노트: 우리 커뮤니티 블로그 포스트 시리즈는 buddybuild 유저와 훌륭한 모바일 개발자들에의해 작성되었습니다. 이 포스트는 Jordan Morgan에의해 작성되었는데, Buffer의 iOS 개발자이다.

스위프트 + 속성들
모든 iOS 개발자들이 모두 여기에 있을거라 생각한다: 우리는 그냥 프로그래밍을 고르거나 어떤것이 이색적인 새로운 언어를 배우기 시작했다. 그리고 몇 코드를 만날 수 있다. 우리가 이해할 수 없을 수도 있지만, 이것이 동작한다고 생각한다. 따라서 그것에대해 좋은 확신을 가지고 계속하면 나아지지 않는다.

이 과정은 정확히 내가 어떻게 내 코드베이스에 스위프트 속성을 넣는것을 시작할수 있는지에 관한 이야기이다. 스위프트는 탄탄하고 다양한 속성들을 지원하는데, 깃헙 저장소를 검색해볼때 우리가 모르는 한두개를 볼 수 있을지도 모르겠다. 나의 경험과 비슷하다면 나중에 구글에 적어두면 당신만의 방법이 된다.

왜 스위프트 속성들을 이용할까?
속성은 우리 코드베이스의 코드 질을 희생시키지 않은채 효율성을 증진시키는데 도움을 준다. 그 코드는 더 읽기 쉬워지고, 컴파일하기 쉬워지고, 궁극적으로 유지보수하기 쉽고 사용하기에 더 안전하게 된다.

개인적인 프로젝트에서나 Buffer에서나 효율성은 항상 나의 iOS 개발의 중심에 있다. 이 포스트는 스위프트 개발자로서 내 효율성을 증진시킬 수 있는 몇가지 스위프트 속성들을 열거한다. 안으로 들어가보자...

기초
수많은 정의를 보기 전에, 속성이 정확하게 무엇인지 빠르게 짚고 넘어가야한다고 생각한다. 그리고 매우, 모든 스위프트의 속성은 타입이나 선언에대한 더맣은 정보를 준다. 이 정보는 어떤것이 메모리에서 어떻게 다뤄질지 컴파일러 경고로부터 모든것을 지시할 수 있다.

어떻게 생겼든, 모두 "@" 표시를 앞에 붙인다. 또한, 속성 선언은 파라미터로 감싸진것 안에 인자를 받을 수 있다.

여기 간단한 것이 있다.
@attributeName

//Or with arguments…
@attributeName(arguments)
아래 몇가지 예시를 보자.

속성들
@available(args) 속성
스위프트 속성중에 맥가이버칼은 @available()이다. 강력한만큼 유연하고, 매번 API를 관리하고 배포하는 입장이라면 반드시 필요한 것임을 발견할 수 있다. 이것으로는 API 네이밍이 바뀌었다는 것을 지시할 수 있고, 가능한 플랫폼을 지시하는 등을 할 수 있다.

블로그 포스트를 올리는 은유적인 API로부터 나온 오브젝트를 생각해보자.
class BasicPost {}
우리 API를 쓰는 사람들은 오랫동안 BasicPost 클래스를 즐겨 사용한다. 그러나 기술 블로그 포스트를 표현하는 오브젝트에서 더 원하는 몇몇 요청을 처리한다고 생각해보자. 여러분이 읽고 있는 것과 대체로 같다. 그러니 버전1.2에서 이것을 소개하였다.
class TechnicalPost {}
이제 우리 문서를 완성하기위해서,  이것이 존재한다는것을 알리기위해 @available() 이점을 취함으로서 API 사용자에게 정보를 제공하고 코드는 분별력있게된다.
@available(*, introduced: 1.2)
class TechnicalPost {}
이 특정 속성은 몇몇 이자를 받을 수 있지만, 첫번째 인자는 항상 의도하는 플랫폼을 나타낸다. 나머지 인자들은 그 순서에따라 공급될수 있게 지원한다.

와일드카드의 이점을 얻을 수도 있다. 이 경우, 와일드카드는 첫번째 인자로 들어가있는 별표이다. 이것은 사용하는 모든 플랫폼의 API에서 소통한다. 이 클래스는 버전1.2에서 처음 소개되었다(두번째 인자가 설명하는 바).

깔끔하지만, 꽤 길다. 고맙게도 더 단축된 문법으로 초점을 맞출 수 있다.
@available(iOS 10.0, macOS 10.12)
class TechnicalPost {}
더 낫다! 이 속성에대해 잘 알지 못하더라도, 이제 우리가 만들어낸 API는 이 클래스가 사용될때 명확하게 볼 수 있다.

그러나 그대로 두면 컴파일타임 에러가 나올 것이다. 왜일까?

애플은 새로운 플랫폼을 소개하는 경향이 있는데, 우리는 우리 코드에 이것을 설명해야한다. 그러기위해서 이 코드는 제공된 플랫폼과 다른 잠재적인 미래의 플랫폼에 사용가능하다는 마지막 인자로서 와일드카드를 넣는다.
@available(iOS 10.0, macOS 10.12, *)
class TechnicalPost {}
이 방법은 여러분의 코드가 다음 애플의 계획에대해 이미 준비해 놓았다는 뜻이다. For now though, 애플은 오늘날에 존재하는 각 플랫폼을 표현한 것들을 나열하여 제공하고 있따.
  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension

다음 속성으로 가보기 전에, 다른 공통성을 생각해보자. 최근의 변경에서, 우리 API가 쓰지않게 되었고, 세상에있는 iOS 개발자들은 기술 블로그 포스트를 표현하는 사랑스러운 약간의 JSON으로 제공하도록 책임을 맡게되었다.

이처럼, 원래 클래스는 더이상 필요없게 되었다. 그리고 디프리케이트시킬 시간이 된 것이다.
@available(*, deprecated: 1.3)
class BasicPost {}
한 스위프트에서 모든 플랫폼에서 BasicPost를 디프리케이스 시켜, 와일드카드 인자가 유용해질 수 있게 명확해졌다.

게다가 이것으로 계속하면서 조금씩 리팩토링 하고 싶으면, API 네이밍 변경사항을 알도록 제공해야 할것이다. 내가 애플로부터 발견한 기술의 예의는, ㅈ사용자들이 사용하기 더 쉽게 만들기 위해 사용불가능한 인자와 타입에일리어스로 짝을 지을 수 있다는 것이다.
//From an earlier API version
class BasicPost {}

//From a new API version, where we renamed it for whatever reason
class BaseTechnicalPost {}

@available(*, unavailable, renamed: “BaseTechnicalPost”)
typealias BasicPost = BaseTechnicalPost
개인적으로 이것을 좋아하는데, 효율성 관점에서나 비용적 관점에서, 가장 명확한 코드는 항상 최고의 코드이다.

이 속성은 워닝, 에러 등을 연결하여 보다 트릭적인 메시지를 제공한다(여기서 '트릭적인'은 옛 코드를 인자로 지정하는 것을 의미함).

@discardableResult 속성
성숙하고 물려받은 코드베이스에서 작업하고 있다면, 몇몇 함수가 약간 장황하게 되있는것이 놀랍지 않을 수 있다.

아마 90년대 초반부터 만들어진 기능이 지금까지 계속 추가되고 바뀐 코드를 물려받았으며 기능 가체에 결함이 없었을 것이다. (여기엔 소프트웨어가 켜질때 일어나는 124가지의 중요한 작업들이 있을 수 있다. 데이터베이스 접근, 케스 설정, 프로세스에서 몇몇 사인을 초기화하기 - 시나리오는 끝도 없다)

그 전까지, 당신이 돌아와서 필요한 리팩토링을 할 수 있게 프로덕트 매니저를 설득하고 있다.

그렇다. 그런가?! 그렇다.
let someUnusedVarBecauseIHaveToCallThisOldInsaneFunction = anOldInsaneFunction()
그러나 이상한 커플링 이유를 위해 이 함수를 실행하게 되버려서 clang은 이것들을 더 컴파일할 것이다. 이제 우리는 이것을 위해 보여주기위해 사용하지 않는 변수도 가진다. 아래로 내려가면서 이야기해보자.

필요 없을것같은 함수의 결과를 컴파일러에게 말하여 @discardableResult 속성이 도움이 될 수 있다. 컴파일타임에 경고도 없애준다.
@discardableResult func anOldInsaneFunction() -> String{
   //Bunch of business logic occurs
   return “”
}
이제 불러진 함수를 호출한 저 코드는 과거의 소프트웨어 공학적 실수처럼만 남아있을 것이다. 그러나 에러 없이 되고 있을 것이다.

명확성을 추가하기위해 간단하게 _를 할당하여 좀 더 명확한 상태로 만들 수 있다.
_ = anOldInsaneFunction()
때론 소프트웨어 개발에선 우리가 직접 제어하거나 고칠 수 없는 함수나 아키텍처가 있으며, 이 속성이 그런 상황을 조금이라도 더 낫게 해준다.

@autoclosure 속성
@autoclosure는 여러분의 코드베이스에 손쉽게 명확성을 추가할 수 있는 또다른 속성이다. 이것은 인자로 공급된 클로저를 자동으로 감싼다. 클로저가 아무 인자도 스스로 받으면 이것으로 감싸인 표현식의 실제 값을 반환할 것이다.

보기에 좀 혼란스러울 수 있는데, 한번 만나보면 쉽게 이해된다. 고수준 관점에서, 자동으로 클로저로 만들기위해 표현식을 얻는 능력에대해 이야기하고 있다. 여러분이 만약 프로젝트에 유닛테스트를 넣으려 해보았다면, 이미 이 속성들을 몇번 만나보았을지도 모르겠다.

아래처럼 클래스에 간단한 테스트를 넣고 싶다고 가정해보자.
class Programmer  {
   var pay:Int
   init(withPay pay:Int)
   {
       self.pay = pay
   }

   func applyRaise(by amount:Int)
   {
       self.pay += amount
   }
}

class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000
       let expectedSalaryPostRaise = devsPay + raiseAmount

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(expectedSalaryPostRaise, aDev.pay, "Unexpected salary after raise was applied.")
    }
}
XCAssertEqual의 앞에 두 파라미터는 둘다 클로저인데, 이 클로저는 제네릭 표현식으로 받는다. 함수의 시그니처가 약간 위협적으로 보이지만, 처음 두 파라미터는 @autoclosure의 이점을 취하였다는 것을 인지하자.
func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> T?, _ expression2: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line) where T : Equatable
@autoclosure 속성이 제공되었기 때문에, 함수를 호출하는 것이 꽤 가독성 좋고 우리에게도 쉬워졌다. 클로저를 값처럼 무언가를 간단하게 전달하거나(이전 예제에서 했던 것처럼) 약간의 로직을 더 전달할 수 있고, 각각은 가볍게 사용할 수 있다.
class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
   }
}
첫번째 인자가 제공될때, 클로저라기보단 추가적인 연산처럼 읽힌다.
XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
반대로 @autoclosure 속성이 없을때는 어떻게 생겼을지 보자.
XCTAssertEqual({
     return aDev.pay + raiseAmount
,}, {
   return 75000}
, "Unexpected salary after raise was applied.")
여러분도 볼 수 있듯, (문법대로) 완전한 클로저를 전달한다. 읽고 쓰기에 좀 많은 편이다. 하나가 마지막 인자로 후행 클로저(trailing closure)를 사용할 수 없으면, 복합적인 문제가 될 수 있다.

@autoclosure를 사용하면 그 안에 랩핑된 실제값을 반환한다. 아마 자동으로 파라미터가 클로저가 된다고까지 말할 수 있으므로... @autoclosure이다!

이 코드는 상속적으로도 지연된다. 실제 클로저가 결국 무거운 작업을 하고 있거나 의도치않은 사이트 이팩트를 낳았다면 추가적인 이점이 있다. 제공된 코드는 안에 감싸인 클로저가 되기 전까지 절때 실행되지 않는다.

더 나아가서, 여러분의 최근 iOS 작업물의 어디에서 보았을까? assert()는 어떨까?
struct Programmer
{
   var isSenior:Bool
   var appsShipped:Int
}

let aSeniorDev = Programmer(isSenior: true, appsShipped: 13)

assert(aSeniorDev.isSenior, “This dev isn’t a senior!”)
제공된 첫번째 인자는 @autoclosure를 사용한다. 그렇지 않았다면 이렇게 호출해야 했을 것이다.
assert({
     return aSeniorDev.isSenior
}, {
    return “This dev isn’t a senior!”
})
@autoclosure를 사용하면, 코드는 작성할때 좀 더 쉬워지고, 즐겁게 코드를 읽게 만드는 경험을 제공한다고도 말할 수 있겠다.

그리고 assert() 시그니처가 어떻게 생겼는지 궁금하다면, 이렇게 생겼을 것이다.
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line)
우리는 이 시그니처에서 두가지 파라미터로부터 벗어나면, 여기에 할당된 디폴트 값이 있기 때문에 예제에서 이것들을 생략한다. 여러분도 알다시피 그렇지 않을까?

여러 속성사용하기
각 속성은 그들이 가지고 있는 것으로 도와줄 수 있는 이점을 가지지만, 어떤 시나리오에서는 이것을 같이 사용하는 것도 도움이 된다.

예를들어, @escaping 속성을 보자. 이 @escaping 속성은 클로저에 들어온 것이 보냈던 함수보다 오래 살 수 있게 해준다.
//A property on a view controller
var onFakeCompletions:[()->()] = []

func fakeNetworkOp(_ completion:@escaping ()->())

{
   //Network stuff happens
   //The closure is appended to an external array outside of the function's scope. This implies it could be invoked outside of the function - i.e., it could "escape" it
   onFakeCompletions.append(completion)
}
이것을 고려하여 같은 파라미티터에 @escaping@autoclosure를 둘다 쓸 수 있다. 예제로 인사부(H.R.)를 생각해보자.  예를들어 임금 인상을위해 직급이 "Senio"인 개발자이면서 적어도 세개의 앱을 출시한 개발자가 누구인지 알려주는 인사부(H.R.)를 생각해보자. 이 개발자는 이력적인 이유로 각각의 평가를 계속 추적해야한다(As an example, let's imagine H.R. let us know that any developer who is both a "Senior" in title and has shipped at least three apps is due for a raise, but we also need to keep track of each evaluation for historical purposes).
class Programmer
{
   var previousPayRaiseEvaluations:[()->Bool] = []
   var isSenior:Bool = false
   var appsShipped:Int = 0

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true
aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true

aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1
확실히 속성들을 함께 "채이닝(chaining)"하는 것을 금지하지 않는다. 그리고 그 상황을 위해 호출할때 꽤 매끄럽게 동작한다.

마지막 생각
속성은 항상 내 코드에서 사용에대해 특별히 열정적으로 해왔던 것이다. 나는 명확하고 정확하고 간단한 방법으로 강력하게 무거운것을 들어올리는 아이디어를 좋아한다. 그리고 이것이 정말로 속성이 하는 일이다. 물론 스위프트와 Objective-C 프로젝트 어디에나 있는 @objc 속성같은 알만한 가치가 있는 것들도 있다.

이것을 고려하여, 한 인자는 속성이 nicety 보다는 necessity에 더 가깝게 만들어질 수 있다. 끝으로 개발자로서 이 모든것들은 여러분의 작업 플로우의 최적화에대한 것이다. 여러분의 코드베이스나 다른 곳에 이렇게 만들어라. 속성은 이런 최적화를 달성하기위한 하나의 방법에 불가하다.

앱이 완성되고나면, buddybuild 같은 서비스를 사용하여 여러분의 지속적 통합(CI, Countinuous Integration)과 배포 과정같은 부담되는 일을 자동화할 수 있겠다.

Buffer에서는 이미 우리 개발 작업 플로우를 최적화하기위한 방법을 항상 찾아보고있고, 이것들은 우리를 도와줄 수 있는 방법 중 우리가 찾은 것이다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: On Comments

코드를 작성할때 가장 큰 적은 복잡성이다. 추상화 수준(level of abstraction)을 유지하고, 코드베이스에서 작업하는 개발자들은 그 개념으로 동작하게 하는 것은 큰 소프트웨어 프로젝트에서 필수적이다.

주석는 복잡성을 유지보수하는 도구이기도하지만, 때로는 돕는게 아니라 미묘하게 코드베이스를 상하게 만들기도 한다.

주석의 내 견해는 두가지로부터 온다. 1) 주석은 컴파일하지 않는다. 2) 주석은 연하게 문법 하이라이트된다. 이것은 컴파일 되지 않고 시아에서 흐릿하게 보이기 때문에 코드에 변경사항이 생길때 무시되기 쉽다. 코드는 변경했는데 주석은 바뀌지 않는다면, 코드의 내용을 정확하게 반영하지 않은 주석과 함께해야한다.

이상적인 세상에서는 코드 리뷰때 이것을 발견할 수도 있지만, 실제로는 절때 발견하지 못할때도 있다. 코드 리뷰 툴이 문법 하이라이트 기능을 가지고 있더라도 주석은 그 문맥에서 백그라운드로 사라지게된다. 또한, 코드 리뷰는 변경된 부분의 주변만 보여주기 때문에, 오래된 주석은 그 자체가 변경될 쯤에만 발견될 수 있을 것이다. 만약 메소드의 전제조건을 변경하여 한 라인을 바꿨다면, 리뷰어는 주석을 보지 못한채로 바꿔라고 말하지 못할 것이다.

일반적인 주석을 피하는 몇가지 방법이 있다. 그 중 몇개는 a blog post by an old manager of mine에서 다루고, 나머지는 Andrew에서 나온 것인데, 루비에서 나온 것이므로 다이나믹 타입 언어에대한 이야기여서, 적용시킬 수 없다.

  1. 이름을 잘 정하자! 주석을 피하는 첫번째 방법은 다음과 같다. 한 단어 이름, 추상적인 이름, 모호한 이름을 피하자. 여러분이 할 수 있는 만큼 더 정확하게 이름을 정할수록 주석을 달 필요가 줄어든다.
  2. 메소드에 전제조건이 있으면 유효하지 않은 값이 들어올때 앱이 크래쉬내는 assertion를 넣자(항상 그러지는 말고 디버깅때만!). 만약 양수지만 0이아닌 정수만 받는다면 코드에 precondition(int>0)으로 작성할 수 있다.
  3. 런타인 assertion보다 나은 것이 컴파일타임의 것이다. 만약 메소드가 비어있지 않는 배열만 받는다면 precondition(!array.isEmpty)를 쓸 수 있다. 그러나 절때 빈 배열이 되지 않는 타입을 사용하는 것도 하나의 방법이다. 여러분의 API의 사용자들은 이제 절때 파라미터에 빈 배열을 보낼 수 없을 것이다.같은 성질에서, 두 이름을 case를 가진 열거형으로 표현된 것보다 더 잘 표현된 불(bool) 파리미터를 가져본 적이 있는가? 다른 열거형처럼 여러분의 옵셔널이 더 잘 표현되는가?  여러분의 의도를 네이밍에 드러내라.
  4. 임시 코드, 프로토타입 코드에 표시를 하자. 나는 종종 이 함수가 이상적으로 짜지지 않았으면 hack_를 표시한다. 스위프트에서 메소드의 언더바는 내 코드베이스를 좋지않게 만들어 괴롭히므로 보기에 거슬린다.그리고 이것을 상기시켜 고치게 만든다. 우리는 최근에 shouldReallyBeInTheCoordinator_를 접두에 붙인 함수를 만들었는데, 코드리뷰를 받아야야할때 코드가 올바른 클래스에 있지 않았기 때문이다. 올바르지 않은 코드가 거슬리게 생긴다면, 코드베이스 요구와 여러분의 감정이 드러맞게 된다. 또다른 좋은 접두에는 perf_temp_ 같은 것들이 있다.
  5. Mark Sands에 의하면, ID를 버그 추적기에서 메소드 이름으로 인코드 할 수 있고, 이것은 스택 트레이스(stack traces)에서 나타날 것이다. UIKit은 몇 케이스에서 레이더 넘버(Radar Number)를 참조한다. 이것은 현실이다.
    -[UIViewController _hackFor11408026_beginAppearanceTransition:animated:]
  6. 함수의 이름이 "왜" 이것인지 설명하는 것을 두려워하지마라. updateFrameOnNextTickBecauseAutoLayoutHasntCompletedYet(frame: CGRect)처럼 함수를 만들 수 있다. 컴파일러는 메소드 길이를 신경쓰지 않으며 코드는 작성하는 수보다 읽히는 횟수가 더 많다. 주석은 단지 단어들이며 메소드 이름이다. 그 코드베이스의 미래 유지보수 담당자는 여러분의 장황한 설명에 감사할 것이다.
  7. TODO(date: Date, message: String) 같은 보조함수(helper function)을 만들어라. 여기서 TODO는 어떤 날짜를 정하지 않으면 에러를 출력한다. (혹은 디버깅에서 크래쉬를 내는게 더 나을 수도 있다) Jordan Rose의 또다른 예제이다.
  8. 테스트에 어떤 알고리즘 요구사항을 인코드하라. 만약 위의 모든것이 실패하고, 특정 문제를 풀기에 precondition, 타입, 메소드이름에 의존할 수 없다면, 테스트를 작성하라. 이것은 특히 엣지 케이스에 좋다. 누군가 코드를 다시 작성했는데, 테스트에 실패한다면 새로운 코드안에 다뤄줘야할 케스이가 있다는 사실을 알게될 것이다.
기억하기: 받아드리기 힘든 코드를 작성하거나 주석이 없는 코드는 변명할 수 없다! 주석을 스킵한 코드는 반드시 명확해야한다. 결국에는 나를 위한 주석이다. 다음 프로그래머에게 내 의도를 표현할 수 있는 또다른 방법을 찾을 수 있다면 주석을 달지 않을 것이다.

더 읽을거리


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Swift: UserDefaults Protocol


스위프트3은 언어뿐만아니라 우리 코드베이스까지도 쓰나미같은 변경이 생겼는데, 이 글을 읽는 몇몇은 아직도 마이그레이션과 투쟁하고 있을지 모르겠다. 그러나 이런 모든 변경에도 stringly typed의 Foundation으로된 몇몇 API들이 남을 것인데, 이는 꽤 괜찮아 보이지만... 그렇지 않을 수도 있다.

이것은 일종의 애증관계인데, API에서 문자열의 유연성은 '애'이나, 그들이 가져오는 상속적인 이유때문에 이것을 사용해야 하는 것을 '증'한다. 신경쓰지 않으면 위험하게 작업하고 있는 것과 동일하다.

Foundation 프레임워크를 만드는 사람들은, 우리가 의도한대로 정확하게 미리 정의할 수 없게 해놓아서 우리는 stringly typed API를 쓸 수 있게 되었다. 그래서 그들의 모든 지혜로움, 능력, 지식으로 개발자로서 무한한 가능성으로 만들 수 있게 하려고 몇몇 API에서는 문자열을 사용하도록 해놓았다. 이것은 어둠의 비밀의 마법이다. (So in all their wisdom, power and knowledge, they decided to use strings in some of the APIs because of the unlimited possibilities it creates for us as developers. It’s either that or some type of dark arcane magic.)
(역자: stringly 타입의 API로 유연하게 사용할 수 있게 해놓은 장점을 말하는 중입니다.)

UserDefaults
오늘의 주제는 내가 iOS 개발을 하면서 배울때 처음으로 친숙해진 API 중 하나이다. 이것이 익숙하지 않는 사람들을 위해 설명하자면 UserDefaults는 한 이미지나 어플리케이션 세팅같은 작은 정보를 저장하지위한 간단한 영속 데이터 저장소이다. 어떤 사람들은 이것을 "다이어트한 코어데이터"라고 생각하기도하지만, 사람들이 그것의 대체물로 만드려 아무리 노력해도 이것은 견고하지 않다.

Stringly Typed API
UserDefaults.standard.set(true, forKey: “isUserLoggedIn”)
UserDefaults.standard.bool(forKey: "isUserLoggedIn")
일반적으로 앱에서 UserDefaults는 앱의 어디에서라도 값을 간단하게 영속적으로 저장(set)하고, 검색(retrieve)하며, 덮어쓰거나(override), 제거하는(remove) 할 수 있다. 그러나 조심하지 않으면 균일성이나 문맥이 없는채로 해볼 순 있겠지만 오타를 칠 가능성이 높아질 것이다. 이 포스팅에서는 UserDefaults의 일반적인 특징을 변형하여 커스터마이징 할 것이다.

상수를 이용하기
let key = "isUserLoggedIn"

UserDefaults.standard.set(true, forKey: key)

UserDefaults.standard.bool(forKey: key)
이런 이상한 트릭을 따라하면 일시적으로는 더 나은 코드를 작성할 수 있을 것이다. 문자열을 한번 이상 쓴다면 상수로 바꿔서 쓰는 규칙을 적용시켜보자. 아마 나에게 고마워 할지도 모르겠다.

그룹 상수
struct Constants {
    let isUserLoggedIn = "isUserLoggedIn"
}
...
UserDefaults.standard
   .set(true, forKey: Constants().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants().isUserLoggedIn)
균일성을 유지하기에 더 도움이 되는 방법은, 한곳에 중요한 디폴트 상수를 모아놓는 것이다. 그래서 여기에 디폴트를 저장하고 참조할 수 있는 Constants 구조체를 만들었다.

또다른 좋은 팁에는, 디폴트로 작업할 때 특히 프로퍼티 이름에 그 값을 반영해놓는 것이다. 이렇게하면 코드를 단순화 시켜주고 전반적인 속성을 더 균일화시켜줄 것이다. 프로퍼티 이름을 복사하고 문자열 안에 붙여넣으면 타이핑을 줄일 수 있을 것이다.
let isUserLoggedIn = "isUserLoggedIn"

문맥 추가하기
struct Constants {
    struct Account

        let isUserLoggedIn = "isUserLoggedIn"
    }
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account().isUserLoggedIn)
단지 Constants 구조체를 가지는 것만으로도 괜찮겠지만, 코드를 작성할 때 문맥을 제공해야함을 잊어서는 안된다. 여러분 자신을 포함한)함께 작업할 누군가에게 더 읽기 좋은 코드를 만들어야함을 목표로 하는것이 좋다.
Constants().token // Huh?
token의 의미가 무엇일까? 문맥에서 네임스페이스가 없기때문에, 이 코드베이스에 익숙하지 않은 누군가(혹은 미래의 이 코드를 관리하는 사람)가 token이 무엇인지 알아내려할때 고생할 것이다.
Constants.Authentication().token // better

초기화 피하기
struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }

    private init() { }
}
절때로 우리가 의도하지도 않았고 우리 Constants 구조체를 초기화시키고 싶지 않기 때문에, 생성자는 private로 선언되어야한다. 이거은 좀 더 예방적인 단계이지만 계속 추진하고 있는 방법이다. 최소한 적어도 static만 원할때도 실수로 인스턴스 프로퍼티를 선언하는 것을 막아줄 것이다. 그러나 static에 관해 말하자면... 다음을 보자.

static 변수들
struct Constants {
    struct Account
        static let isUserLoggedIn = "isUserLoggedIn"
    }
    ...
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account.isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account.isUserLoggedIn)
키에 접근할때마다 주의해야하는 것이 있는데, 접근할때마다 이것이 속한 구조체를 초기화해야할 수 있다. 그러지말고 static 선언을 사용하면 한번만 초기화한다.

구조체를 저장 타입으로 정했기 때문에 class대신 static을 사용한다. 스위프트 컴파일러 법에 따르면 구조체는 class 프로퍼티 정의를 사용할 수 없다고 한다. 또한 class 프로퍼티에 static 선언을 사용하면 그 프로퍼티는 final class로 선언한 것과 같다.
final class name: String

static name: String
// final class == static

열거형 케이스로 더 적게 타이핑하기
enum Constants {
    enum Account : String {
        case isUserLoggedIn
    }
    ...
}
...

UserDefaults.standard
    .set(true, forKey: Constants.Account.isUserLoggedIn.rawValue)
UserDefaults.standard
    .bool(forKey: Constants.Account.isUserLoggedIn.rawValue)
이 포스트의 초반부에서 말했듯, 균일성을 위해 프로퍼티는 그 값을 반영해야한다고 했었다. 여기에 static let 대신 enum case를 써서 그 과정을 자동화하여 한걸을 더 나가볼 것이다.

여러분도 인지했듯, 우리는 String을 따르는 Account열거형을 만들었는데, 이것은 RawRepresentable 프로토콜을 따른다. 이렇게 한 이유는, case를 위한 rawValue를 제공하지 않으려면 디폴트로 케이스가 반영될 것이기 때문에 이 작업을 한다. 우리가 해야할 타이핑이나 복사/붙여넣기를 줄이면 더 편해질 것이다.
// Constants.Account.isUserLoggedIn.rawValue == "isUserLoggedIn"

위에는 지금까지 UserDefaults로 꽤 괜찮은 것들을 달성했지만, 멋진것을 했다고 하기엔 좀 부족해 보인다. 가장 큰 문제는 문자열을 입고 있을지라도 여전히 stringly typed API로 작업하고 있어서 여전히 우리 프로젝트에 문제가 생길 수 있다는 점이다.

우리는 주어진 것으로만 작업할 수 있다는 마음을 가진다. 스위프트는 아주 많이 멋진 언어이고, 우리가 배워왔던, 그리고 Objective-C를 작성해가면서 알고 있는 많은 것들을 도전해볼 수 있다. 이 API에 문법 슈거를 만들어보자.

API 목표
UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
// APIGoals
남은 이야기에서는 일반적인 populus 대신, UserDefaults와 소통할때 더 괜찮게 작업할 수 있는 API를 우리 필요에 맞게 만들어보려 할 것이다. and what better way than to do so than making extensions with protocols.

BoolUserDefaultable
protocol BoolUserDefaultable {
    associatedType BoolDefaultKey : RawRepresentable
}
불리언 UserDefaults를 위한 프로토콜을 만들면서 시작해보자. 변수나 함수가 없는 간단한 프로토콜이다. 그러나 RawRepresentable을 따르는 BoolDefaultKey라는 associatedType을 지원하는데 왜 이렇게 했는지는 바로 다음에 이해할 수 있을 것이다.

익스텐션
extension BoolUserDefaultable
    where BoolDefaultKey.RawValue == String { ... }
만약 Crusty's Laws 프로토콜을 따르려는 계획이라면 프로토콜 익스텐션을 선언할 수 있다. 그러나 associatedTyperawValueString 타입이라는 익스텐션에만 제약하는 where절을 적용시켰다.
모든 프로토콜로, 동등하고 해당되는 프로토콜 익스텐션이 있다 - Crusty's Third Law
With every protocol, there is an equal and corresponding protocol extension

UserDefaults 세터
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
}
그렇다. 이것은 표준 UserDefaults를 감싼 간단한 API이다. 이렇게 하는 이유는 Key-Path로된 문자열을 보내는것 보다 간략한 enum case를 보내는게 가독성면에서 더 좋기 때문이다.
UserDefaults.set(false,
    forKey: Aint.Nobody.Got.Time.For.this.rawValue)

프로토콜 따르기
extension UserDefaults : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
}
우리는 BoolDefaultable을 따르게 하기 위해 UserDefaults를 익스텐션하고 RawRepresentable (String)을 따르는 BoolDefaultKey라는 연관타입을 지원했다.
// Setter

UserDefaults.set(true, forKey: .isUserLoggedIn)

// Getter

UserDefaults.bool(forKey: .isUserLoggedIn)
다시 말하자면, 작업하는 표준에 우리것을 정의하는 것 대신 우리가 지원한 API로 도전하는 중이다. UserDefaults를 익스텐션하면서 우리 API와함께 문맥을 잃어버리기 때문이다. 만약 .isUserLoggedIn 말고 다른 키 였다면 무엇과 관련되었는지 이해할 수 있었을까?
UserDefaults.set(true, forKey: .isAccepted)
// Huh? isAccepted for what?
이 키는 매우 모호해서, 모든 범주의 어떤것이든 될 수 있다. 이것처럼 보이지 않더라도 문맥을 제공하는 것은 항상 유익할 것이다.
필요하지만 가지지 못한것 보다는, 필요없더라도 가지고 있는 편이 낫다.
어렵게 생각하지 말자. 문맥을 추가하는 것은 쉬운 일이다. 간단하게 키를 위한 네임스페이스를 만든다. 이 경우, isUserLoggedIn 키가 있는 곳인 Account 네임스페이스를 만들었다.
struct Account : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn

    }
    ...
}
...
Account.set(true, forKey: .isUserLoggedIn)

충돌
let account = Account.BoolDefaultKey.isUserLoggedIn.rawValue
let default = UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue
// account == default
// "isUserLoggedIn" == "isUserLoggedIn"
같은 프로토콜을 따르고 같은 키 케이스를 제공하는 서로다른 두 타입을 가지는 것이 가능하다. 이걸 출시하기 전까지 해결하지 못하면 분명 이것이 새벽에 우리를 깨우는 버그가 될것이다. 다른 값을 바꾸는 키를 가지는 위험을 안고 갈 수 없다. 그러니 우리 키를 네임스페이스한 것으로 만들자.

네임스페이스로 만들기
protocol KeyNamespaceable { }
물론 우리는 스위프트 개발자니까 프로토콜을 만든다. 프로토콜은 우리가 직면한 문제를 풀때 제일 먼저 하게되는 시도일 것이다. 만약 프로토콜이 초콜릿 소스라면, 심지어 스테이크에까지도 어디든지 올려놓을 수 있다(역자: 왜하필 초콜릿 소스에 비유를 했는지.. 다목적 소스라면 역시 굴소스 아닌가요?). 이것이 우리가 프로토콜을 만들어가며 개발하는게 얼마나 좋은지 보여준다.
extension KeyNamespaceable {
    static func namespace<T>(_ key: T) -> String

    where T: RawRepresentable {
          return "\(Self.self).\(key.rawValue)"
    }
}
이 간단한 함수는 두 오젝트를 합친 문자열 보간법을 쓰고, 그 사이에 마침표로 구분했다. 클래스의 이름과 그 키의 rawValue이다. 이 함수는 RawRepresentable을 따르면 key 인자로 받을 수 있게 제네릭을 인자로 받도록 해놓았다.
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = namespace(key)

    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = namespace(key)

    return UserDefaults.standard.bool(forKey: key)
}
...

let account = namespace(Account.BoolDefaultKey.isUserLoggedIn)

let default = namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)


// account != default

// "Account.isUserLoggedIn" != "UserDefaults.isUserLoggedIn"

문맥
우리가 이 프로토콜을 만들었기 때문에, UserDefaults API 사용으로부터 해방된 느낌을 받고, 아마 프로토콜의 힘에 취했을 것이다. 이렇게하여 우리은 키를 우리가 원하는 곳으로 옮겨 문맥을 만듦으로서 코드를 읽을때 이해할 수 있게 되었다.
Account.set(true, forKey: .isUserLoggedIn)
그러나 API가 완전히 이해되지 않게 문맥을 잃어버리기도 했다. 처음 보면 이 코드가, 불리언을 영속적으로 저장시키는지 아니면 UserDefaults에 넣는지에대한 아무런 정보도 주지 않는다. 따라서 모든 사이클을 보여주기위해 UserDefaults를 익스텐션하여 우리의 디폴트 타입을 그 안에 넣을 것이다.
extension UserDefaults {
    struct Account : BoolUserDefaultable { ... }
}
...

UserDefaults.Account.set(true, forKey: .isUserLoggedIn)
UserDefaults.Account.bool(forKey: .isUserLoggedIn)


NatashaTheRobot에게 감사의 말을 전한다. 9월에 try! Swift NYC에서 발표할 기회를 얻었었다. 내 발표가 녹화되어 Realm에서 이것을 남겨두었고 Speaker Deck에 슬라이드 자료가 있으니 확인해보자. 발표를 한 이례로 몇가지 배운점을 이 글에 반영했으며, 샘플코드는 Gist나 Playground
에 있다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Boost Smooth Scrolling with iOS 10 Pre-Fetching API



이전 포스트에서 우리는 iOS 모바일 앱에서 부드러운 스크롤링을 달성하기위한 일반적인 전략을 살펴보았다. 이 전략을 적용시키는 주된 목표는 버벅이는 스크롤링(choppy scrolling)을 피하는 것이고 이런 일반적인 이슈는 사용자 경험에 부정적인 형향을 끼친다. 이런 작업으로 개발자들에게 도움이 되기 위해, 애플은 iOS10에서 UICollectionView를 매우 유용한 변화를 만들었다. 그러나 이렇게 새로이 소개된 기능을 검토하기전에, 우리를 위해 어떤 것이 필요한지 설명하면서 시작해보자.

어떤 이유로 스크롤링이 거칠게 되는가?
이따금씩 앱이 거친 스크롤이 될때를 본적이 있는가? 그 대답이 "그렇다"라면, 스크롤을 빠르게 하려고하는데 앱의 컨텐트가 더듬거리며 나올때 얼마나 실망하는지 경험해보았을 것이다. 여러분은 이런 거친 스크롤 동작이 어떤 이유에서 나왔는지 스스로에게 물어보았을 것이고, 이것이 나쁜 사용자 경험을 따라오게 한다.

짧은 대답은: 앱의 프레임이 떨어질때이다. 그러나 정확히 무슨 뜻일까?

부드러운 스크롤을 유지하려면, 앱은 안정적으로 60 FPS(Frame Per Second)로 표시해야한다. 다른말로 해보자면, 앱은 1초에 60번 컨텐트를 갱신할 필요가 있다. 이 의미는 각 프레임은 대략 16ms만에 렌더링 된다는 의미이다. 운이 나쁠 경우, 할당된 시간보다 더 많이 걸리고, 다음 프레임에 보여줄 데이터가 없어서 앱의 "프레임이 떨어지게" 된다. 이런 불행한 시나리오는 아래 다이어그램으로 표현할 수 있다. 파란색 표시는 그리는 작업을 지시하고 그것들의 생각은 렌더링을 완료하는데 필요한 시간을 표시한다. 여기서 볼 수 있듯, 두번째 프레임에서 우리는 할당된 시간(~16ms)보다 많은 작업이 드는 렌더링 이벤트를 가지는데, 결과로서 세번째 프레임이 드롭된다.


갱신 작업에 쓰인 CPU 시간의 뷰 포인트로부터 같은 시나리오를 보여줄 수 있다. 아래 그래프에서 위로 튀어올라가있는 부분은 앱이 현재 컨텐트를 갱신하기위해 예상했던 16ms보다 더 걸릴때, 프레임이 떨어지는 현상을 나타낸다.


좋은 사용자 경험을 달성하기 위해서는, 갱신 시간을 언제나 16ms 최대치보다 작도록 만들어야한다. 이상적으로는 훌륭한 사용자 경험(한번만이 아닌)을 만들고 싶으므로, 각 갱신 시간은 아래처럼 될 수 있다.
  • 지속적으로 허용된 시간(16ms)보다 아래로한다.
  • 가능한 다른 작업에 사용될 수 있는 CPU 시간을 절약한다.
 프레임을 떨어뜨리는 가장 일반적인 이유는 메인스레드에서 셀의 무거운 데이터 모델을 불러오는 것이다. 이 시나리오의 일반적인 예시는 다음과 같다.
  • URL을통해 이미지를 불러오기.
  • 데이터베이스나 코어데이터로부터 항목에 접근하기.

애플은 iOS10에서 셀들이 불러와지고 표시되는 방법의 최적화를 소개했었다. iOS10으로 증진시킬수 있는 방법을 보고, 부드러운 스크롤링의 사용자 경험을 만들기위해 개발자들이 어떠헥 더 쉽게 됐는지 보다.

iOS9에서의 셀 라이프사이클
UICollectionViewCell의 라이프사이클은 아래처럼 보여줄 수 있다.


컬랙션뷰와 그 셀 사이의 주된 인터렉션은 다음과 같다.
  • 컬랙션뷰는 보여지게될 셀을 위한 컨텐트를 필요로하고 있다. 그 셀은 visible 필드에 들어가는 것에 대한 것이다. collectionView(_ :cellForItemAt:)
  • 컬랙션뷰는 셀을 표시하는것을 요구하고 있다. 그 셀은 단지 visible 필드에 들어가있다. collectionView(_ :willDisplay: forItemAt:)
  • 컬랙션뷰는 셀을 제거하는 중이다. 그 셀은 visible 필드의 바깥에 있다. collectionView(didEndDisplaying: forItemAt:)

iOS10에서의 셀 라이프사이클
iOS10에서 셀의 라이프사이클은 iOS9에서와 거의 비슷하다. 그러나 몇가지 중요한 다른점이 존재한다.

첫번째로 다른점은 OS가 collectionView(_ :cellForItemAt:)를 사용하는것보다 훨씬 쉽게 호출한다는 것이다. 이것은 두가지를 의미한다.
  • 셀을 불러오는 무거운 작업은 셀이 표시되야한다는 필요가 있기 전에 완료될 수 있다.
  • 셀은 결국 표시되지 않을 수 있다. (visible 필드에 들어가지 않을 수 있기 때문에)

두번째로 다른점은 셀이 visible 필드에서 떠날때 그 안에서 일어나는 것이다. iOS10은 보통 collectionView(didEndDisplaying:forItemAt:)이 호출되는데, 셀은 즉시 재활용되지 않는다. OS는 사용자가 스크롤 방향을 뒤집는 경우를 대비해 몇개를 가지고 있는다. 그런 일이 일어나면 셀은 여전히 사용가능하고 컨텐트를 다시 로드할 필요 없이 (collectionView(_ :willDisplay: forItemAt:)가 호출되며) 다시 표시될 수 있다.


iOS9에서 일어나는 것과 비교해보자. 여러분도 알아차릴 것인데, 특정 유스케이스에대해 셀을 불러오는 무거운 작업(collectionView(_ :cellForItemAt:))은 더이상 필요없다. 이 최적화는 사용자가 빠르게 스크롤 할때나 스크롤 방향을 바꿀 때 모두 셀을 빠르게 렌더링할 수 있게 해준다.

세번째로 iOS9와 iOS10간에 중요하게 다른점은 여러행의 레이아웃으로된 컬랙션뷰에서 셀을 로드하는 방법이다.



iOS10은 visible 필드로 들어오는 각 셀마다 분리되어 로드된다(collectionView(_:cellForItemAt:)). 셀 라이프사이클을 설명하는동안 보았듯, 이런 일은 각 셀이 실제로 표시되여햘 때보다 훨씬 쉽다. OS가 다른 요청을 처리하고 셀 로드를 배치하여 최적화의 문을 열게되었다.

셀의 열이 visible 필드로 들어오면, 셀은 한 배치(single batch) (동시에 모든 열을 위해 collectionView(_ :willDisplay: forItemAt:)를 호출한다.)처럼 표시되는데, 이 작업은 CPU 사이클의 주기에서 아주 비싸지 않기 때문이다(적어도 셀 컨텐트를 불러오는 것과 비교해서).

프리-패칭하는 것은, 여러행의 레이아웃을 불러오는 것에 대한 이런 차이점은 iOS10의 UICollectionView 최적화에서 가장 중요한 점이다. 좀 더 세부적인 내용을 살펴보자.

프리-패칭 API
iOS10을 발표할때, 애플은 프리-패칭(Pre-Fetching)을 적용한 기술(Adaptive Technology)로서 소개했었다. 프리-패칭하는것은 스크롤 퍼포먼스 증진의 최적화를 위해 사용자들이 앱과 인터렉션하여 이점을 취하려고 할것이라는 의미다. 예를들어 이런 새로운 기술은 새로운 셀을 불러오기 위해(프리-패칭) 비어있는 시간을 찾아볼 것이다(사용자가 느리게 스크롤할때나 스크롤을 더이상 하지 않을때). 사용자 스크롤 패턴에따라 퍼포먼스같은 것을 실하기위한 프리-패칭의 기회를 더 (혹은 덜) 얻을 수 있다.

가능한 API를 검도하기 전에, 이 기술로 작업할 수 있는 최고의 실행을 살펴보자. 프리-패칭하는 것의 최고의 이점을 얻기위해 셀 컨텐트를 세팅하는 작업들은 반드시 collectionView(_ :cellForItemAt:)에서 실행되어야한다. collectionView(_ :willDisplay:forItemAt:)에서 실행되는 작업들과 collectionView(didEndDisplaying:forItemAt:)에서 실행되는 작업들은 최소화해야하고 CPU가 부담되지 않도록 만들어야한다. 더 나은것은, 이 라이프사이클 이벤트에 아무 작업도 실행시키지 않는게 더 최적화된 것일 것이다! 또한 collectionView(_ :cellForItemAt:)이 셀을 위해 호출될지라도 셀이 절때 표시되지 않을 수 있다는 가능성을 마음에 새겨두자.

어떤 굉장히 좋은 소식은 프리-패칭하는것이 iOS10에서 컴파일된 앱에는 디폴트로 되었다는 것이다. 그러나 UICollectionView의 isPrefetchingEnabled 프로퍼티를 false로 설정하여 이 기능을 끌 수도 있다. 우리가 구현해놓은 컬랙션뷰의 코드를 바꿀 필요가 없다는 뜻이다. 프리-패칭하는 기능의 이점을 얻기위해 취해야할 유일한 옵션은 Pre-Fetching API를 시행하는 것이다.

프리-패칭하는 API와 UICollectionView
UICollectionView를위한 프리-패칭하는 API는 UICollectionViewDataSourcePrefetching 프로토콜에 정의되있다. 이 API는 두가지 메소드를 따르도록 정의되있다.

collectionView(_:prefetchItemsAt:)(필수)—이 메소드는 [IndexPath] 파라미터에의해 지정된 셀의 데이터를 비동기적 로딩을 초기화해준다. 이 비동기 로딩은 Grand Central Dispatch를 통해 실행되거나 OperationQueue를 통해 실행될 수 있다. 이 메소드를 구현할때 중요한 점은 데이터 로딩의 burden을 메인큐에서 백그라운드큐로 이동하는 코드를 작성하는 것이다. 이것의 목표는, 새로운것을 표시하는 중요한 작업을 실행하는 많은 시간을 백그라운드 큐에서 보내도록 하기위해 데이터 로딩 부담을 메인큐에서 백그라운드큐로 넘겨 작업량을 줄이는데 있다.

collectionView(_:cancelPrefetchingForItemsAt:)(선택)—이 메소드는 더이상 필요없는 셀을 [IndexPath] 파라미터로 지정하여 데이터를 소통한다. 이 메소드를 구현하면 필요에따라 데이터 로딩이 되지 못한 것을 취소하는데 사용할 수 있고, 불필요한 작업을 취소하여 CPU 시간을 절감하는 좋은 방법이 된다(일반적으로 사용자가 스크롤 방향을 바꾸기 때문에).

우리가 이전에 말했듯, 프리-패칭하는것은 적용가능한 기술이다. 그러므로 위의 메소드는 앱과 인터렉트하는 사용자의 방법에따라 다르게 유발된다. 이것의 한 결말은 collectionView(_:prefetchItemsAt:)메소드가 컬랙션뷰의 셀마다 호출되지 않는다. 이 말은 collectionView(_:prefetchItemsAt:)를통해 셀을 로딩할때, 앱은 아래의 모든 시나리오를 다룰 수 있다.
  • 데이터가 미리 패치되었고 표시되기위해 준비되었다.
  • 데이터가 현재 패치되는 중이고 표시되기위해 준비되진 않았다.
  • 데이터가 아직 요청되지도 않았다.

프리-패칭하는 API와 UITableView
또한 iOS10은 UITableView를 위한 프리_패칭하는 것도 소개했다. UICollectionView에서 설명했던 모든 주요 개념들이 비슷한 방법으로 UITableView에 적용된다. UITableView를위한 프리-패칭하는 기술은 UITableViewPrefetchingDataSource 프로토콜에 정의되있다. 이 API는 아래 두 메소드를 정의한다.

tableView(_:prefetchRowsAt:)(필수)—이 메소드는 [IndexPath] 파라미터에의해 지정된 셀의 데이터를 비동기적 로딩을 초기화해준다.

tableView(_:cancelPrefetchingForRowsAt:)(선택)—이 메소드는 더이상 필요없는 셀을 [IndexPath] 파라미터로 지정하여 데이터를 소통한다.

UICollectionView 프리-패칭하는 API 메소드에대해 소개한 생각들은 같은 방법으로 비슷한 각 UITableView 프리-패칭하는 API 메소드에 적용된다.

결론
나는 iOS10에서 프리-패칭하는 기능을 적용시키기위해 UITableView와 UICollectionView의 샘플 코드를 수정해놓았다. 여기에서 확인할 수 있다.

이 포스트에서는, UICollectionView와 UITableView에대해 부드러운 스크롤 증진이 가능한 구현을 검토했다. 특히 새로운 OS 최적화에대한 모든 이점을 취할 수 있고 우리의 모바일 앱과 인터렉트할때 최고의 사용자 경험을 제공할 수 있는 특정 프리-패칭하는 API를 구현하여 보았다.

추가적인 링크




이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: My Development Toolset 2017 for iOS


모두들 반갑다! 내가 현재 맥북에서 사용하는 iOS 개발 툴, 잡다한 것, 서비스, 웹사이트, 프레임워크 툴에 대해 소개하고자 한다.

  • Git 사용을 위한 GitKraken를 추천한다. GitKraken은 자동으로 GitFlow를 추가할 수 있다.
  • 이슈에 대해서는 이 Gitscout를 따로 쓰고 있다.
  • GitBar는 커밋되지 않은 소스코드를 상기시켜준다.
  • Build Time Analyzer는 여러분 프로젝트의 스위프트 빌드타임을 쪼개에 보야주는 macOS용 앱이다.
  • Tomato One를 사용해서 Pomodoro Technique로 효율을 증가시키자.
  • WatchDog는 Xcode를 끄거나 macOS를 재시작하지 않은채 자동으로 DerivedData를 정리해준다.
  • Cakebrew는 GUI로 Homebrew를 관리한다.
  • Liya는 하나의 인터페이스에서 MySQL, PostgreSQL, SQLite3에 접근할 수 있다.
  • Quiver는 코드 조각 메니저이고 메모나 코드, 파일을 저장해놓는 노트이다. 5년동안 snippets를 사용했었는데, 스위프트가 추가되었다는 이유로 이것을 사용했다. 그러나 이제 바꿀때가 된것 같다.
  • Oh My Zsh와함께 터미널을 쓰면 터미널이 눈과 두뇌를 가지게 될 것이다.
  • 나중에 읽고 싶으면 Pocket에 담아두자. 사파리나 크롬 익스텐션은 설치하지 말자. LINER 이것도 있다.
  • 이미지를 작게 만드려면 Squash를 사용하자. 무료의 솔루션으로는 guetzli도 있다.
  • 여러분이 Sketch는 알거라 생각된다. 그러면 Zeplin라는 것도 있는데, 이것도 확인해보자.
  • 인터렉트 레이아웃 텍스트나 모든 화면 크기를 확인하고자할때는 RevealApp를 쓰자.
  • SizeUp는 단축키로 여러분의 창 크기를 조정하고 위치를 조정해준다.
  • BitBar는 모든 스크립트 출력을 macOS 메뉴바에 보여준다. 더 중요한 것은 리뷰의 평균 점수, 앱 상태, 버전, 이런 당신이 필요로하는 어떤것이든 넣을 수 있다. dev 플러그인을 확인하는 것을 잊지말자.
  • LittleIpsum 단어나 문장, 문단을 생성해준다.
  • 지금은 코드 snippets을 사용하고 있다.코드 snippets이 어디에 쓰였는지, 언제 쓰였는지 알고 싶다면 Paste는 클립보드 관리자이다.
  • Gitsome은 터미널에서 쓸 수 있는 멋진 Git/GitHub 커멘드라인 인터페이스이다.
  • Easy APNs Provider는 최고의 푸시 노티피케이션 테스팅 툴이다.
  • Houston은 애플 푸시 노티피케이션을 위한 간단한 gem이다.
  • Bee는 JIRA 클라이언트, JIRA 에자일 클라이언트, 깃헙 이슈 클라이언트, FogBugz 클라이언트, 마크다운 편집기의 모든 기능이 들어있다.

팟케스트
여러분이 팟케스트를 들을 시간이 있다면 PodcastMenu는 내가 좋아하는 맥용 앱이다.
  • Fedrico Viticci, John Voorhees의 AppStories를 확인해보아라.

프레임워크
  • 내 프로젝트에서 가짜 데이터가 필요하다면 Fakery를 추천한다.
  • LocalizationKit는 스위프트로 다이나믹 다국어 번역 배달 시스템이다.
  • Armchair는 스위프트로 작성된 간단하지만 강력한 앱 리뷰 관리자이다. iOS와 macOS용이 있다.
  • Siren는 설치된 iOS 앱의 버전을 확인하고 새 버전이 출시되었을 때 알림을 준다.
  • SwiftGen는 자동 스위프트 코드 생성을 위한 코드 생성기이다.
  • Bohr는 화면을 설정하기위한 초기 세팅을 할 수 있게 해준다.
  • SwiftyJSON는 JSON 파싱계의 최고봉이다. 만약 문제를 겪으면 kitura(링크) 버전을 확인해보자.
  • SwiftyBeaver는 스위프트를 위한 다채롭고, 유연하며, 가벼운 로그 툴이다. 그리고 탐색, 검색, 필터링을 위한 맥용 앱을 제공한다.
  • 스위프트에서 JSON 파싱 라이브러리의 대안으로는 ✨ Gloss가 있다.
  • Hero는 커스텀된 뷰컨트롤러 트렌지션을 제공한다.



웹사이트
  • AppSight는 iOS 모바일 앱에서 회사가 어떤 SDK와 서비스를 사용했는지 찾아준다.
  • iOSCookies는 스위프트로 작성된 iOS 라이브러리 컬랙션이다.
  • Ole Begemann는 iOS에대한 거장의 블로그이다.
  • littlebitesofcocoa는 iOS와 맥을 위한 팁과 기술이다.
  • 멋진 모바일 엔지니어링 블로그이다. 당신은 Toptal에 있는 모든 글을 읽으면 좋을 것이다.
  • Erica Sadun는 깊은 iOS 블로그이다.

잡다한것

서비스
  • 로컬에 MongoDB, MySQL, Jenkins, Minecraft을 설치하고 싶지 않으면 Docker가 그것에대해 혹은 그 이상으로 도움을 줄것이다. Docker를 사용하면 여러분의 프로젝트나 프로토타입을 위해 백엔드, 데이터베이스, 배포된 앱을 빌드할 수 있다. 스위프트를 위한 Docker를 확인해보자.
  • 여러분이 Docker를 사용하고 있다면, KitematicCaptain를 사용하면 좋다.
  • 클라우드 컴퓨팅 플랫폼이 필요하면 나는 digitalOcean를 즐겨쓴다.
  • 프로젝트를 관리하는데는 Asana를 사용한다.
  • rollout.io는 앱에서 크래쉬를 고치고 파라미터를 재정의한다.
  • GoogleAnalytics는 제품 사용을 분석해준다.
  • Zoommy에서는 한 곳에서 무료 사진을 찾을 수 있다.
  • heNounProject는 무료이며 높은 품질의 아이콘을 가지고 있다.

편집
  • Ulysses는 발행에있어서 최고의 맥용 앱이며, 마크다운 편집기, 글을 쓰는데 좋다.
  • Podfile과 함께 작업할때는 나는 Atom를 더 선호한다.
  • node.js나 express.js로 작업한다면 Brackets 사용을 선호한다. 여러분이 WWDC2016(링크) 테마를 다우받고 싶으면 여기서 다운받을 수 있다.

이게다다. 읽어줘서 고맙다. 이 모든 툴이 여러분의 생산성에 도움이 되길 바란다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Refactoring singleton usage in Swift

더 명료하고, 더 모듈화시키며, 더 테스트용이한 코드베이스를 위한 팁

소프트웨어 개발에서 싱글톤널리 권장되지 않고 눈쌀을 찌푸리게 만든다. 이것을 테스트하는 것은 어렵거나 불가능하고, 묵시적으로 다른 클래스에서 사용하면 여러분의 코드베이스는 헝클어져버린다. 또한 이것은 코드의 재사용도 어렵게 만든다. 오랫동안 싱글톤은 전역 변수나 가변 상태의 변형에 지나지 않다고 생각해왔다. 적어도 많은 사람들은 이 방법이 나쁜 방법이라는 것 정도는 인지하고 있다. 그러나 때때로 싱글톤은 피할수 없는, 필요한 독이기도 하다. 이것을 어떻게 깔끔하고 모듈화되고 테스트용이하게 우리 코드에 집어넣을 수 있을까?

싱글톤은 어디에나 있다
애플 플랫폼에서 싱글톤은 Cocoa와 Cocoa Touch 프레임워크 어디에나 있다. UIApplication.shared, FileManager.default, NotificationCenter.default, UserDefaults.standard, URLSession.shared 등이 있다. 또한 이 디자인 패턴은 Cocoa Core Competencies 가이드의 한 섹션으로 나와있다.

여러분이 묵시적으로 저 싱글톤(혹은 여러분의 싱글톤)을 참조할때 여러분의 코드를 변경하는데 드는 노력이 증가할 것이다. 싱글톤을 사용하는 클래스에서 싱글톤을 변경하거나 목(mock) 할 수 있는 방법이 없기 때문에 여려분의 코드를 테스트하기 어려워지거나 불가능해진다. 아래는 iOS 앱에서 일반적으로 볼 수 있는 것이다.
class MyViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let currentUser = CurrentUserManager.shared.user
        if currentUser != nil {
            // do something with current user
        }

        let mySetting = UserDefaults.standard.bool(forKey: "mySetting")
        if mySetting {
            // do something with setting
        }

        URLSession.shared.dataTask(with: URL(string: "http://someResource")!) { (data, response, error) in
            // handle response
        }
    }
}
이것이 묵시적인 참조이다(클래스 안에서 직접 싱글톤을 사용한다). 더 나아지게 할 수 있는데, 스위프트로 가볍고 쉬우며 의존성을 줄이는 방법이 있다. 또한 스위프트로 우아하게 만들 수도 있다.

의존성 주입
짧게 말해, 답은 의존성 주입이다. 이 원리는 여러분의 함수와 클래스를 모든 입력이 명시적으로 되게 설계하는 방법이다. 위의 코드를 의존성 주입을 사용하여 리팩토링 한다면 아래처럼 생겼을 것이다.
class MyViewController: UIViewController {

    let userManager: CurrentUserManager
    let defaults: UserDefaults
    let urlSession: URLSession

    init(userManager: CurrentUserManager, defaults: UserDefaults, urlSession: URLSession) {
        self.userManager = userManager
        self.defaults = defaults
        self.urlSession = urlSession
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let currentUser = userManager.user
        if currentUser != nil {
            // do something with current user
        }

        let mySetting = defaults.bool(forKey: "mySetting")
        if mySetting {
            // do something with setting
        }

        urlSession.dataTask(with: URL(string: "http://someResource")!) { (data, response, error) in
            // handle response
        }
    }
}
이 클래스는 이제 더이상 모든 싱글톤에 묵시적으로 의존하지 않는다. 이것은 명시적으로 CurrentUserManager, UserDefault, URLSession에 의존한다. 그러나 이런 의존성들이 이것들이 싱글톤이라는 것을 이 클래스는 전혀 모른다. 이런 세부사항들은 기능이 바뀌지 않은채 문제가 없다. 뷰컨트롤러는 단이 이 오브젝트의 인스턴스가 존재한다는 사실만 알고있고, 호출 시점에서 싱글톤을 담아 보낼 수 있다. 다시 말하지만, 이런 세부사항은 이번 수업의 관점과는 관련이 없다.
let controller = MyViewController(userManager: .shared, defaults: .standard, urlSession: .shared)

present(controller, animated: true, completion: nil)
프로 팁: 여기서 스위프트 타입 추론이 일어난다. URLSession.shared라고 쓰는 것 대신에 .shared라고 쓸 수 있다.

만약 다른 userDefaults를 줘야한다면(예를들어 App Groups과 데이터를 공유해야한다면) 바꾸기 쉽다. 사실 이 클래스에서는 아무것도 바뀌지 않아야한다. UserDefualts.standard를 보내는 것 대신에 UserDefaults(suitName: "com.myApp")을 보내야한다.

게다가 이제 유닛테스트에서 이 클래스의 가짜나 목(mock)을 보낼 수 있다. 실제 스위프트에서 목 하는 것은 불가능하지는 않으나 더 편한 workarounds가 있다. 이것은 여러분의 코드를 구성하고 싶은대로 할 수 있다. CurrentUserManager라는 프로토콜을 쓸 수 있는데, 이것은 테스트에서 "목"할 수 있다. 또한 테스트를 위한 가짜 UserDefault에 제공할 수 있고 URLSession을 옵셔널로 만들어 테스트에선 nil을 넣으면 된다.

리팩토링 지옥
이 방법에 열중하여 이제 기술적 빚을 안고있는 여러분의 코드베이스를 해방시키고 싶을 것이다. 의존성 주입은 이상적이고 더 순수한 객체 모델을 제공하지만, 종종 달성하기 어려울때가 있다. 게다가 처음 코드를 짤 때 이것을 수용하도록 설계하지 않을 것이다.

위에 우리가 리팩토링한 것은 이제 더 모듈화되고 테스트용이하다. 그러나 여기에는 현실적인 문제가 있다. MyViewController의 생성자는 빈(init())것에 익숙한데 이제 3개의 파라미터를 받아야한다. 모든 호출시점이 바뀌게 되는 것이다. 이것을 구성하기위해 깔끔하고 적절한 방법은, 계층 위아래로 혹은 이전 뷰컨트롤러에서 지금 뷰컨트롤러까지 인스턴스를 보내도록 만드는 방법이다. 이것은 객체 그래프의 루트에서부터 모든 자식까지 데이터를 보내야 한다는 뜻이다. iOS에서는 특히 뷰컨트롤러에서 뷰컨트롤러로 데이터를 보내는 것이 머리아픈일이다. 특히 다른 사람에게 넘겨받은 코드베이스는 갑자기 많이 바뀌는 구현에서 애를 먹을 것이다. 그러면 대부분 클래스들(특히 뷰컨트롤러)의 생성자는 바뀌어야한다. 이런 바뀜은 앱 전체를 일괄적으로 리팩토링해야하는데 당신이 인지하고 있기 어려운 범주가 되버린다. 모든 코드를 고칠 수도 있겠지만 아니면 다른 클래스들은 여전히 묵시적으로 싱글톤을 참조하게 두고 몇개만 의존성 주입으로 바꾸는 것이다. 그래도 이런 부조화는 훗날에 문제를 야기할 수도 있다.

따라서 이런 리팩토링은 복잡하고, 크고, 넘겨받은 코드베이스에는 알맞지 않다. 이런 이유로 리팩토링을 하지 말고 이런 기술적 부채와 함께 살아갈것인지도 의논해보아야한다. 그러다가 몇달, 몇년뒤 멀티 사용자 기능을 지원해야할때, 계정을 바꿀때 CurrentUserManager가 동작하지 않을 수 있다. 이것은 어떻게 해결할 것인가?

여러분이 어떤 프로젝트를 시작할때부터는 클래스 설계의 첫 단계부터 이런 종류의 변경을 수용할 수 있게 만드는 방법이 있을 것이다.

디폴트 파라미터 값들
스위프트에서 내가 좋아하는 기능 중 하나는 디폴트 파라미터 값들이다. 이것은 놀랍도록 유용하고 여러분 코드에 엄청난 유연성을 가져다준다. 디폴트 파라미터로, 의존성 주입이라는 토끼구멍으로 들어가지 않고, 또 여러분의 코드베이스에서 엄청난 복잡성도 만들지 않고서, 위에서 말한 문제를 해결할 수 있다. 아마 여러분의 앱은 한명의 유저만을 가질 것이니, 위와같으 의존성 주입의 구현이 불필요하게 과하다.

싱글톤을 디폴트 파라미터로하여 사용할 수 있다.
class MyViewController: UIViewController {

    init(userManager: CurrentUserManager = .shared, defaults: UserDefaults = .standard, urlSession: URLSession = .shared) {
        self.userManager = userManager
        self.defaults = defaults
        self.urlSession = urlSession
        super.init(nibName: nil, bundle: nil)
    }
}
이제 호출 시점을 고칠 필요가 없다. 그러나 클래스 그 자체 안에서는 수많은 다양한 것들이 있다. 이제 의존성 주입을 사용하고 더이상 싱글톤을 참조하지 않는다.
let controller = MyViewController()

present(controller, animated: true, completion: nil)
이러한 변경으로 어떤 이득을 취할 수 있을까? 모든 호출 지점을 바꾸지 않고 이 패턴을 사용하기 위해 모든 클래스를 리팩토링할 수 있다. 의미로나 기능적으로나 바뀐것은 없다. 그러나 여러분의 클래스들은 이제 의존성 주입을 사용한다. 이것들은 단지 내부적으로 인스턴스를 사용하고 있다. 이것을 위에서 설명한 것처럼 테스트할 수 있고 유연하게 모듈화된 API를 유지보수 할 수 있다. (그래도 모든 퍼블릭 인터페이스는 바뀌지 않는다는 점) 본질적으로는 아무것도 바뀌지 않은채 계속 코드베이스에서 작업할 수 있을 것이다.

커스텀을 전달받는 떄가 오면, non-싱글톤 파라미터로 어떤 클래스도 변경하지 않고서 해결할 수 있다. 오직 호출 지점만 바꾸면 된다. 게다가 완전한 의존성 주입을 구현하여 계층의 위에서 애래로 모든 의존성마다 전달해가려면 그냥 드폴트 파라미터를 지우고 그 위에서 의존성으로 전달하면 된다.

필요에따라 어떤 디폴트값의 opt-in 혹은 opt-out도 할 수 있다. 아래 예제에서는 커스텀 UserDefaults를 제공하지만, CurrentUserManagerURLSession을 위해 디폴트 파라미터를 가지고 있는다.
let appGroupDefaults = UserDefaults(suiteName: "com.myApp")!

let controller = MyViewController(defaults: appGroupDefaults)

present(controller, animated: true, completion: nil)

결론
스위프트는 적은 노력으로 "partial" 종류의 의존성 주입을 만든다. 여러분의 클래스에 드폴트값으로 새 프로퍼티와 생성자 파라미터를 추가하여, 코드를 모듈화시키고 테스트하기 좋게 만들 수 있다(리팩토링에 빠지지 않고 완전한 의존성 주입을 만들 필요 없이 가능하다). 만약 프로젝트 시작 시점에 클래스를 이렇게 설계하면 코딩하면서 궁지에 몰리는 일이 더 적어질 것이다(그리고 여러분이 궁지에 몰리더라도 쉽게 빠져나올 수 있을 것이다).

여러분은 이 예제를 넘어 클래스, 구조체, 열거형, 함수 등 코드 전반에 이 개념과 설계를 적용시켜 볼 수 있다. 스위프트의 모든 함수는 디폴트 파라미터 값을 받을 수 있다. 나중에 어떤게 바뀔 수 있을지 생각해봄으로서, 적은 노력으로 변경할 수 있는 타입이나 함수를 만들어낼 수 있을 것이다.

좋은 소프트웨어를 만드는 것과 설계하는 것은 원하는 것을 쉽게 바꿀 수 있지만, 모든것을 바꾸진 않아도 되는 코드를 짠다는 의미이다. 이것이 의존성 주입 뒤에 있는 그 이유이며 스위프트의 디폴트 파라미터가 이것을 빠르고 쉽고 우아하게 해결할 수 있게 도와줄 것이다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: System Level Breakpoints in Swift


모든 훌륭한 소프트웨어 개발자들은 결국 훌륭한 소프트웨어 디버거가 되야한다. 디버깅은 크게는 브레이크포인트의 설정으로 이루어지고, 실행시간동안 앱의 임의의 상태 지점을 관찰하기위해 브레이크 포인트를 걸어 놓은 곳에 가본다. 큼직하게는 두가지 종류의 브레이크 포인트가 있다. 하나는 당신의 코드에 설정한 것이고, 다른 하나는 다른사람의 코드에 설정한 것이다.

여러분의 코드에 브레이크 포인트를 설정하는것은 간단하다. 그냥 Xcode 프로젝트의 소스코드 라인을 찾아서, 관련된 라인 옆에 홈 안의 공간을 탭하면 된다.



그러나 시스템 API에대해 브레이크 포인트를 설정하고 싶거나 소스코드를 가지고 있지 않은 라이브러리 안에 구현된 메소드에 브레이크 포인트를 설정하고 싶은 경우는 어떨까? 예를들어, 레이아웃 버그를 잡아야한다고 할때, UIView에서 애플의 고유의 내부 layoutSubviews 메소드 호출을 관찰하는데 도움이 될 수 있다. 역사적으로 Objective-C 개발자들에게는 이것이 큰 문제가 아니다. 어떤 메소드를 심볼릭하게 표현하기위해 그리고 그것을 브레이크하기위해, Xcode의 lldb 콘솔(View -> Debug Area -> Activate Console)로 들어가서 이 이름을 지정해가면서 브레이크 포인트를 지정하면 된다는 것을 안다. lldb에서 "b"라는 단축 명령어는 우리가 입력한 것으로부터 전체 이름과 매칭되는 것을 찾는 정규식이다.

(lldb) b -[UIView layoutSubviews]
Breakpoint 3: where = UIKit`-[UIView(Hierarchy) layoutSubviews], address = 0x000000010c02f642(lldb)

lldb 콘솔에서 겁을 주거나, 현재 디버그 세션보다 더 길게 붙이는 브레이크 포인트를 원하면, Xcode의 내장된 심볼릭 브레이크 포인트 인터페이스(Debug -> Breakpoints -> Create Symbolic Breakpoint)를 사용하여 같은 결과를 달성할 수 있다.


사실, 여러분의 iOS 앱에 브레이크 포인트를 걸고 앱을 돌리면 애플의 layoutSubviews 메소드에서 브레이크 포인트 안으로 실행시켜볼것이라고 생각한다 lldb 콘솔로 돌아와서 메시지를 받은 오브젝트를 확인한다.

(lldb) po $arg1
<UIClassicWindow: 0x7f8e7dd06660; frame = (0 0; 414 736); userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x60000004b7c0>; layer = <UIWindowLayer: 0x600000024260>>

이제 계속해서 심블에 브레이크를 걸어보자. 그리고 또다시 그렇게 하자. lldb 콘솔에 "po $arg1"이라고 입력하여 매번 타겟을 확인하라. 유별난 버그를 추적하는동안 이런 종류의 분석을 시행하는게 얼마나 유용한지 상상할 수 있을 것이다.

그러나 우리 플랫폼을 이제 시작한 가여운 스위프트 프로그래머들이나 스위프트 문법에만 열관하는 사람들은 어떻게할까? 애플의 문서를 읽어보거나, "-[UIView layoutSubviews]"를 해석하는게 불가능한 사람들은 "UIView.layoutSubviews"이 완전히 눈에 거슬릴 뿐만 아니라, 스위프트에게는 옳은 것일까?

불행히도 "UIView.layoutSubviews"라고 브레이크포인트를 설정하면 동작하지 않는다.

(lldb) b UIView.layoutSubviews
Breakpoint 3: no locations (pending).WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb)

이것이 실패하는 이유는, UIView에 스위프트로 구현된 layoutSubviews라는 메소드가 없기 때문이다. 이것은 모두 Objective-C로 구현되있다. 사실 아주 많은 스위프트에서 쓸 수 있는 Objective-C 메소드들이 Objective-C 메시지 전송으로 바로 전달하도록 컴파일된다. 스위프트 파일에 "UIView().layoutIfNeeded()"같은 것을 입력하면, 컴파일은 될것이며, layoutIfNeeded라는 스위프트 메소드가 없기때문에 호출해도 아무일도 일어나지 않을 것이다.

이것이 스위프트로 맵핑된 모든 Cocoa 타입들에게 해당되지는 않는다. 예를들어, 모든 "Data.write(to:options:)" 호출에 브레이크를 걸고 싶다고 생각하자. 아마 "Data.write"에 브레이크 포인트를 걸어서 동작하길 원할것이다.

(lldb) b Data.write
Breakpoint 11: where = libswiftFoundation.dylib`Foundation.Data.write (to : Foundation.URL, options : __ObjC.NSData.WritingOptions) throws -> (), address = 0x00000001044edf10

그리고 된다! 이건 어떨까? 실제로 이것만 아니다. 이것은 -[NSData writeToURL:options:error:]의 길에서 libswiftFoundation을 통해 전달하는 모든 호출에 브레이크가 걸릴 것인데, Objective-C 구현을 호출하는것에 직접 잡히지는 않을 것이다. 메소드에서 모든 호출을 잡기 위해서는, 저수준(Objective-C 메소드)에서 브레이크포인트를 설정할 필요가 있다.

따라서 규칙으로, iOS나 Mac 플랫폼에서 더 좋은 디버거를 원하는 스위프트 프로그래머들은 Objective-C의것과 동일하게 스위프트 메소드를 매핑할 수 있는 능력이 필요하다. UIView.layoutSubviews같은 메소드의경우, 바로 "-[UIView layoutSubviews]"로 맵핑하지만, 많은 메소드의경우 지금처럼 간단하게 될것이다.

스위프트로 맵핑된 메소드 이름을 Objective-C로 다시 매핑하기위해서는, 많은 Foundation 클래스들이 NS 접두를 뺐다는 것을 인식하고, 스위프트 API 가이드라인을 적용하기위해 메소드 시그니처를 다시 작성한 영향이다. 예를들어 순수한 스위프트 프로그래머들은 "Data.write(to:options)"의 저수준 구현으로 브레이크 포인트를 설정하기 때문에 쉽게 유추하지 못할것이다. "NS" 접두를 붙이고, URL 파라미터를 명시적으로 넣고, 이상한 에러 파라미터를 추가한다. 이것은 보기에 옛날의 나쁜 시기에 까탈스러운 백발의 노인이 실패를 전달하는데 사용하는 것 같아 보인다.

(lldb) b -[NSData writeToURL:options:error:]
Breakpoint 13: where = Foundation`-[NSData(NSData) writeToURL:options:error:], address = 0x00000001018328c3

성공했다!

이런 Obejctive-C 메시지 시그니처와 API 규약들에대한 추가적인 지식을 개발하게 만드는 생각은 여러분을 힘겹게 만든다는 점에서, 나는 여러분의 다음 과제를 통해 얻을 약간의 수정된것을 제공한다(I offer a little hack that will likely get you through your next challenge). API가 이런 원리중 하나를 사용햐여 작성되었으면, 함수의 스위프트 이름은 거의 Objective-C 메소드 이름의 부분집합이다. 아마도 lldb의 정규식 일치 기능을 활용하여 브레이크포인트를 설정하고싶은 메소드를 0으로 만들 수 있다.

(lldb) break set -s Foundation -r Data.*write
Breakpoint 17: 8 locations.

이제 "break list"를 입력하고 lldb가 표시한 매칭 수를 보자. 그것들 중에는 스위프트로 된 (libswiftFoundation으로 이루어진) 여러 메소드가 있으나, 질문에서 타겟 메소드를 찾아야할 것이다. 사실, 여러분이 브레이크를 걸고싶은 다른 저수준의 Objective-C 메소드도 찾아봐야할 것이다.

리스트를 더 잘 관리하기위해, 주어진 Objective-C 프레임워크안에 타겟 메소드가 있다는 지식을 주고, 이름으로 특정 공유된 라이브러리에 제한된 매칭을위한 "-s" 플래그를 넣는다.

(lldb) break set -s Foundation -r Data.*write
Breakpoint 17: 8 locations.

이 브레이크포인트 사이에서 NSPageData에서 몇 거짓이 있지만, 그 목록은 모두 더 관리할 수 있다. 하나의 브레이크포인트 "17"은 하위-숫자로 식별된 매치의 모든것을 가지고 있는다. 여러분의 방법으로 브레이크포인트의 목록을 걸러내도 좋다.

(lldb) break disable 17.6 17.7 17.8
3 breakpoints disabled.
(lldb) c

Objective-C를 스위프트에 맵핑하는 애플의 방식은 스위프트 개발자를위해 더 즐거운 프로그래밍 경험을 함께 만들게 해주지만, 그 세부적인 구현을 이해하지 못하고 있거나 어떻게 동작하는지에대한 이해가 부족하게되면 엄청난 혼란으로 빠질 수 있다. 나는 이 글이 여러분의 스위프트 앱을 디버깅하는데 필요한 도구가 되었으면 좋겠고, 불가피하게 사용하는 Objective-C 코드를 더 효율적이게 썼으면 좋겠다.

Update: 나는 두가지 관련된 버그를 넣었다: Radar #31115822 스위프트 메소드 포맷에서 자동으로 매핑한 것을 Obejctive-C 메소드로 돌아오도록 해야한다, 그리고 Radar #31115942 간결한 스위프트 메소드 시그니처를 만드는것에대해 lldb를 더 직관적으로 만들도록 해야한다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Service-oriented AppDelegate


보통 앱델리게이트(AppDelegate)는 거대한 클래스이다. 이것은 여러분의 앱에 대해 너무 많이 알고 있고 점점 더 커지게 된다. 이 글에서는 깔끔한 플러그인 기반 아키텍처를 만들어서 그 세부 기능으로부터 어떻게 분리해낼 수 있는지 보여줄 것이다.

우리는 올바르지 않은 방법으로 문제를 직면하고 있다. 당신의 앱델리게이트를 어떻게 관리하고 있는지 생각해보자. 내 생각엔 굉장히 거대하거나, 아니면 적어도 거기서 코딩한 것들에 대해 자신감이 있지는 않을 것 같아 보인다.

앱델리게이트의 문제는 바로 앱델리게이트가 여러분의 앱에대해 너무 많이 알고 있다는 점이다. 이것은 여러분의 의존성들을 알고있는데, 어떻게 초기화하는지, 어떻게 푸시 노티피케이션을 파싱하는지 등 많은 것을 알고 있다.

요약을 해보자면
ApplicationSercvices를 만들어낼 수 있다. 이것들은 AppDelegate 라이프사이클을 공유하고 해당 단계에 작업을 실행하는 오브젝트이다. 여러분의 앱델리게이트는 피관찰자(Observable)이고 여러분의 서비스들은 관찰자(Observer)이다. 이런 접근법은 그 서비스들로부터 앱델리게이트를 분리시켜주고 단일 책임 오브젝트를 생성해준다.

물론 많은 양의 코드를 작성하게 되므로 이 문제를 다루기위해 CocoaPod을 만들었다. PluggableAppDelegate라 부르며 여기에서 확인할 수 있다: http://github.com/fm091/PluggableApplicationDelegate

서비스를 생성해서 AppDelegate에 등록만 하면 된다. 그 결과는 여러분이 봐왔던 것 중에 가장 작고 깔끔한 AppDelegate가 될것이다.

일반적인 앱델리게이트
일반적으로, 앱델리게이트는 많은 역할을 가지고 있다.
  1. 여러분 앱의 모든 의존성을 초기화한다.
  2. 전역의 UIAppearance 값들을 초기설정한다.
  3. 푸시 노티피케이션을 다룬다.
  4. 푸시 노티피케이션을 등록한다.

그밖에 더 있을 것이다.

여기서 문제는 여러분의 앱델리게이트가 정확하게 모든 컴포넌트가 어떻게 동작하는지, 그리고 어떻게 인스턴스화하는지 알고 있기 때문에 생긴 것이다. 또한 이것은 여러분의 앱이 어떻게 생겼는지도 알 수도 있다. 그리고 푸시 노티피케이션 포멧도 알고있다. 결과적으로 생각했던것보다 너무 커져버린다.

다른 접근법
ApplicationService라 부르는 그 고유의 컴포넌트에 여러분의 의존성들을 캡슐화하여 들고 있다고 생각해보자.

모든 ApplicationService는 하위-앱델리게이트(sub-AppDelegate)이다. 이것은 앱델리게이트의 라이프사이클을 공유하고, 요청된 이벤트에서 행동을 취한다.

여러분 AppDelegate는 이벤트의 발생를 관찰하는 관찰자(observable)이 되어, 어떤 일이 일어나면 서비스에게 알려주기만 하면 된다.



어플리케이션 서비스들
어플리케이션 서비스들은 AppDelegate 라이프사이클을 공유하는 오브젝트들이다. ApplicationService는 그냥 태그한 프로토콜이다. 만약 PluggableApplicationDelegate를 사용한다면 이렇게 생겼을 것이다.
public protocol ApplicationService: UIApplicationDelegate {}
위에서 본 것처럼, UIApplicationDelegate만이지만, 더 전달력있는 이름이다.

이것을 구현할때, AppDelegate를 구현하듯이 하면 된다. 그러나 여기서 다른점은 단일 책임 요소를 코딩한다는 점이다.

아래에 구현에대한 예시가 있다.
import Foundation
import PluggableApplicationDelegate

final class LoggerApplicationService: NSObject, ApplicationService {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
        print("It has started!") return true
    }
    func applicationDidEnterBackground(_ application: UIApplication) {
        print("It has entered background”)
    }
}

새로운 앱델리게이트
PluggableApplicationDelegate를 상속받고 이것을 서비스에 등록함으로서, 이제 여러분의 앱델리게이트는 더 작아졌다.

아래를 확인해보자
import UIKit 
import PluggableApplicationDelegate 

@UIApplicationMain 
class AppDelegate: PluggableApplicationDelegate { 

    override var services: [ApplicationService] { 
        return [ LoggerApplicationService() ] 
    } 
}

PluggableApplicationDelegate는 너무 크다. 뭔가 일어났을때만 그 서비스에 알려준다. 그것에대한 코드를 여기서 확인할 수 있을 것이다.

의존성과 기능을 점점 더 앱에 추가하더래도 앱델리게이트는 아래처럼 깨끗하게 유지될 것이다.
import UIKit 
import PluggableApplicationDelegate 

@UIApplicationMain 
class AppDelegate: PluggableApplicationDelegate { 

    override var services: [ApplicationService] { 
        return [ 
            PushNotificationsService(), 
            AnalyticsService(), 
            CustomAppearanceService(),
            FirebaseService(), 
            FacebookSDKService() 
        ] 
    } 
}

결론
이게 다다. 여러분의 앱델리게이트를 분리시키는 간단한 방법이며, 그것이 커다란 덩어리로 되지 않게 막아준다.

내 깃헙 저장소를 꼭 확인해보기 바란다:

내가 도움이 됐던 만큼 여러분에게도 도움이 되길 바라며, 이것에대한 피드백을 꼭 받고싶다. 아래 댓글로 남겨달라

고맙다!


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: 50 iOS Interview Questions And Answers - Part3



내가 받았던 iOS 인터뷰 질문의 마지막 부분으로 올리기로 결정했다. 아직 보지 못했다면 Part1(링크)와 Part2(링크)를 확인해보아라.

1-  좋아하는 앱은 무엇이고 왜 좋아하는가?
아래 답변은 나의 실제 답변이다. 이것들은 내가 좋아하는 앱들과 그 이유이다.

  • Sleep Cycle : 애플의 헬스를 넣었고, 일어나기 모드, 유용한 원형 차트 리프토 도구와 함께 세계의 통계와 필립스 휴를 넣었다.
  • WaterMinder : 하루 물 섭취 이력을 따라갈 수 있다. 코딩하는동안 물 마시는 것을 밎을 수 있는데, 이 앱이 물을 마시라고 알려주며, 다른 종류의 액체류 섭취량(커피 휫수!)도 넣을 수 있다.
  • DayOne : 2년이상 쓰고 있는 앱이다.
  • Facebook : 모든것이 한곳에 있는 앱이다. 스포츠나 메시지, voip, 날씨나 뉴스앱을 따로 설치할 필요가 없다.
  • Mdeium : 순수하고 네이티브하고 오가닉하며 최신의 그리고 point shot 기술관련 글을 쉽게 찾을 있고 그 회사와 개발자들의 비전을 배울 수 있다.

2- JSONSerialization이 어떤 ReadingOption들을 가지는가?
  • mutableContainers는 배열과 딕셔너리를 변수 오브젝트(상수가 아닌)로 만들어준다.
  • mutableLeaves는 JSON 오브젝트 그래프에서 리프(leaf)의 문자열을 변수 문자열 인스턴스로 만들어준다.
  • allowFragments는 배열이나 딕셔너리 인스턴스가 아닌 최고수준 오브젝트로 파싱할 수 있게 해준다.

3- 서브스크립트(subscript)를 설명하시오
클래스, 구조체, 열거형에서 서브스크립트를 정의할 수 있으며, 이것은 컬랙션, 리스트, 시퀸스의 맴버 요소에 접근하기위한 단축방식(shortcut)이다.

4- 디스패치 그룹(DispatchGroup)이란 무엇인가?
디스패치 그룹은 어떤 작업의 동기화를 모을 수 있게 한다. 여러 다른 작업 항목을 보내서, 각각이 다른 큐에서 동작할지라도 모든것이 완료된 것을 확인하는데 사용한다. 이 동작은 명시된 작업이 모두 완료되기 전까지 처리하지 않도록 만들때 유용하다— 애플의 문서

가장 기본적인 답변은, 처리하기 전에 여러 비동기나 동기적인 작업을 기다릴 필요가 있을때, 디스패치 그룹을 사용할 수 있다고 하면 된다.

5- RGR(Red-Green-Refactor)가 무엇인가?


Red, Green, Refactor는 TDD(테스트 주도 개발)의 단계이다.
  1. Red : 보통 7줄이 넘지 않는 짧은 테스트 코드를 작성하고 실패를 확인한다
  2. Green : 짧은 제품 코드를 작성한다. 다시 7줄이 넘지않는 짧은 테스트 코드를 작성하고 테스트가 통과하게 만든다.
  3. Refactor : 테스트가 통과하면, 걱정없이 변경할 수 있다. 코드를 정리(clean up)한다. 여기에 훌륭한 워크샵 노트가 있다.

6- 의존성 주입(Dependency Injection)이란 무엇인가?
iOS 앱에서 스토리보드나 xib을 사용하면 IBOutlet을 만들게된다. IBOutlet은 뷰에 관련된 프로퍼티이다. 이것들은 그 뷰가 인스턴스화될때 뷰컨트롤러에 주입(inject)되며, 이것이 의존성 주입의 본질적인 형태이다.

의존성 주입의 형식에는 생성자 주입, 프로퍼티 주입, 메소드 주입이 있다.

7- 노티피케이션의 타입을 설명하라
노티피케이션 타입에는 리모트(remote)와 로컬(local), 두가지 타입이 존재한다. 리모트 노티피케이션은 서버 접속을 필요로 한다. 로컬 노티피케이션은 서버 접속을 필요로 하지 않는다. 로컬 노티피케이션은 기기 내부적으로 일어나는 것이다.

8- 프로젝트에서 언제 의존성 주입을 사용하면 좋을까?
여러분이 해볼 수 있는 몇몇 가이드라인이 존재한다.
규칙1. 테스트 가능함이 중요한가? 그렇다면 테스트하고 싶은 클래스에 외부 의존성을 식별하는 것이 필수이다. 의존성을 주입할 수 있으면, 테스트하기 쉽게 만들기위해 실제 서비스를 가짜의 서비스(mock)으로 쉽게 대체할 수 있다.
규칙2. 복잡한 클래스는 복잡한 의존성을 가진다. 앱-수준 로직을 가지고있거나 디스크나 네트워크같은 외부 리소스에 접근한다. 앱의 대부분 클래스는 복잡할 것이며, 많은 컨트롤러 오브젝트와 많은 모델 오브젝트를 가지고 있을 것이다. 이것을 시작하는 가장 쉬운 방법은 여러분의 앱에서 복잡한 클래스를 뻬내고 그 클래스안에 다른 복잡한 오브젝트를 초기화할 장소를 찾는 것이다.
규칙3. 한 오브젝트가 다른 오브젝트와 의존성을 공유하는 오브젝트 인스턴스를 만들고 있다면 의존성 주입을 고려해야할 대상이다.

9- 컬랙션 타입(collection type)에서 우리가 사용할 수 있는 오더 함수(order function)은 어떤 종류가 있는가?
map, filter, flatMap에대한 자세한 내용을 확인해보자!

10- 커밋들을 합치게 해주는 깃 명령은 무엇인가?
git squash

11- AnyAnyObject의 차이는 무엇인가?
애플의 스위프트 문서에 따르면 아래와 같다.
  • Any는 모든 타입에대해 인스턴스를 나타낼 수 있으며, 함수 타입이나 옵셔널 타입까지도 포함된다.
  • AnyObject는 모든 클래스 타입의 인스턴스를 나타낸다.

더 세부적인 내용를 확인해보자.

12- 기본적으로 SOAP과 REST의 차이는 무엇인가?
둘 다 웹 서비스의 접근을 도와준다. SOAP는 서비스에 메시지를 날리기위해 독점적으로 XML과 친하다. SOAP은 웹 서비스 접근에대해 절대적으로 무겁다. 원래는 마이크로소프트에서 만들어진 것이다.

REST (Representational State Transfer)은 경량의 대안이다. 요청시 XML을 사용하는 것 대신에 REST는 대부분 간단한 URL을 사용한다. REST는 작업을 수행하기위해 4가지의 HTTP 1.1 용어(GET, POST, PUT, DELETE)를 사용한다.

13- 당신이 좋아하는 시각화 차트 라이브러리는 무엇인가?
Charts는 iOS, tvOS, OSX를 지원하며 애플 버전의 MPAndroidChart이다.
Core Plot는 macOS, iOS, tvOS를 위한 2D 점에대한 곡선을 그리는(plotting) 프레임워크이다.
TEAChart는 iOS를 지원한다.


14- 나쁜 커밋을 찾아주는 깃 명령은 무엇인가?
git bisect

15- 코어데이터(CoreData)란 무엇인가?
코어데이터는 디스크에 영속 저장소에 오브젝트 그래프를 영속할 수 있게 만드는 오브젝트 그래프 매니저이다. 한 오브젝트 그래프는 일반적인 Model View Controller iOS 앱에서 다른 모든 Model의 지도같은 역할을 한다.

16- 연관타입(Associatedtype)을 설명하시오
제네릭 프로토콜을 만들고 싶다면 연관타입을 사용할 수 있다. 더 자세한 내용을 확인해보자.

17- 깃에서 커밋을 만들지 않고서 코드를 저장하는 명령은 무엇인가?
git stash

18- Priority InversionPriority Inheritance 설명하시오
높은 우선순위 스레드가 낮은 우선순위 스레드를 기다리는 것을 Priority Inversion이라 부른다. 낮은 우선순위 스레드가 일시적으로 제일 높은 우선순위의 스레드의 우선순위를 물려받으면(inherit) Priority Ingeritance라 부른다.

19- Hashable이 무엇인가?
Hashable는 딕셔너리에서 키로서 오브젝트를 사용할 수 있게 해주는 것을 말한다. 따라서 우리만의 커스텀 타입을 만들 수 있다.

20- 옵셔널 체이닝(optional chaining)과 if let, guard는 언제 사용할까?
옵셔널 체이닝은 작업이 실패하는것을 별로 신경쓰지 않을때 사용한다. 그렇지 않으면 if let이나 guard를 사용한다. 이 대답은 Parsing JSON in Swift의 확장판에서 가져온 대답이다.

물음표 연산자를 사용하는 것을 옵셔널 체이닝이라한다. 애플의 문서에는 이렇게 설명하고 있다.
옵셔널 체이닝은 현재 nil일 수 있는 옵셔널에대해 프로토콜, 메소드, 서브스크립트를 쿼리하고 호출하기위해 진행한다. 옵셔널이 값을 가지고 있다면 프로퍼티, 메소드, 서브스크립트 호출은 성공할 것이고; 옵셔널이 nil이라면 프로퍼티, 메소드, 서브스크립트는 nil을 반환할 것이다. 다중 쿼리가 체인(chained)될 수 있으며, 체인에서 어떤 연결이라도 nil이면 모든 체인이 우아하게 실패한다.

21- 스위프트에서 데이터를 전달하는 방법에는 몇가지가 있는가?
델리게이트, KVO, Segue, NSNotification, Target-Action, Callback등 여러 방법들이 존재한다.

22- 어떻게 프로젝트 코드를 깨끗하게 만들 수 있는가?
GithubSwiftLint의 스위프트 프로젝트를 위한 코딩 규약, 스타일 가이드를 따른다.

23- iOS10에서 새로 발표된 내용은 무엇인가?
  • SiriKit
  • Proactive Suggestions
  • 메시지 앱과 동합하기
  • 사용자 노티피케이션
  • 앱 익스텐션
  • CallKit
  • 음성 인식
  • 앱 검색 증진

더 자세한 내용을 확인하자.

24- 옵셔널에서 nil.None의 차이점은 무엇인가?
차이는 없다. Optional.None( .None은 단축형이다)은 값을 잃어버린 옵셔널 변수의 올바른 초기화 방법이다. 반면  nil.None에대한 간편한 문법형태(sugar)이다. 확인해보자.

25- iOS 프로젝트를 위한 다른 IDE를 사용하고 있는가?

그렇다 혹은 아니다. 만약 그렇다고 하고싶다면 Appcode를 확인해보자.

26- VIPER 아키텍처란 무엇인가?
여기에 VIPER에대한 완벽한 설명이 있다.

27- Continuous Integration이 무엇인가?
Continuous Integration은 앱 개발시 뭔가 잘못 되었을때 빠른 피드백을 줄 수 있는 어떤것이다. 수많은 Continuous Integration 툴을 사용할 수 있다.

스스로 호스팅되는 서버

클라우드 솔루션

28- 델리게이트와 콜백의 차이점은 무엇인가?
델리게이트는 NetworkService가 델리게이트에게 "뭔가 바뀌었어"라고 말하는 것이고, 콜백은 델리게이트가 NetworkService를 주시하면서 지켜보고 있는 것이다.


29- 이전에 디자인 툴이나 프로토타입 툴을 사용해 본 적이 있는가?
디자인 툴을 아무것도 모른다면 오늘 바로 시작해보길 바란다.

Sketch + Marvel는 좋은 궁합이다. 코딩을 하면서 프로토타입을 만들어보고 싶으면 Framer도 좋은 선택일 것이다.

30- 백엔드 개발에대해 알고있는가?
사람마다 다를것이다. 나는 PARSE에 있어보았고 FBStart를 받았었다. 나는 순수 백엔드를 배우기로 결정했었다. 여러분은 두가지 선택지가 있을것이다. node.js + express.js 그리고 mongodb를 배우거나, Vapor 나 Kitura를 배울 수도 있을 것이다.

Firebase를 좋아하거나 사용하지 못해보았는가?

Firebase는 macOS 개발자들을 위한 길이 없다.

여러분이 Firebase를 배우고 싶으면, 한달코스의 Firebase Google Group를 따라가보아라.

31- 오토레이아웃을 설명하라
오토레이아웃은 유연하고 강력한 레이아웃 시스템을 제공하며, 이것은 뷰와 UI 조작이 그 계층안에서 크기와 위치를 어떻게 계산할지 표현한다.

32- 하드코딩 로그 절의 단점은 무엇인가?
첫째로, 로그를 시작할때이다. 모으기위해서 이것을 시작한다.많아 보이지는 않을 것이지만 매 분마다 추가된다. 프로젝트 끝쯤에는 잃어버린 분들이 시간 단위가 되있을 것이다.
둘째로, 코드베이스에 추가할 때마다 우리 코드에 새로운 버그를 주입하는 위험을 겪는다.

33- 포인터란 무엇인가?
포인터는 메모리 주소에 직접 참조하는 것이다. 한 변수는 값을위한 투명한 용기로서 동작하는 반면, 포인터들은 추상화층을 없애고 값이 어떻게 저장되있는지 볼 수 있게 해준다.

34- 계약직으로 일해본 적이 있는가?
그렇게 해보면 좋다. 그것은 그것의 장점이 있다. 내 생각에는 클라이언트를 관리해보고 소통하고 작업 관리 능력을 증진시키는데 좋은 경험인 것 같다.

35- 쌍 프로그래밍(pair programming)이란 무엇인가?
쌍 프로그래밍은 하급 개발자와함께 정보를 공유하는 도구이다. 하급 개발자와 상급 개발자가 나란히 앉아서, 상급 개발자에게 배울 수 있는 방법이다.

Martin Fowler의 “Pair Programming Misconceptions” WikiHow의 "Pair Programming" 를 확인해보아라

36- 블럭(block)에대해 설명하라
블럭은 단일 작업이나 동작의 유닛을 전체적인 Objective-C 클래스를 쓰지 않고 정의할 수 있는 하나의 방법이다. 익명 함수이기도 하다.

37- 키체인(Keychain)이란 무엇인가?
키체인은 iOS 앱에서 데이터를 보안상 저장하기위한 API이다. 좋은 라이브러리가 있다(Locksmith).

38- UserNotification에서 가장 크게 바뀐 점은?
  • 오디오, 비디오, 이미지를 넣을 수 있게 되었다.
  • 노티피케이션에 커스텀 인터페이스를 넣을 수 있게 되었다.
  • 노티피케이션 센터에서 인터페이스를 관리할 수 있게 되었다.
  • 새로운 노티피케이션 익스텐션은 우리가 전달하기 전에 리모트 노티피케이션 적재량을 관리할 수 있게 되었다.

39- atomicnonatomic synthesized 프로퍼티의 차이에대해 설명하라
atomic : 디폴트 동작이다. 한 오브젝트가 atomic으로 선언되있으면 스레드 세이프하게된다. 스레드 세이프의 의미는, 클래스의 특정 인스턴스의 한 스레드만 그 오브젝트에대해 조작할 수 있다.
nonatomic : 스레드 세이프 하지 않다. nonatomic 프로퍼티 속성을 사용하면 synthesized 접근자가 간단하게 직접 값을 세팅하거나 리턴하도록 지정할 수 있다. 서로 다른 스레드에서 동시에 같은 값에 접근하려 할때 어떤 일이 일어날지는 보장하지 못한다. 이런 이유로, atomic보다 nonatomic이 접근 속도가 더 빠르다.

40- availability 속성은 왜 사용하는가?
애플은 한 시스템 버전 back을 지원하고 싶어하는데, 이 의미는 우리가 iOS9와 iOS8을 지원해야한다는 것이다. Availability 속성은 이전 버전의 iOS를 지원하게 해준다.

41- 디바이스 토큰을 얻는 방법에는 무엇이 있는가?
두가지 단계가 있다. 첫째로 사용자 허가 화면을 반드시 보여야한다. 그 뒤에 리모트 노티피케이션을 등록할 수 있다. 이 단계가 잘 넘어가면 시스템에서 디바이스 토큰을 줄 것이다. 만약 앱을 지우거나 재설치하면 디바이스 토큰은 변경될 것이다.
 
42- 캡슐화란 무엇인가?
캡슐화는 객체지향 설계 원리이며 내부 상태와 오브젝트의 기능을 숨긴다. 이 의미는, 오브젝트가 자신의 상태 정보를 사적으로(private) 들고 있는다.

43- 빅오(Big-O) 노테이션이란 무엇인가?
알고리즘은 N 크기의 입력에대해 결정되는 작업 횟수를 표현하는 방법이다. 빅오 노테이션 등급은 가장 높은 값으로 표현된다. 그리고 빅오 노테이션은 질문의 O(n)으로 답을 찾는다. 여기 cheat sheet swift algorithm club가 있다. 아래는 예제이다.
for 루프의 빅오 노테이션은 O(n)이다. for 루프가 n번 동작하기 때문이다.
변수(var number: Int = 4) 빅오 노테이션은 O(1)이다.

44- 의존성 관리란 무엇인가?
오픈소스 프로젝트를 통합하고 싶을때나 써트파티 프로젝트로부터 프레임워크를 추가하거나 어떤 고유의 프로젝트의 코드를 재사용할때, 의존성 관리는 이 관계를 관리할 수 있도록 해준다. 여기를 왁인해보자

45- UML 클래스 다이어그램란 무엇인가?
UML 클래스 다이어그램은 규칙의 집합이고 소프트웨어의 명세에대한 노테이션인데, 오브젝트 관리 그룹(Object Management Group)에의해 관리되고 생성된다.

46- throw를 설명하라
throws 키워드를 이용하면 컴파일러에게 에러를 던저라고 할 수 있다. 에러를 던지기 전에, 여러분이 받고 싶은 에러 목록을 만들어야한다.

47- 프로토콜 익스텐션이란 무엇인가?
익스텐션을 이용하여 프로토콜을 적용할 수 있는데, 기존의 타입 정의에도 가능하다. 가지고 있지 않는 타입에 프로토콜을 추가할 수 있게 해준다.

48- 2017년 iOS 앱 개발에서 트랜드는 무엇이었나?
  • 스위프트 코딩
  • 인자화된 현실성
  • IoT
  • 보안성 증진
  • 엔터프라이즈 앱에서의 성장
  • 하이브리드 기술(리엑트 네이티브)에서의 성장

49- ObjC에서 Selectors를 설명하시오
Selectors는 Objective-C의 내부적인 메소드이름 표현법이다.

50- 리보트 노티피케이션 첨부물의 한계는 어떻게 되는가?
푸시 노티피케이션으로 비니오나 이미지를 보낼 수 있다. 그러나 최대는 4kb이다. 만약 높은 퀄리티의 첨부물을 보내고 싶으면, 노티피케이션 서비스 익스텐션을 사용할 수 있다.


나의 추천들




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목 :



반갑다 , Part2가 준비되었다. Part1을 확인해보고 싶으면 여기(링크)를 확인해보자.

1- 스위프트에서 메소드 스위즐링(Method Swizzling)을 설명하라
메소드 스위즐링은 Objective-C나 동적 메소드 디스패치를 지원하는 다른 언어에서는 잘 알려진 방법이다.

스위즐링은 통해, 특정 #selector(method)와 그 구현을 담은 함수 사이의 맵핑을 변경하여, 메소드의 구현이 런타임시 다른 것으로 대체될 수 있다.

스위프트 클래스와함께 메소드 스위즐링을 사용하기 위해서는 반드시 따라야할 두가지 필수조건이 있다.
  • 스위즐될 메소드를 가진 클래스는 NSObject를 확장해야한다.
  • 스우즐하고싶은 메소드는 동적 속성을 가지고 있어야한다.

2- Non-Escaping 클로저와 Escaping 클로저의 차이는 무엇인가?
non-escaping 클로저의 라이프사이클은 단순하다.
  1. 함수에 클로저를 전달한다.
  2. 함수가 클로저를 실행시킨다.
  3. 함수는 반환한다.

escaping 클로저의 의미는, 함수 안에서, 여전히 클로저를 실행시킬 수 있다; 클로저의 추가적인 부분은 함수 바깥에서 죽지않는 장소에 저장되있다. 여기에는 클로저가 그 담겨진 함수를 escape하는 여러 방법들이 잇다.
  • 비동기 실행 : 디스패치 큐에서 비동기적으로 클로저를 실행시킨다면 큐는 클로저를 잡고 있을 것이다. 클로저가 언제 실행될지는 알지 못하고 함수가 리턴되기 전에 이것이 완료될지는 보장하지 못한다.
  • 스토리지 : 전역 변수, 프로퍼티, 아니면 다른 스토리지(이전의 함수호출에 살고있는)에 클로저를 저장하면 클로저는 escape된다.


3- [weak self][unownded self]를 설명하라
unowned는 한가지만 빼고서 weak와 같다. 바로, 이 변수는 nil이 될 수 없다는 것이고 그러므로 이 변수는 절때 옵셔널이 될 수 없다.

그러나 그 인스턴스가 메모리 해제된 후에 변수에 접근하려하려한다면, 인스턴스가 메모리 해제된 후에 절때 그 변수를 사용하지 않을것이라고 확신할때만 unowned를 사용해야한다.

그러나 변수를 weak로 만들고 싶지 않고 해당 인스턴스가 메모르 해제된 후에 그 변수에 접근하지 않을 확신이 있을때 unowned를 사용할 수 있다.

[weak self]로 정의하면, 클로저 내의 어떤 부분이 nil일 수도 있는 경우를 다룰 수 있게 해주므로 변수는 반드시 옵셔널이여야한다. 비동기 네트워크 요청에서 [weak self]를 쓰는 경우는 그 요청이 빈번하게 사용되는 뷰의 View Controller에서 이다.

4- ARC란 무엇인가?
ARC는 애플에서만든 컴파일타임의 자동 메모리 관리 기능이다. Automatic Reference Counting의 약자이다. 이 말은 오브젝트의 강참조(strong reference)가 0이될때만 그 오브젝트를 메모리에서 해제한다는 뜻이다.

5- #keyPath()를 설명하시오
#keyPath()를 사용하면, StaticString이나 StringLiteralConvertible로 사용된 key-path 리터럴 문자열의 이점에의해 스테틱 타입 체크가 시행될 것이다. 이 시점에서 아래의 것을 보장하는 체크가 일어난다.
  • 실제로 존재하는가
  • Objective-C로 적절하게 노출되었는가

6- IGListKit가 개발자에게 제공하는 것이 무엇인가?
IGListKit은 자동으로 다른 오브젝트들을 비교하여, 변경된 것을 UICollectionView에 배치(batch) 업데이트 애니메이션을 시행한다. 우리가 절때로 배치를 짜지 않는 방법으로 갱신한다.

7- 리엑트 네이티브가 iOS에게 특별하게 만드는 것은 무엇인가?
  1. (PhoneGap과 다르게) 앱을 리엑트 네이티브로 짜면 자바스크립트로 작성하고 실행시키지만, 앱 UI는 전적으로 네이티브이다. 따라서 HTML5 UI와 소통하는 요소는 없다.
  2. 추가적으로(Titanium과 다르게), 리엑트는 novel, radical, 매우 함수적인 접근법을 UI 구성에 사용한다.요약하면, 앱 UI는 간단히 현재 앱 상태의 함수로 표현된다.

8- NSFetchRequest란 무엇인가?
NSFetchRequest는 Core Data에서 패치하는 역할의 클래스이다. 패치 요청은 강력하기도하고 유연하기도하다. 어떤 기준에따른 오브젝트 집합을 패치하라고 요청할 수도 있고 개별의 값들을 패치하라고 요청할 수도 있고 등등이 있다.

9- NSPersistentContainer란 무엇인가?
퍼시스턴트 컨테이너는 앱을 위해 로드한 것을 가지고 있는 컨테이너를 만들어서 반환한다. 저장된 것을 생성하는데 실패한 정규의 에러 상태가 있으므로, 이 프로퍼티는 옵셔널이다.

10- NSFetchedResultsController를 설명하시오
NSFetchedResultsController는 컨트롤러이지만 뷰컨트롤러는 아니다. 사용자 인터페이스를 가지고 있지 않는다. 이것은 Core Data에서 나온 데이터 소스와 테이블뷰의 동기화 코드를 추상화시켜 개발자들을 좀 더 편하게 만들어주는 목적이 있다.

옳바르게 NSFetchedResultsController를 초기설정하면 여러분의 테이블은 몇줄 코드 없이 데이터 소스를 표현해줄것이다.

11- Xcode8에서 주된 3가지 디버깅 개선사항이 무엇인가?
  • View Debugger는 런타임시 우리의 레이아웃을 시각화해주고 constraint 정의를 보여준다. 이것이 Xcode6부터 있었지만 Xcode8에서 constraint 충돌에대한 경고가 새로  나왔는데 매우 유용하며, 다른 편리한 기능들도 소개되었다.
  • Tread Sanitizer는 Xcode8에서의 완전히 새로운 런타임 도구인데, 스레드 이슈를 경고해준다-가장 눈에띄게는 잠재적인 경쟁상태가 있다.
  • Memory Graph Debugger도 Xcode8에서 완전히 새로 나온 것이다. 이것은 시간상 한 지점의 앱 메모리 그래프를 시각화해주고 이슈 네비게이터에서 누수를 표시한다.

12- 테스트 주도 개발의 세가지 규칙은 무엇인가?
  1. 실패한 유닛테스트를 통과시키지 않은 것을 제품의 코드로 만들지 않아야한다.
  2. 충분히 실패한 것보다 더 많은 코드를 작성하지 않으면 안된다; complication 실패는 실패이다.
  3. 한가지 실패한 유닛테스트를 통과시킨것보다 더 제품의 코드를 쓰지 않아야 한다.
  1. You are not allowed to write any production code unless it is to make a failing unit test pass. 
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

13- 클래스 안에서 final 키워드가 무엇인지 설명하시오
메소드 이름 앞에 final이라는 키워드를 넣으면, 그 메소드가 오버라이드 되는 것을 막아준다. 만약 final class 키워드를 static이라는 한 단어로 바꿔도 같은 동작을 한다.

14- Yak Shaving이 의미하는 것은 무엇인가?
Yak Shaving은 한 프로젝트가 다음 단계로 진행하기 전에 실행해야할 일련의 작업들을 말하는 프로그래밍 용어이다.

// 역자: 접근자 수준은 스위프트4에서 또 크게 바뀌었습니다. 이 번역글(링크)를 참조해 주세요.
15- openpublic 접근 수준의 차이는 무엇인가?
open은 다른 모듈이 클래스를 사용하거나 상속받는게 가능한 것이다; 맴버의경우 다른 모듈이 그 맴버를 사용하거나 오버라이드할 수 있다.
public은 다른 모듈이나 public 클래스나 public 맴버로 사용할 수 있다. public 클래스는 더이상 하위클래스를 만들 수 없고 public 맴버도 오버라이드할 수 없다.

16- fileprivateprivate 접근 수준의 차이는 무엇인가?
fileprivate는 현재 파일 안에서 접근할 수 있음을 의미하고, private는 현재 선언 안에서 접근할 수 있음을 의미한다. 

17- 내부 접근(Internal access)이 무엇인가?
내부 접근(internal)은 모든 소스 파일 안에서 그들이 정의한 모든 모듈 안에서 엔티티를 사용할 수 있게 하는 것이지만 모듈 바깥의 소스파일은 안된다.

내부 접근은 접근 수준의 디폴트이다. 코드에 어떤 접근 제어자도 쓰지 않았다면 디폴트로 내부접근 수준으로 되있을 것이다.

18- BDD와 TDD의 차이는 무엇인가?
BDD와 TDD의 가장 큰 차이는 공학도가 아닌 사람도 BDD 테스트 케이스를 읽을 수 있다는 점인데, 팀에게 매우 유리하다.

나는 iOS에서 Quick라는 BDD 프레임워크를 선호하고 그것과 궁합이 잘 맞는 Nimble가 있다.

19- Arrange-Act-Assert를 설명하라
AAA는 유닛테스트에서 코드를 정렬(arrange)하고 포맷화(format)시키는 패턴이다. 만약 XCTest를 쓰기로 했으면 각 테스트는 빈 줄을 기준으로 기능적인 섹션별로 그룹화할 것이다.
  • Arrange, 필요한 모든 예측과 입력
  • Act, 테스트중에 오브젝트나 메소드에서 일어나는 것
  • Assert, 예상한 결과가 나오는 것

20- iOS앱에서 테스트를 만드는 것에는 어떤 이점이 있는가?
  • 좋은 테스트는 예상되는 동작의 훌륭한 문서가 된다.
  • 우리가 뭔가 바꾸면 테스트가 실패할거라는 것을 알고 있기 때문에 계속해서 우리 코드를 리팩토링할것이라는 확신을 준다.
  • 테스트를 짜기 어려우면 그 아키텍처를 개선시킬 수 있다. 다음 RGR(Red-Green-Refactor)는 초기에 개선할 수 있게 도와준다.

21- 모바일 제품 설계의 타이포그래픽 퀄리티를 개선시키기위한 필수의 다섯가지 가이드라인은 무엇인가?
  1. 텍스트의 서체를 정하는 것부터 시작하라
  2. 서체를 섞어 쓰는 것을 피하라
  3. 줄 길이를 확인해라
  4. 줄의 높이와 포인트 크기의 균형을 맞춰라
  5. 적절한 아포스트로피(Apostrophe)와 데쉬(Dash)를 사용하라

22- if let의 구조를 설명하라
if let은 스위프트에서 옵셔널 rhs가 값을 가지고 있는지 확인하게 해주는 특별한 구조이다. - 만약 값이 있다면 언랩시켜서 lhs에 할당한다.

23- 어떻게 문맥으로 앱을 학습시킬 것인가?
사용자를 돕는 문맥 기술상의 학습은 요소와 상호소통하고 아주 이전에 완성하지 못하는 방법에 직면한다. 이 기술은 종종 약간의 시각적 단서나 미묘한 애니메이션을 포함한다.



24- bitcode란 무엇인가?
XML타입 목적으로 사용할 수 있는 매우 일반적인 인코딩 포맷이다. 스스로 설명하는 파일 포맷이며 여러 다른 것들이 Bitcode로 인코딩 될 수 있다. Apple Watch 앱은 Bitcode로 되어야하고 Apple TV도 필요하다. iOS앱은 아직 선택적으로 가능하다. 이것에대한 이점은 컴파일러가 keep getting better 하는 것이다.

25- 스위프트 표준 라이브러리 프로토콜을 설명하라
세가지 프로토콜이 있다. equatable 프로토콜은 같은 타입의 두 인스턴스가 어떻게 구별되는지를 결정한다. 우리 앱에서 특정 값을 가지고 있다면. comparable 프로토콜은 같은 타입의 두 인스턴스를 비교하며 sequence 프로토콜은 prefix(while:)drop(while:)이다. [SE-0045]

26- SVN과 Git의 차이점은 무엇인가?
SVN은 버전 관리를 위한 중앙집중식 체계를 따른다. 중앙 저장소는 작업물의 복사본을 생성하고 접근시 네트워크 접속을 필요로한다.

Git은 버전 관리를 위한 분산형 체계를 따른다. 작업하고있는 로컬 저장소를 가질 것이고, 동기화가 필요할 때만 네트워크 접속을 한다.

27- CollectionViewTableView의 차이점은 무엇인가?
TableView는 한 컬럼으로 여러 항목들의 리스트를 보여주고, 수직적 방법이며, 수직 스크롤로 제한된다.
CollectionView도 여러 항복의 리스트를 보여주지만 여러 컬럼과 로우를 가질 수 있다.

28- Alamofire가 하는일은 무엇인가?
Alamofire는 요청/응답 메소드, JSON 파라미터, 응답 시리얼라이즈, 인증등 여러 기능을 연결할 수 있게(chainable) 해준다.

29- REST, HTTP, JSON가 무엇인가?
HTTP는 어플리케이션 프로토콜이며 규칙들의 집합이다. 웹사이트는 웹서버에서부터 클라이언트까지 데이터를 전송하는데 사용한다. 클라이언트(웹브라우저나 앱)는 필요한 행동을 나타내는데 사용한다.
  • GET : 웹페이지같은 것이 데이터를 검색하는데 사용한다. 그러나 서버의 데이터를 변경할 순 없다.
  • HEAD : GET의 식별이지만 헤더에 되돌려받으며 실제 데이터는 들어있지 않다.
  • POST : 서버에 데이터를 보내는데 사용되며, 보통 양식(form)을 채워넣고 보내기(submit)버튼을 누를때 사용된다.
  • PUT : 특정 제공된 위치에 데이터를 보낼때 사용한다.
  • DELETE : 특정 제공된 위치의 데이터를 지울 때 사용한다.

REST(혹은 REpresentational State Teansfer)는 일관성을 설계하기위한 규칙들의 집합이며, 웹-API를 사용하기 쉽고 유지보수하기 용이하게 만든다.

JSON은 JavaScript Object Notation의 약자로서, 직관적이고 사람이 읽을 수 있도록 해주며, 두 시스템간의 데이터 전송을 위한 포터블 메커니즘이다. 애플은 오브젝트를 변환하거나 그 반대로 할때 JSONSerailization 클래스를 사용할 수 있게 해놓았다.

30- 델리게이션이 해결할 수 있는 문제는 무엇인가?
  • 오브젝트들이 엮이는것을 막는다
  • 자식클래스 오브젝트 필요 없이 동작이나 모습을 수정한다
  • 임의의 오브젝트에게 작업을 넘겨줄 수 있다

31- 프레임워크의 주 목적이 무엇인가?
프레임워크는 세가지 목적을 가진다
  • 코드의 캡슐화
  • 코드의 모듈화
  • 코드의 재사용성

당신은 다른 앱이나 팀원, 아니면 iOS 커뮤니티에서 프레임워크를 공유할 수 있다. 스위프트의 접근 제어와 함께 쓰면 프레임워크는 강력하게 정의하고 코드 모듈간에 테스트할 수 있는 인터페이스를 선언할 수 있게 해준다.

32- 스위프트 패키지 매니저를 설명하시오
스위프트 패키지 매니저는 스위프트 생태계를 광활하게 증대시켜주며, 리눅스같은 Xcode가 없는 플랫폼에서도 스위프트를 배포하고 사용하기 쉽게 만들어준다. 스위프트 패키지 매니저는 의존성 지옥이라는 문제를 안고있는데, 이것은 서로 의존하는 라이브러리를 사용할때 나타날 수 있다.

스위프트 패키지 매니저는 마스터 브런치 사용만 지원한다.

33- 일대다 패턴으로 엮임을 막기 위한 소통 방법은 무엇인가?
노티피케이션

34- 일대일 패턴으로 엮임을 막기 위한 소통 방법은 무엇인가?
한 오브젝트에서 메소드를 호출하는 것

35- 텍스트 필드의 이벤트를 받기위해 왜 델리게이트 패턴을 사용하는가?
그 이벤트에 대해서는 대부분 한 오브젝트에서만 필요로 하기때문이다

36- inout을 명시한 파라미터가 보통 파라미터와 다른점은 무엇인가?
보통 파라미터가 pass by value여도 inout은 pass by reference로 해준다.

37- 뷰컨트롤러의 라이프사이클 이벤트 순서를 설명하라
몇몇 다른 라이프사이클 이벤트가 있다.

- loadView
컨트롤러가 관리하는 뷰를 만든다. 이것은 뷰컨트롤러가 생성되고 순차적으로 완성되었을때만 호출된다.

- viewDidLoad
컨트롤러의 뷰가 메모리에 올라간 뒤에 호출된다. 뷰가 생성될때만 호출된다.

- viewWillApear
화면에 뷰가 표시될때마다 호출된다. 이 단계는 뷰는 정의된 바운드를 가지고 있지만 화면회전은 적용되지 않는다.

- viewWillLayoutSubviews
뷰컨트롤러에게 그 자식뷰의 레이아웃을 조정하는 것에 대한 것을 알려주기위해 호출된다. 이 메소드는 frame이 바뀔때마다 호출될 것이다.

- viewDidLayoutSubviews
뷰가 그 자식 뷰의 레이아웃에 영향을 준 것을 뷰컨트롤러에 알려주기 위해 호출된다. 뷰가 그 자식뷰의 레이아웃을 바꾸고난 뒤에 추가적인 변경을 하고싶으면 여기서 하면된다.

- viewDidAppear
뷰가 뷰 계층에 추가되었다고 뷰컨트롤러에 알려준다.

- viewWillDisappear
다음 뷰컨트롤러로 트랜지션하기 전에 일어나며, 기존의 뷰컨트롤러가 화면으로부터 사라지기 전에 이 메소드가 호출된다.

- viewDidDisappear
뷰컨트롤러가 화면에서 사라지고나면 이 함수가 호출된다. 뷰컨트롤러가 화면에 없을때, 실헹시키지 않을 작업들을 중지시키는 용도로 이 메소드를 오버라이드한다.

38- LLVM과 Clang의 차이점은 무엇인가?
Clang는 LLVM 툴 체인의 프론트엔드이다(“clang” C Language Family Frontend for LLVM). 대부분 컴파일는 세가지 파트로 나뉜다.
  1. 앞단-front end(lexical analysis, 파싱)
  2. 최적화기-optimizer(추상 문법 트리를 최적화)
  3. 뒷단-back end(기계어를 생성)

앞단(clang)은 코드를 받아서 추상 문법 트리(LLVM IR)을 생성한다.

39- 클래스란 무엇인가?
클래스는 오브젝트의 선언과 그것이 어떻게 동작하는지를 담고있다. 이 경우, 클래스는 오브젝트의 청사진과 같다.

40- 오브젝트란 무엇인가?
오브젝트는 클래스의 인스턴스이다.

41- 인터페이스란 무엇인가?
Objective-C에서 @interface는 Java의 interface처럼 아무것도 하지 않는다. 간단하게 클래스의 공개된 인터페이스를 선언하며, 그것의 공개 API이다.

42- 언제, 왜 구조체대신에 오브젝트를 사용해야하나?
구조체는 값타입이다. 클래스(오브젝트)는 참조타입이다.

43- UIStackView란 무엇인가?
UIStackView는 수평 혹은 수직으로 일련의 뷰 레이아웃을 잡아주는 방법이다. 사용가능한 공간에 담겨진 뷰들을 어떻게 조정할지 정의할 수 있다. 이 글을 꼭 읽어보길 바란다.

44- iOS 앱 상태는 무엇이 있나?
  1. Non-running : 앱이 실행되지 않고 있음
  2. Inactive : 앱이 포그라운드에서 실행되고 있으나 이벤트는 받지 않음. iOS앱은 inactive 상태로 들어갈 수 있는데, 전화나 SMS 메시지를 받을때가 그 예이다.
  3. Active : 앱이 포그라운드에서 실행되며 이벤트도 받고 있음.
  4. Background : 앱이 백그라운드에서 실행되고 코드를 실행하고 있음.
  5. Suspended : 앱이 백그라운드에 있으나 코드가 실행되지 않음.

45- 개잘자가 다뤄야할 가장 중요한 어플리케이션 델리게이트 메소드는 무엇인가?
운영체제는 다양한 상태로/로부터 변경된것을 편리하게 하기 위해 앱 델리게이트를 이용하여 특정 메소드를 호출한다.아래 일곱가지는 개발자가 다룰 가장 중요한 어플리케이션 델리게이트 메소드이다.

application:willFinishLaunchingWithOptions
시작 프로세스가 초기화되면 호출되는 메소드이다. 이 메소드는 앱에서 어떤 코드보다 가장 먼저 실행되는 기회를 갖는다.

application:didFinishLauchingWithOptions
시작 프로세스가 거의 완료되면 호출되는 메소드이다. 앱 창에 표시되기 직전에 호출되는 메소드이기때문에 인터페이스를 준비하고 최종 조정을 할 수 있는 마지막 기회이다.

applicationDidBecomeActive
앱이 활성상태가 되면 앱 델리게이트는 applicationDidBecomeActive 메소드를 통해 콜백 노티피케이션 메시지를 받을 것이다.

또한 이 메소드는 이전의 비활성(inactive) 상태에서 활성상태로 돌아올때마다(전화나 SMS에서 돌아올때마다) 호출된다.

applicationWithResignActive
applicationWillResignActive 메소드가 호출되는데는 여러 조건들이 있다. 임시적인 이벤트(핸트폰 전화같은)가 일어날때마다 메소드가 호출된다. 또한 iOS앱을 "끈다(quitting)"는 말은 프로세스를 죽인다는 뜻이 아니라 앱을 백그라운드(background)로 보낸다는 뜻임에 주의하자.

applicationDidEnterBackgroud
이 메소드는 iOS 앱이 동작하고 있지만 더이상 포그라운드(foreground)에 있지 않을때 호출된다. 다른말로는 사용자 인터페이스가 현재 보여지고 있지 않다는 뜻이다. 애플의 UIApplicationDelegate Protocol Reference에따르면 앱은 작업을 실행하고 리턴하기위해 대략 5초정도 가지고 있는다. 이 메소드가 5초안에 나타나지 않았다면 앱은 죽은 것이다.

applicationWillEnterForeground
이 메소드는 앱이 백그라운드에서 포그라운드로 넘어갈 준비가 되었을때 호출된다. 그러나 앱이 applicationDidBecomeActive 메소드가 호출되지 않은채로 활성 상태로 넘어가지 않는다. 이 메소드는 앱이 활성상태로 오기 전에 이전 실행 상태로 다시 세팅할 수 있는 기회를 준다.

applicationWillTerminate
이 메소드는 종료 이벤트가 발생했을때 알려주는 어플리케이션 델리게이트이다. 홈버튼을 눌러서 앱일 끈다. iOS를 강제로 종료하거나 기기를 끄게되면 applicationWillTerminate 메소드가 호출된다. 이것은 앱 구성, 설정, 사용자 선택을 저장할 수 있는 기회를 제공해준다.

45- 코드 사이닝(code signing)이 하는 일은 무엇인가?
앱을 사이닝하면 iOS가 우리 앱에 사인한 사람이 누군지 식별하고 사인한 이휴로 앱이 변경되지 않았다는 것을 보장한다. 사이닝 식별(Signing Identity)는 애플이 만든 public키-private키 쌍으로 이루어져있다.

46- 프로퍼티와 인스턴스 변수의 차이는 무엇인가?
프로퍼티는 추상적인 개념이다. 인스턴스 변수는 그냥 저장 슬롯이다(구조체의 슬롯처럼). 일반적으로 다른 오브젝트는 그것에 직접 접근할 수 없다고 가정한다. 보통 프로퍼티는 인스턴스 변수를 겟(get)하거나 셋(set) 하지만, 어떨때는 여러곳으로부터 데이터를 불러와 사용할 수도 있고 불러오지 않고서 바로 사용할 수도 있다.

47- 스위프트 패키지 매니저
스위프트 패키지 매니저는 UIKit을 지원하지 않는다. 스위프트 패키지 매니저를 이용하면 여러분은 파일 템플릿이나 다른 프로젝트를 위한 프레임워크를 만들 수 있다.

48- SDK와 프레임워크의 차이를 설명하라
SDK는 소프트웨어 개발 툴의 집합이다. 이 집합은 앱을 만드는데 사용한다. 프레임워크는 기본적으로 소프트웨어 어플리케이션을 개발하는데 사용되는 플랫폼이다. 이것은 특정 플랫폼에서 개발될 수 있는 플랫폼에 필요한 파운데이션을 제공한다. SDK와 프레임워크는 각자의 완성본이며 SDK는 프레임워크로 이용가능하다.

49- 다운캐스팅(Downcasting)은 어떻게 사용할까?
Objective-C에서 다른 타임으로 케스팅할때는 그렇게 하는 방법이 하나밖에 없었으므로 아주 간단했다. 스위프트에서는 두가지 방법의 캐스팅이 존재하는데, 한가지는 세이프하고 한가지는 그렇지 않다.
  • as는 업캐스팅(Upcasting)에 사용되고 타입을 연결된 타입에 타입캐스팅에 사용된다.
  • as?는 실패하면 nil을 반환하는 안전한 캐스팅으로 사용된다.
  • as!는 실패하면 크래쉬가나는 강제 캐스팅에 사용된다. 다운캐스팅이 실패하지 않을 것이라는 것을 알때만 사용한다.

50- do-catch 블락은 왜 있는가?
스위프트에서 에러는 do-catch 블락 안에서 에러를 받아(thrown)서 다룬다(handled),



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: 50 iOS Interview Questions And Answers - Part1


 



1- 실시간 랜더링(Live Rendering)은 어떻게 세팅하는가?
@IBDesignable 속성은 특정 뷰에서 인터페이스 빌더를 실시간 갱신시키게 해준다.

2- 동기 & 비동기 작업의 차이가 무엇인가?
동기: 작업이 완료되기 전까지 기다린다.
비동기: 백그라운드에서 작업을 완료하고나면 알림을 받을 수 있다.

3- B-Tree란?
B-Tree는 정렬된 키-값을 제공하는 서치 트리이며 훌륭한 퍼포먼스의 특징을 가진다. 각 노드는 정렬된 그 고유의 요소 배열을 가지고 있고, 그 자식 노드를 위한 또다른 배열을 가지고 있다.

4- NSError 오브젝트는 무엇으로 구성되있는가?
NSError 객체는 도메인, 에러코드, 사용자 정보 딕셔너리의 세가지 부분으로 나뉜다. 도메인은 어떤 카테고리의 에러인지, 어디서부터 온 에러인지 식별하는 문자열이다.

5- 좋아하는 WWDC 영상은 무엇인가?
2015년도의 Protocol-Oriented Programming in Swift이다.
2016년도의 Visual Debuggin with Xcode이다.

6- 바운딩 박스(bounding box)란?
바운딩 박스는 기하학에서 쓰이는 용어인데, 주어진 점의 집합으로 나타내는 가장 작은 단위(넓이나 부피)이다.

7- Objective-C에서는 왜 열거형을 속성에서 strong을 쓰지 않을까?
열거형은 객체가 아니므로 특정 strong이나 weak를 지정할 수 없다.

8- Objective-C에서 synthesize는 무엇인가?
synthesize는 여러분의 프로퍼티를 위해 gettersetter 메소드를 생성해준다.

9- Objective-C에서 dynamic이란 무엇인가?
우리는 NSManagedObject의 자식 클래스를 위한 dynamic을 사용한다. 또한 @dynamic는 델리게이트를 접근자 구현의 역할에 익숙하다.

10- 왜 synchronized를 사용할까?
synchronized는 주어진 시점에서 한 스레드가 블럭안의 코드를 실행시킬 수 있음을 보장한다.

11- strong, weaks, read only, copy의 차이는 무엇인가?
strong는 참조 카운트가 증가될 것이고 그것에 참조는 그 객체의 생명주기를 통해 유지될 수 있음을 의미한다.
weak는 객체에 포인터를 가리키고 있지만 잠조 카운팅을 올리진 않는다. 부모 자식 관계가 만들어질때 종종 사용된다. 부모는 자식에게 strong 참조를 가지지만 자식은 부모에게 weak 참조를 가지게 만든다.
read only, 초기에 속성을 세팅할 수 있지만 변경할 수 없다.
copy는 객체가 생성될때 객체의 값을 복사한다는 의미이다. 또한 변경으로부터 값을 보호한다.

12- 다이나믹 디스패치란(Dynamic Dispatch)?
다이나믹 디스패치는 여러 형태의 오퍼레이션 구현 선택의 처리인데, 이 오퍼레이션은 런타임시 호출하기위한 메소드나 함수이다. 이 의미는 오브젝트 메소드처럼 우리 메소드를 불러내고 싶을 때이다. 그러나 스위프트는 다이나믹 디스패치에 디폴트가 아니다.

13- 코드 커버리지란 무엇인가?
코드 커버리지는 유닛테스트의 측정 값을 측정하기 위한 도구이다.

14- 컴플리션 핸들러란 무엇인가?
컴플리션 핸들러는 우리 앱이 그 작업이 끝났을때 뭔가 API 호출 하게 만들고 싶을때 최고의 편의를 만들어준다. 애플의 dataTaskWithRequest같은 API에서 컴플리션 핸들러를 볼 수 있고 당신 코드에서 꽤 간편하게 사용할 수 있게 해준다.

컴플리션 핸들러는 세가지 인자(NSDate?, NSURLResponse?, NSError?)로 코드를 받는데, 아무것도 반환하지 않는다(Void). 이것은 클로저이다.

15- 디자인에서 유용성 우선순위를 어떻게 세우는가?
유용성을 우선순위 매기기 위해서는 4단계로 디자인 과정을 쪼갠다.
  • 사용자처럼 생각하고 UX를 설계하기
  • 사용자가 밀집된 인구가 아닌 사람들이라는 점을 기억하기
  • 앱을 알릴때 모든 상황에서 유용한지 고려하기
  • 앱을 출시한 이후에서 계속 앱 사용성을 고민하기

16- framebounds의 차이는 무엇인가?
UIViewbounds는 그 고유 좌표 시스템 (0, 0)에 연관된 position (x, y)size (width, height)로 표현되는 사각형이고,
UIViewframe은 그 뷰를 담고있는 부모뷰에 연관된 position (x, y)size (width, height)로 표현되는 사각형이다.

17- Responder Chain이란 무엇인가?
ResponderChain은 받은 이벤트에 응답하기위한 기회를 가진 객체들의 계층을 의미한다.

18- 정규식(Regular expressions)이란 무엇인가?
정규식은 문자열을 통해 어떻게 검색할지 표현하는 특수한 문자열 패턴이다.

19- 연산자 오버로딩(Operator Overloading)이란 무엇인가?
연산자 오버로딩은 이미 있던 연산자의 동작을 바꿀 수 있게 해준다.

20- TVMLKit이란 무엇인가?
TVMLKit은 TVML과 JavaScript, 네이티브 tvOS 앱을 붙여주는 역할을 한다.

21- tvOS의 플랫폼 한계는 무엇인가?
첫째, tvOS는 어떤 종류로더 브라우저를 제공하지 않으며 여러분이 프로그래밍할 수 있는 어떠한 WebKit이나 웹기반 렌더링 엔진도 없다. 이 말은, 여러분의 앱이 웹 링크나 OAuth, 소셜 미디어 사이트같은 어떤것도 링크를 타고 웹브라우저로 갈 수 없다는 의미이다.

두번째, tvOS 앱은 명시적으로 로컬 스토리지를 사용할 수 없다. 제품 출시 시점에 기기들은 32GB나 64GB의 하드 용량을 탑재한다. 그러나 앱은 그 탑재된 스토리지에 직접 쓰기를 할 권한이 없다.

tvOS 앱 번들은 4GB를 초과하지 못한다.

22- 함수란 무엇인가?
함수는 어떤 작업을 수행하기위해 일련의 명령을 함께 모을 수 있게 해준다. 함수를 만들어 놓으면, 코드상에서 계속 사용할 수 있다. 만약 여러분의 코드에서 중복되는 명령을 찾는다면, 함수가 그 반복을 피하는데 답이 되어 줄 것이다.

23- ABI란 무엇인가?
ABI는 외부 라이브러리를 사용하게 되는 앱이라면 중요하다. 프로그램이 특정 라이브러리를 사용하도록 만들어져있고, 나중에 그 라이브러리가 업데이트될때마다 앱을 다시 컴파일 하고 싶지 않을 것이다(그리고 끝 사용자 관점에서, 그 소스를 가지고 있지 않을 것이다). 업데이트된 라이브러리가 같은 ABI를 사용한다면 여러분의 프로그램은 변경할 필요가 없다.



24- 디자인 패턴이 왜 중요한가?
디자인 패턴은 소프트웨어 설계에서 흔히 만나는 문제들에대한 재사용가능한 솔루션이다. 이것은 이해하기 쉽고 재사용하기 쉬운 코드를 짜는데 도움을 주기위한 설계의 템플릿이다. 아래는 아주 일반적인 Cocoa 디자인 패턴들이다.
  • 생성 : 싱글톤
  • 구조 : MVC, Decorator, Adapter, Facade
  • 동작 : 옵져버, Memento

25- 싱글톤 패턴이란 무엇인가?
싱글톤 디자인 패턴은 주어진 클래스에대해 오직 하나의 인스턴스만 존재하도록 보장하고 그 인스턴스를 가리키는 전역 접근을 가진다. 보통 그 싱글톤이 필요한 첫번째 호출에서 싱글톤 인스턴스를 lazy하게 불러와서 생성한다. 

26- Facade 디자인 패턴이란 무엇인가?
Facade 디자인 패턴은 복잡한 서브세스템에 하나의 인터페이스를 제공해준다. 클래스들이나 API들의 집합을 사용자에게 보여주는 것 대신에, 하나로 모인 간단한 API를 보여준다.

27- Decorator 디자인 패턴이란 무엇인가?
데코레이터 디자인 패턴은 그 코드를 수정하지 않고 한 오브젝트에대한 행동자와 책임들을 동적으로 추가한다. 다른 오브젝트로 그 클래스의 동작을 수정하려할때 서브클래스로 만들지 않고 서 가능하게 해준다.

28- 어답터(Adapter) 패턴이란 무엇인가?
어답터는 클래스들의 호환되지 않는 인터페이스 간에 함께 작업할 수 있게 해준다. 오브젝트를 감싸고 그 오브젝트와 소통하기위해 표준 인터페이스를 보여준다.

29- 옵저버 패턴이란 무엇인가?
옵저버 패턴에서 어떤 오브젝트의 상태변화를 다른 오브젝트에게 알려준다.

30- Memento 패턴이란 무엇인가?
Memento 패턴에서는 어딘가 작업을 저장한다. 그뒤에 이 외부화된 상태는 캡슐화 파괴를 하지 않고서 다시 저장될 수 있다; private 데이터는 private로 남는다. 애플에서 Memento 패턴의 구현 중 하나는 Archiving이다.

31- MVC를 설명하시오
  • Models : 도메인 데이터의 역할이나 데이터를 조작하는 레이어를 접근하는 데이터. 'Person' 이나 'PersonDataProvider'를 생각하면 된다.
  • Views : 표현하는 레이어(GUI)의 역할, iOS 환경에서는 접두에 UI로 시작하는 모든 것들이라고 생각하면 된다.
  • Controller/Presenter/ViewModel : Model과 View의 중개인, 보통은 View에서 실행된 사용자 행동에 반응하여 Model을 바꾸고 바뀐 Model로부터 View에 갱신하는 역할을 한다.

32- MVVM을 설명하시오
View와 그 상태의 표현이 독립적인 UIKit. ViewModel은 Model에서 바뀐 것을 부르고, 갱신된 Model로 그 자신을 갱신한다. 그리고 View와 View Model 사이에 바인딩되있으므로 첫번째가 적절하게 갱신된다.

View Model은 실제로 여러분의 Model안에 들어있을 것이고 View에 나타날 정보의 틀을 잡을 수 있다.

33- Objective-C에서 어떤 주석(annotation)을 사용할 수 있을까?
  • _Null_unspecified, 이것은 스위프트스위프트 묵시적 언랩핑된 옵셔널에 연결해준다. 디폴트로 되어있다.
  • _Nonnull, 이 값은 nil이 되면 안될때 정규 참조에 연결해준다.
  • _Nullable, 이 값은 nil이 될 수 있고, 옵셔널로 연결해준다.
  • _Null_resettable, 읽을때, 그러나 재설정한다는 것을 알기위해 이것을 설정할 수 있을때, 이 값은 절때 nil이 될 수 없다. 이것은 프로퍼티에만 적용가능하다.

34- JSON/PLIST의 한계은 무엇인가?
  • 당신의 오브젝트들를 생성하고 디스크에 시리얼라이즈한다(serialize)
  • 이것은 훌륭하나 매우 제한된 유스케이스이다
  • 여러분의 결과물을 필터링하기위한 복잡한 쿼리를 명확하게 사용할 수 없다.
  • 매우 느리다
  • 뭔가 필요할 때마다, 시리얼라이즈나 디시리얼라이즈를 해야한다.
  • 스레드-세이프 하지 않다

35- SQLite의 한계는 무엇인가?
  • 테이블간의 관계를 정의해야한다. 모든 테이블마다 스킴을 정의한다
  • 데이터를 패치하기위해 손수 쿼리를 작성해야한다
  • 데이터를 쿼라한 다음 모델에 매핑 시켜야한다
  • 쿼리는 매우 빠르다

36- Realm의 장점은 무엇인가?
  • 오픈소스 데이터베이스 프레임워크이다
  • 스크래치에서 구현되었다
  • Zero copy object store
  • 빠르다

37- 베터리-효율 위치 추적을 위한 API가 몇개나 있는가?
3개의 api가 있다.
  • 중요한 위치 변화(Significant location changes) : 약 500미터마다 위치가 전송된다.(때로는 1km 이상일 때도 있다)
  • 지역 모니터링(Region monitoring) : 반경이 100m나 그 이상으로의 원형 지역에서 들어가고 나가는 것을 추적한다. 지역 모니터링은 GPS 이후 가장 정확한 API이다.
  • 방문 이벤트(Visit events) : 한 장소(집/사무실)로 들어가고 나오는 방문 이벤트 장소를 모니터링한다.

38- 스위프트스위프트의 주 이점은 무엇인가?
  • 옵셔널타입, 앱의 크레쉬를 방지해준다
  • 내장된 에러 핸들링
  • 클로저
  • 다른 언어에비해 빠르다
  • 타입-세이프 언어
  • 패턴 매칭을 지원한다

39- 스위프트에서 제네릭을 설명하라
제네릭은 특정 데이터타입에 의존하지 않는 코드를 만드는 것이다.

40- 스위프트에서 lazy를 설명하라
프로퍼티에 저장된 lazy로된 초기 값은 이 프로퍼티가 제일 처음 호출되었을때 계산된다. 개발자에게 lazy 프로퍼티가 아주 유용할때가 있다.

41- defer이 무엇인지 설명하라
defer 키워드는 실행이 현재 스코프를 벗어날때 호출될 코드 블럭을 제공해준다.

42- 변수를 어떻게 참조하여 전달할 수 있는가?
변수에는 두가지 타입이 있다는 점부터 언급해야한다. 참조타입과 값타입이다. 이 두 타입의 차이점은, 값타입을 전달할때는 그 데이터의 복사본을 만들어낼것이고, 참조타입의 변수는 그냥 메모리에서 원래 데이터의 포인터를 가리키고 있을 것이다.

43- 왜 higher order 함수를 사용하는 것이 낫나?
파라미터로 함수를 받을 수 있거나 결과로 함수를 반환할 수 있는 함수들을 higher-order 함수라 부른다. 스위프트는 이런 함수들을 CollectionType으로 정의한다.

아주 기본적인 higher order 함수로는 filter가 있다.

44- 동시성(Concurrency)이란 무엇인가?
동시성은 여러분의 프로그램에서 실행 경로를 쪼개는 것이므로 동일한 시간에 실행될 것이다. 일반적인 용어에는 프로세스, 스레드, 멀티스레드, 등이 있다.
  • 프로세스, 실행되고있는 한 앱의 한 인스턴스
  • 스레드. 코드상의 실행 경로
  • 멀티스레드, 동시간에 실행되는 다중 스레드 혹은 다중 경로
  • 동시성, 스케일 조절이 가능한 방법으로 한번에 여러 작업을 실행시키는 것
  • 큐, 큐는 먼저들어간 것이 먼저나온다는 FIFO(First-in, First-out)방식으로 오브젝트를 관리하는 경량의 자료구조
  • 동기 vs 비동기 작업

45- Grand Central Dispatch(GCD)
GCD는 뒤편에서 스레드를 관리하면서 동시적으로 작업을 실행시키는 저수준 API를 제공하는 라이브러리이다. 용어에는 다음이 있다.
  • Dispatch Queues, 디스패치 큐는 FIFO 순서로 작업을 실행시키는 역할을 담당한다.
  • Serial Dispatch Queue, 시리얼 디스패치 큐는 한번에 한 작업만 실행시킨다.
  • Concurrent Dispatch Queue, 컨커런트 디스패치 큐는 시작한 작업이 끝나는것을 기다리지 않고 가능한 많은 작업을 실행시킨다.
  • Main Dispatch Queue, 앱의 메인 스레드에서 작업을 실행할 수있는 전역에서 사용가능한 시리얼 큐

46- Readers-Writers
읽기는 동시에 여러 스레드에서 할 수 있지만 쓰기는 한 스레드에서만 할 수 있다. 이 문제에 대한 해결책은 readers-writers인데, 이것은 이 방법은 동시에 read-only 접근을 허용하고 독점적인 write 접근을 하게 해준다.
아래는 용어들이다.
  • Race Condition(경쟁 상태), 경쟁상태는 공유된 데이터를 2개 이상의 스레드가 접근하거나 동시에 서로 변경하려고 할 때 나타난다
  • Deadlock(데드락), 데드락은 2개 혹은 그 이상의 작업이 다른 작업이 끝나기를 서로 기다리고 있을때 나타난다
  • Readers-Writers problem, 읽기는 동시에 여러 스레드에서 할 수 있지만 쓰기는 한 스레드에서만 할 수 있다
  • Readers-Writer lock, 이런 락은 동시에 공유된 데이터에 read-only 접근은 허용하되 write 작업은 독점적인 접근을 요구한다
  • Dispatch Barrier Block, 디스패치 베리어 블럭은 동시적인 큐와 작업할때 시리얼-스타일(serial-style)의 버틀넥을 만든다

47- NSOperation-NSOperationQueue-NSBlockOperation
NSOperation은 GCD와 비교했을땐 추가적인 오버해드가 있으나, 다양한 작업들 가운데 의존성을 추가할 수 있고, 재사용, 취소, 중지시킬 수 있다.
NSOperationQueue, 이것은 NSOperation들을 만들어서 병렬로 실행시키는 스레드 풀을 제공한다. Operation queue가 GCD의 일부는 아니다.
NSBlockOperation은 하나 혹은 그 이상의 클로저에서 NSOperation을 생성할 수 있게 해준다. NSBlockOperation들은 동시적으로 실행하는 다중 블락을 가질 수 있다.

48- KVC-KVO
KVC는 Key-Value Coding을 의미한다. 이 메커니즘은 개발시점에 프로퍼티 이름을 알아야하는게 아니라, 런타임중에 문자열을 사용하여 한 오브젝트의 프로퍼티들에 접근할 수 있게 해준다. 
KVO는 Key-Value Observind을 의미하고 컨트롤러나 클래스가 프로퍼티 값이 변경되는지 관찰(observe)하게 해준다. KVO에서는 한 오브젝트가 특정 프로퍼티가 변경되는 모든 알림을 달라고 요구할 수 있으며, 그 프로퍼티는 자기자신것도 되고 다른 오브젝트의 프로퍼티도 된다.

49- 스위프트스위프트의 패턴 매칭 기술들을 설명하라
  • 튜플 패턴(Tuple patterns)은 튜플타입에 해당하는 값을 매칭시키는데 사용된다.
  • 타입케스팅 패턴(Type-casting patterns)은 타입을 캐스팅하거나 매칭할 수 있게 해준다.
  • 와일드카드 패턴(Wildcard patterns)은 어떤 종류나 타입의 값을 매칭 혹은 무시한다.
  • 옵셔널 패턴(Optional patterns)은 옵셔널 값을 매칭하는데 사용된다.
  • 열거형 케이스 패턴(Enumeration case patterns)은 존재하는 열거형 타입의 케이스를 메칭한다.
  • 표현식 패턴(Expression patterns)은 주어진 식에대한 주어진 값을 비교할 수 있게 해준다.

50- guard의 장점은 무엇인가?
guard에는 두가지 큰 장점이 있다. 한가지는 다른사람들이 언급해왔던 피라미드 코드를 피한다는 것이다-엄청나게 많은 수의 if let 절은 각 안으로 점점 오른쪽으로 가면서 감싸이게 된다. 다른 이점은 breakreturn을 사용하여 초기에 함수를 빠져나갈 수 있게 한다는 것이다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

NSCodingNSObjectProtocol이라는 클래스 프로토콜이 필요하다. 그리고 그 프로토콜은 구제체가 따를 순 없다. NSCoding을 사용해서 인코딩하고 싶다면 가장 쉬운 방법이 클래스로 만들어 NSObject를 상속받는 것이다.

나는 구조체를 NSCoding으로 감쌀 수 있는 말끔한 방법을 찾았고 거추장스러운 작업 없이 저장할 수 있다. 예제로서 Coordinate를 사용할 것이다.

두개의 스칼라 프로퍼티를 가지는 간단한 한 타입이다. 이제 NSCoding을 따르고 Coordinate를 감싸는 클래스를 만들어보자.

이 로직을 다른 유형으로 사용하는 것이 좋으며 단일 책임 원칙에 더 잘 지킨다. 눈치 빠른 독자는 위 클래스에 Coordinate 프로퍼티의 EncodableCorrdinate가 옵셔널이라는 것을 눈치 챘을 것이지만, 꼭 그럴 필요는 없음을 알 수 있을 것이다. 우리는 옵셔널이 아닌 Coordinate를 받는(혹은 실패하게 만드는) 생성자를 만들 수 있는데, 그렇다면 이미 init(coder:) 메소드는 불가능하다. 그리하여 EnabledCoordinate 클래스 인스턴스를 들고 있을때 항상 coordinate를 가지고 있음을 보장해주어야한다.

그러나 더블(Double)타입(혹은 어떤 원시 타입)을 인코딩 할 때 NSCoder 동작 방법의 특징으로 인해 Any?를 반환하는 decodeObejct(forKey:)로 추출해낼 수 없게 되었다. DoubledecodeDouble(forKey:)로 하여 특정 윈시타입에 특정 메소드가 있다. 불행히도 이 특정 메소드는 옵셔널을 반환하지 않으며, 키가 맞지 않거나 오류가 나면 0.0을 반환해 버린다. 이러한 이유 때문에 coordinate 프로퍼티를 옵셔널로 두기로 했고 옵셔널로 인코딩한다. 그리하여 나는 decodeObject(forKey:)를 사용해 Double?을 얻어냄으로써 추가적인 안정성을 보장했다.

이제 Coordinate 오브젝트를 인코딩/디코딩 하기 위해 EncodableCoordinate를 하나 만들고 NSKeyedArchiver로 디스크에 저장할 수 있다.

이러한 추가적인 오브젝트를 만드는게 이상적이진 않으며, 나는 "가능하면 나를 캐싱해줘" 글에서 사용한 SKCache와같은 오브젝트로 작업하기를 좋아한다. 그리하여 내가 인코더와 인코딩 된 사이의 관계를 형식화 할 수 있다면, 아마 매번 NSCoding 컨테이너를 만들지 않아도 될 것이다.

그 끝으로 두가지 프로토콜을 추가하자.
그리고 우리의 두가지 타입에 대해 적용시킨다.

이렇게하여 이제 타입 시스템은 오브젝트 쌍에서 타입과 값 사이에서 어떻게 변환하여 넣고 빼는지 안다.

블로그 포스팅에서 나온 SKCache 오브젝트는 Encoded 타입을 넘어 제네릭으로 업그레이드 되어왔다. 인코더 값 타입이 그 자신이라는 조건으로, 이것은 이 두 타입 사이에 쌍방향으로 변환할 수 있게 해준다.

이 타입에서 마지막 퍼즐조각인 savefetch 메소드가 남아있다. saveencoder를 잡아두고있고(사실 이것은 NSCoding을 따르는 오브젝트이다) path로 저장해둔다.

패칭은 약간의 컴파일러 댄스(compiler dance)를 포함한다. 우리는 언아카이브(unarchive)된 오브젝트를 T.Encoable로 캐스팅 해야하는데, 이것은 encoder 타입이다. 그리고 그 값을 잡아두고, 동적으로 그것을 캐스팅하여 T로 돌려준다.

이제 캐시를 사용하기위해 한 인스턴스를 만들고 Coordinate 제네릭으로 만든다.

이렇게 하면 좌표 구조체를 쉽게 저장하고 검색할 수 있다.
이것을 가지면 NSCoding을 이용해 구조체를 인코딩할 수 있고, 단일 책임 원칙에 따렴, 타입 세이프티하게 만들어준다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,





애플은 이번 2016년 WWDC에서 Xcode Source Editor Extensions를 소개했다. 내가 좋아하는 툴을 쉽고 간편한 방법으로 확장할 수 있게 해준다. 이것이 왜 생산성에 영향을 미칠지 의아해 한다면 아래 이야기를 계속 읽어보아라.

소프트웨어 개발 업계의 수많은 거장들은 이렇게 생각할 것이다. 전문성은 장인정신 자체에서 나오는 것이다. 이 비유는 특히 그렇다. — 도구. 좋은 장인은 좋은 도구가 필요다. 그러나 "못난 일꾼이 늘 연장 탓한다"이라는 속담이 있다.

크리스토프 고켈은 우리에게 이렇게 상기시켜주었다. "일을 할때 옳바른 연장을 손에 쥐어라" 그러나 적당한 연장이 없다면 우리 스스로 연장을 만들 수 있으며 이 이야기가 바로 우리가 해야할 일이다.

Xcode는 수많은 써드파티 플러그인을 사용했었다. "사용했었다"라고 과거시제를 쓴 이유는 Xcode8부터 더이상 플러그인을 지원하지 않기 때문이다. 이것은 굉장히 슬픈 소식이지만, 좋은 이유(보안상의 이유나 신뢰성과 관련되있는)에서 이렇게 하게 되었다.

애플은 무책임하게 IDE를 플러그인으로부터 벗어내어 개발자를 힘들게 하려는게 아니라, 새로운 방식으로 고유의 툴을 만들 수 있게 제공한다. WWDC에서 새 기능에 대해 멋지게 설명해주었지만 우리 App'n'roll'에서는 우리 손으로 직접 코드를 짜보는게 최고의 방법이라 생각하여 그렇게 해보았다.

JSON Models
많은 개발자들이 앱에서 네트워킹 작업을 할 것이다. 그리고 JSON을 파싱하여 모델로 만드는 작업을 하는데 시간을 소비한다. 이러한 일은 IDE에서 대신 해줄 수 있는 일이므로 우리는 extension으로 이것을 만들어 보기로 했다. 몇 가정을 하고 작업에 갔는데, 첫째로 현재 수정되는 파일은 JSON 타입이여야한다. 둘째로 감쌓여진(nested) 오브젝트에 연관된 가장자리 케이스는 무시한다.

중요 note : 우리 예제는 Xcode8.0 베타2에서 만들어졌고, 아마 항상 모든 버전에서 잘 동작하지는 않을지도 모른다. 만약 여전히 엘케피탄에서 작업하고 있다면 Xcode8.0 beta Release Notes에 들어가서 IDE와 Source Editor Extension에 관한 Xcode8.0 beta 이슈를 보아라. 또한 당신의 extension을 실행시킬때 약간의 딜레이가 있다. 만약 프로젝트가 너무 빨리 열린다면 extension이 불러와지기 전에 켜진것이며, 테스트 Xcode의 인스턴스 메뉴에서 사용할 수 없을 것이다.

이제 새 macOS 프로젝트를 생성하고(UnitTests 박스에 체크했는지 확인해보라) 기본 앱에서 Xcode Source Editor Extension이라는 새 타깃을 추가하여 시작해보자.



이렇게하면 하나의 Info.plist와 두개의 클래스를 자동으로 만들어 줄 것이다. 코드를 자세히 살펴보기 전에 먼저 plist를 보자. Xcode의 메뉴에 보이는 이름을 바꾸기 위해 Bundle Name과 XCSourceEditorCommandName을 고친다.

자동으로 만들어진 첫번째 클래스는 XCSourceEditorExtension이고 이것은 extension이 불러와질때 우리에게 알려주는 역할을 한다. 이번 프로젝트에서는 굳이 손 델 필요가 없다. 두번째 클래스는 XCSourceEditorCommand이다. 명령을 내릴때 실행되는 perform(with invocation:, completionHandler:)  메소드가 하나 프로토콜로서 정의되있을 것이다. 이 extension은 현재 파일의 내용물과 그것을 수정하는 방법을 제공한다. 우리는 간단하게 추상화된 층을 사용하여 유닛테스트하기 쉬운 방향으로 만들어갈 것이다. 파일과 함께 인터렉션 하는 것은 SourceFile 프로토콜을 따른다.

다음 순서는 테스트하기 쉽게 도와주는 오브젝트이다. 이것을 JSONConverter라 부르자.

SourceFile을 받고 뭔가 문제가 생기면 예외로 넘겨주는 메소드 하나만 가지고 있다. XCSourceEditorCommand와 합치려면 아래처럼 간단한 연결점이 필요하다.

아직까지는 extension을 실행하고 명령을 해도 아무일도 일어나지 않을 것이다. 이제 TDD 방식을 조금 사용해보자. 먼저 테스트에 기반한 시스템을 만들고 소스파일을 위한 테스트 쌍을 작성한다.

첫번째 테스트는 유효한 JSON으로 파싱되었는지 체크한다.

다음으로는 한 JSON 양식으로된 문자열을 JSON으로 파싱한다. 여기 테스트가 있다.

...그리고 여기 그 구현이 있다

이제 약간 꼼수를 써서 NSNumber의 서로다른 타입들을 위해 3개의 테스트를 만든다.
...그리고 이것은 한번에 테스트를 통과할 것이다.
다음 이 코드는 런타임동안 오브젝트의 타입을 체크하고 Swift 타입과 일치하게 만들어준다.

다음 우리의 리스트가 한 배열로 만들어지는지의 테스트이다.

The next step is parsing a simple JSON with one String property. Here is the test:

...그리고 구현이다.

마지막으로 감싸진 타입의 파싱 기능은 특별히 우리에게 필요한 것이었다. 이것이 가능한지 알아보는 테스트는 아래 테스트로 충분히 확인할 수 있다.

감싸진 타입은 다른 타입보다 더 많은 구현이 필요하다. 여기 JSONConverter의 완성된 구현이다.

끝이다. 우리는 앞으로 모델 네이밍 시스템을 약간 바꿀 예정이다. JSONConverter는 이미 XCSourceEditorCommand와 합쳐졌고 이제 어떻게 Xcode에서 동작하는지 체크하는 것만 남았다.

첫 Xcode Source Editor Extension을 완성한 것을 축하한다.

요약
툴을 사용하는 것은 모든 소프트웨어 개발자에게 매우 중요한 영역이다. 우리 IDE 프로바이더는 항상 우리가 원하는 것을 제공해주는 것이 아니므로 때론 우리 손으로 직접 그 문제를 해결해야한다. 이런 맥락에서 Xcode Extension은 활용하기 좋은 기능이다.

애플이 이번에 처음으로 발표한 시점이긴 하지만, 우리 개발자에게 AST와 파일 시스템에 접근할 수 있게 해주는 순간 어마어마한 가능성을 가지게 될 것이다. 여기 Github에서 예제코드를 확인해 볼 수 있다.

사용된 모든 이미지는 CC0 1.0 Universal (CC0 1.0)로 쓰였다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

요즘 많은 사람들이 그들의 다음 그들의 다음 모바일 앱 개발에 사용할 플랫폼으로 리액트 네이티브에 다가가고 있다. 이것은 사소한 일이 아니다. 당신의 소프트웨어 개발 플랫폼을 바꾸는 것은 높은 초기 설정 비용을 수반하여 매일 프로그래밍 작업 플로우에 깊숙히 영향을 줄 것이다. 또한 지금까지 만들어놨던 모든 요소들을 전환하는 것도 가장 비싼 비용의 결정 중 하나이다.

아마 더 중요한 것은, 당신의 소프트웨어 개발 플랫폼 또한 소프트웨어 엔지니어로서 당신을 이루는 것 중 하나이다. 소프트웨어 개발 플랫폼은 한 언어만 사용하게하고, 어느 특정 아키텍처를 우선시하며, 특정 툴을 요구하고, 당신을 그 모든 생태계와 플랫폼의 개발자 커뮤니티와 결혼시켜버리도록 지향(혹은 강요)한다.

페이스북은 당신이 전환해오기를 바란다.

그리고 리액트 네이티브 팀의 꾸준하며, 공들인 노력은 그 야망과 같다. 그들은 우리의 전통적인 Xcode/Swift/Objective-C 스택을 대체할 수 있는지 고려해야하는, 너무 근본의 소프트웨어 개발 플랫폼을 만들어왔다.

이것이 실용적인 변화일 수 있을까? 내가 읽어온 리액트 네이티브에 관련된 블로그 포스팅들은 오히려 피상적인 접근을 알려주었다. 그것의 장점과 단점에대한 깊이있는 접근은 없고, 여러분이 누군가로부터 소프트웨어 개발 플랫폼을 바꿔라고 설득당한 사람이라고 가정하고 이야기를 시작한다.

지난 몇달동안 리액트 네이티브를 사용하면서, 나는 이것으로 개발할 수도 없고 이것을 사용하기를 추천하지도 않을 플랫폼인 것으로 결론이 나왔다. 이 글의 목표는 Swift 개발에서 리액트 네이티브로 전환하는 것에 대한 빈틈없는 평가의 찬반을 알려주는 것이다. 그리고 그 전환에대해 반대하는 이야기도 할 것이다.

찬성
선언 스타일(Declarative style)
리액트 네이티브와함께 작업할때 기뻤던 한가지는 UI 프로그래밍의 선언 스타일이었다. 리액트 방법으로 하면 UI는 함수의 상태와 프로퍼티들이다. 반면 Cocoa Touch에서는 UI를 명령적으로 짜야한다.

아래 예제가 내가 말하는 바를 설명해 줄 것이다. 화면 왼쪽 상단에 작은 사각형을 넣고 싶다고 가정하자. 이 작은 사각형은 사용자가 연결되있을 때는 빨강, 연결되지 않을때는 초록을 표시한다.

아래는 우리가 일반적으로 iOS에서 어떻게 했는지 보여준다.

명령형 스타일에서는 UI를 갱신하기 위해 이 모든 단계를 명시해주어야한다. isConnected가 바뀌었는지 듣고 있어야 하고, 뷰에 일치하게 갱신해야한다. 우리는 iOS에게 어떻게 상태를 컴퓨트할지 말해줘야한다.

이제 리액트의 선언 방법과 비교해보아라.

간단하게 어떻게 여러분의 앱이 주어진 점을 보여주는지 표현했고, 리액트는 데이터 갱신때마다 자동으로 모든 UI 갱신을 관리해줄 것이다.

리액트의 선언 스타일은 뷰의 render() 메소드로 작용하여 여러분의 UI를 묘사하개 해준다. 리액트 프레임워크는 상태의 모든 갱신을 다시 랜더링하도록 해준다. 여러분의 데이터 갱신(backgroundColor의 갱신이 일어날때)은 자동으로 UI 갱신으로 이어진다.

이렇게하면 모델의 갱신에대한 응답에서 손수 뷰를 갱신하지 않앋 되게 해준다. 여러분이 이 책임으로부터 벗어나고 싶든 아니든, 리액트는 당신의 설명에따라 갱신 관리를 보장해주는 점은 매우 좋은 점이며, 여러분은 더이상 isConnected 변수에대한 프로퍼티를 관리할 필요가 없다. 당신이 갱신한 모든 것은 상태이다.

또한 UI 요소들이 결국 클래스의 객체가 아니라 함수인것처럼 생각하게 될 것이다. 그것들은 상태를 받아서 UIKit 오브젝트로 랜더링한다. Component의 역할은, 요청이 들어왔을때 예상되는 상태에서의 그 자신을 반환하는 것이다.

UI에 대해서는 유용한 방법이라 생각한다. 그리고 MVC로부터 좋은 발전이다. 뷰는 단지 그 자체를 보여주는 역할만 하고 데이터를 관리하지는 않으며, UIViewController에서 시작하기를 좋아하고, 뷰와 컨트롤러 사이 관계에 밀접하게 붙어있는 장님으로 바뀐다.

빠른 반복
리액트 네이티브의 매력은 단지 지적임이 아니다. 프레임워크는 실용적이기까지 할 것이다.

여러분이 리액트 네이티브에서 프로그래밍을 하면, 프레임워크는 당신이 작업하고 있는 자바스크립트 코드를 서브하는 로컬 서버를 만들어 줄 것이다. 앱을 만들면 iOS 시뮬레이터나 기기에서 돌려보고, 리택트 네이티브는 자바스크립트로 만든 모든 변화를 앱에 반영하게 해준다.

여러분에게 두가지 옵션이 있다.
  • 실시간 리로딩(Live reloading)은 당신이 그 파일에 수정하고 저장할 때마다 앱을 리로드할 것이다. 기본적으로 시뮬레이터로 전환하는 것을 막고 ⌘ + R을 누르지 않아도 된다.
  • 핫 리로딩(Hot reloading)은 앱 전체를 리로드하지 않고 당신이 수정한 부분만 리로드한다. 당신이 네비게이션 스택 깊숙히 테이블 뷰 셀의 UI를 작업하고 있었다면, 그 바뀐점을 보기위해 시작화면에서 셀까지 찾아가지 않아도 된다. 그리고 컴포넌트는 이미 그 안에 있던 상태로 그대로 있을 것이다. 이것은 WYSIWYG 프로그래밍 경험(WYSIWYG programming experience)이다. 앱을 위해 Xcode가 결코 제공하지 않았던 호화로운 기능이다.

나는 앱을 시작에서부터 보여주고 싶은 ViewController마다마다 AppDelegate에 디버그 메소드를 넣었던 것을 기억한다. 그리하여 앱을 켤때마다 손수 그 페이지를 찾아가고 싶지 않아서 코드로 그 페이지를 찾아가게 했었다. 핫 리로딩은 나를 동굴에서 나온 사람의 기분으로 만들어 버렸다.

리액트 네이티브의 피드백 루프 주기는 넋이 나갈 정도로 낮다. 앱에서 파일을 저장하고 그것을 눈으로 보기까지 1~2초정도 걸린다. 이것은 Xcode에서 일반적으로 우리가 사용한 빌드&런보다 10배 작게 든다.

이것은 방송처럼 코드 갱신을 가능하게 해준다. 자바스크립트 코드에서의 어떤 변화라도 앱이 제품으로 출시되어 있는동안 사용자에게 즉시 적용될 수 있다.

크로스 플랫폼
품위없고 서튼 이야기를 기억한다.

"훌륭한 앱이군요! 안드로이드에서도 동작하나요? [...]오, 안드로이드 버전은 언제 배포할 계획이시죠?[...], 오."

이제 코드베이스가 수백만 개의 추가 장치에서 실행될 수있는 앱을 만들 수 있으며 아웃 리치를 몇 배 증가시킬 수 있다.

그리고 여러 플랫폼에서 동일한 코드가 더욱 확실하게 같은 모습과 같은 동작의 앱을 만들 것이라 확신한다.

같은 코드베이스로 여러분 앱의 안드로이드 것을 만들 수 있고, 그래픽적으로든 기능적으로든 iOS쪽과 동등해지게 한다.

크로스 플랫폼 프레임워크의 명백한 장점은 언제나 시장 개발, 통합 코드베이스 및 앱의 코드베이스를 유지하는데 필요한 통합 기술 세트이다.

반대
불확실한 로드맵
리액트 네이티브를 사용할 때 생기는 중요한 걱정은 그 프로젝트에서의 장기적인 보장이 없다는 것이다. 

만약 이 프로젝트가 네트워크 라이브러리나 CGPath를 그리는 SVG같은 부가적으로 합쳐서 쓸 수 있는 요소라면, 장기적인 지원이 두번째 걱정으로 밀려날것이다. 리액트 네이티브 개발자들이 그만둬버리거나 리액트 네이티브 개발 속도가 마음에 들지 않으면, 대충 비슷한 라이브러리로 대체하거나 아니면 내 스스로 고쳐 쓸수도 있었을 것이다. 이것은 큰 작업이 될지도 모르겠지만, 이러나저러나 거대한 일은 아니다. 한 라이브러리를 쓸 수 없게 되었다해서 프로젝트 전체가 붕괴되지 않음을 보장하기 위해선 모든 나의 써드파티 라이브러리/CocoaPods을 내 프로젝트에서 충분히 떼어놓으면 된다.

그러나 리액트 네이티브는 CocoaPod처럼 부가적으로 쓰는 요소가 아니며, 단지 SDK도 아니고, 단지 라이브러리도 아니다. 이것은 완전히 소프트웨어 개발 플랫폼이다. 내 앱이 이것에 "아주 붙어있다"고 말하는 것은 너무 조심스러운 표현이고, 애 앱이 그것에 완전히 의존적이라고 말해야한다. 만약 페이스북이 리액트 네이티브 유지보수를 중단하면, 그것을 쓰고 있던 내 앱은 썩어갈 것이고, 내가 할 수 있는 "리액트 네이티브 대체물"은 없을 것이다. 만약 그것을 내 스스로 개발하려 한다면 리액트 네이티브 소스에도 친숙해야할 뿐 아니라, React.js 코드베이스, 네이티브 CLI 툴, JavaScriptCore에도 익숙해져야 할것이다. 커뮤니티가 이 프로젝트의 생존을 보장해줄까? 아마-만약 그렇게 되버리면 우리가 사용할 수 있을 정도의 개발 속도가 나지 않을 것이다.

깃헙 저장소에서는 2주에 한번 정도 건강한 속도로 리액트 네이티브 릴리즈가 되고있다. 두개의 분리된 것과 복잡한 소프트웨어 개발 플랫폼을 타겟으로 하는 소프트웨어 개발 플랫폼에는 나쁘지 않다. 페이스북이 오늘 약속할지도 모르지만, 지속적인 네이티브를 유지하기위한 장기간의 약속을하지 않았다. 회사에서는 이 프로젝트에서 손을 떼지 않을 거라는 어떠한 보장도 하지 않고 있다. 가까운 미래뿐만 아니라 여러분의 앱이 살아있는 동안에 말이다. 다른 말로는, 현재 시점에서 리액트 네이티브가 iOS11이나 iOS12에 호환될거라는 보장은 어디에도 없다는 것이다.
여기서 다음년도로!
2016년 4월 이후 공식적인 리액트 네이티브 블로그 포스팅은 종료되었다. 그 이후엔 어떻게 되었을까? 그것에대해 페이스북은 걱정없이 침묵만 지키고 있다.
우리는 곧 계획을 발표할 것이다-Konstantin
2015년 12월에 리액티브 팀이 말한 것이다. 이 계획이란것은 아직도 발표되지 않고 있다. 확실히 리액트 네이티브의 페이스북 장기간 비용 분석 결과는 당신이 생각한 만큼 멋지게 보이지 않는다.

다시 말하면, 우리는 개별의 CocoaPod에대해 이야기하는게 아니라, 여러분의 코드가 실행될 수 있는 유일한 플랫폼에대해 이야기하고 있는 중이다. 굉장히 장기적인 시각으로 다뤄야할 중요한 문제이다.

명백히 위압하는(Patently daunting)
리액트 네이티브는 복제를 허용하는 BSD-스타일 라이센스와함께 페이스북의 추가적인 특허권 문서(Additional Grant of Potent Right)를 만들었다.(버전2) 이 파일을 넣은 페이스북의 입장은 분명하지 않고, 이 파일 자체도 불분명하다.

양극의 문서이다. 문서가 끝나자마자 리액트 네이티브를 사용하기 위해서는 "perpetual(영속의), worldwide(널리), royalty-free(무료저작권), non-exclusive(독점불가능한), irrevocable(변경불가능한)" 라이센스를 따라야한다고 한다. 아래 조항들을 제공받으면서 말이다.

The license granted hereunder will terminate, automatically and without notice, if you (or any of your subsidiaries, corporate affiliates or agents) initiate directly or indirectly, or take a direct financial interest in, any Patent Assertion: (i) against Facebook or any of its subsidiaries or corporate affiliates, (ii) against any party if such Patent Assertion arises in whole or in part from any software, technology, product or service of Facebook or any of its subsidiaries or corporate affiliates, or (iii) against any party relating to the Software.

나는 변호사에게 이것을 명확하게 해달라고 직접 물어보았다(여러분이라도 그렇게 할 수 있었을 것이다). 그 변호사 말에 따르면 이 조항은 다음으로 요약된다. 내가 만약 페이스북에게 특허권 위배를 주장하며 소송을 걸면 리액트 네이티브를 사용하는 라이센스가 즉시 사라질 수 있다.(아래 Appendix에서 그의 더 세부적인 설명을 인용했다)

실제로는 특허권으로 페이스북으로 고소하지 못하게 하는 것처럼 보였다. 이런 의미에서, 이것은 페이스북이 내 특허권을 침해할 수 있는 기회를 주는 것처럼 보였다.("당신이 여기에있는 훌륭한 양자 물리학 기술은 당신의 앱에 나쁜 일이 생길 경우 부끄러운 일이 될 것입니다.")

위 설명이 비판적이라고 생각할지도 모르겠다. 그러나 소프트웨어 라이센스와 특허권을 평가할 때는 비판적으로 해야한다. 알고리즘이 최악의 시나리오에서 얼마나 잘 동작하는지 그 효율을 평가한다. 똑같은 이유로서 소프트웨어 라이센스와 특허권도 그렇게 평가해야한다고 생각한다.

리니어 서치(linear search) 알고리즘은 타겟값이 리스트의 첫번째로 일어나는 시나리오에서는 최적화된 동작을 할 것이다. 그 이유 하나로, 모든 시나리오에 리니어 서치 알고리즘을 사용하라고 나를 설득하기엔 만족스러운 이유가 아니다. 만약 타겟값이 리스트 마지막에 있을 수 있다면, 내 리니어 서치 알고리즘의 평균 퍼포먼스는 굉장히 나빠질 것이다.

비슷한 맥락에서, 페이스 북이 내일 일련의 IP를 침해하는 것이 가능하고 React Native를 사용하기위한 라이센스를 취소함으로써 내측으로부터의 보복을 처벌 할 수 있다면, 페이스북이 오늘 일련의 IP 침해를 하지 않았다는 사실은 나를 조금 안심시킨다.(원문: Likewise, the fact that Facebook happens not to be a serial IP infringer today reassures me little if it is possible for Facebook to become a serial IP infringer tomorrow and punish any retaliation from my side by revoking my license to use React Native.) 만약 내일 페이스북이 내 IP를 침해하고(그러나 그것이 내 소프트웨어와 관련은 없다) 그들에게 소송을 걸면,  페이스북에게 내 앱에서 리액트 네이티브를 빼내게 만들 기회를 주는 셈이 되버린다.

내 앱이 그 플랫폼에 의존적인 것 뿐만 아니라 영리한 내 프로퍼티들까지도 위험하게된다.

iOS 앱들은 완전히 애플의 재량으로 앱스토어에 들어간다. 나는 쉽지 않은 느낌을 두배로 느끼고 싶지 않다.

명백히 침묵하는(Patently silent)
위의 해석이 정확한가? 최악의 시나리오 가능성인가? 이런것들을 걱정할 만큼 좋은 이유가 있는가? 걱정하지 않아도 될 좋은 이유가 있는가?

수많은 깃헙 이슈와 포럼 포스팅에서, 그들의 법률 부서가 이런 조항 때문에 리액트나 리액트 네이티브를 쓰지 마라고 조언한다고 이야기해왔다. 볍률 부서가 없는 개발자들에게는 유감스럽게도 이 문제들이 모호하게 남아있다.

좀 더 걱정스럽게도, 페이스북은 이 문제를 더 명확하게 하기 위한 근본적인 노력을 해오지 않고 있다. 2015년 페이스북 오픈소스 블로그 포스팅에서는 이러한 "혼란"에대해 인정했고, 기꺼히 추가적인 특허권 문제에대해 명확하게 할것이라 알렸었다. 깃헙에서 그것과 연관된 5개의 이슈 이후에도 그 불분명함은 사라지지 않고있따.

여러 페이스북 개발자들이 이 이슈에대해 응답해오고 있는데, 그들이 적어놓은 것에는 안심될만한게 하나도 없으면서 그것을 명확하게 하는 동안에 개발자들을 안심시키려고 노력하고 있다. 그들 중 한명은 Reddit과 HackerNews에있는 이 이슈에대한 토론 링크를 주기도 했었다(도움이 되지 않을 뿐더러 오해시키기까지 했다).

이것은 해석학과 추측의 작업으로 바뀌어버렸다. 페이스북은 리액트 네이티브 라이센스를 취소 할 수 있을까? 그렇다면 어떤 조건에서?

자바스크립트
Swift에서 리액트 네이티브로 전환할때 생기는 심각한 부정적인 면은 기술적 역행이다. 여러분은 자바스크립트에 적응하고 사용해야하는데 이 언어는 다음과 같은 특징을 담고있다.
  • 기술적으로 불완전함
  • 언세이프
  • 늦은 발전
왜 그렇게 말하는지 보자.

여기 이후에 예제에 나오는 모든 자바스크립트 코드는 ES2016에 유효하다.

자바스크립트의 결점
내가 좋아하는 법퍼 스티커에서 하는 말이다.
안전은 사고가 없는 것이다(Safety is no accident)
처음 봤을땐, 이 말은 중의적이었다. 우리는 안전의 정의를 사고가 나지 않는 것과 여러 안전장치 제품의 제안으로 알고 있다.

자동차에 안전벨트나 에어백이 탑재된 여러 안전장치의 제품은 안전하다고 할 수 있는가?

물론 대답은 같은 조건에서 둘 다이다.

운전자는 수많은 안정장치가 탑재된 차를 더 선호할 수 있다. 운전하기 어렵게 만들지라도 막을 수 있는 사고는 최대한 줄일 것이다.

비슷한 의미에서, 프로그래밍 언어는 프로그래머 에러에 대비한 안전장치를 제공할 수 있다.

수많은 운전자가 생산성을 위해 안전벨트를 착용하지 않고 운전한다는 사실은 옳바른 주장이 아니다. 비슷하게, 수많은 자바스크립트 개발자들이 생산성을 위해 태생의 언세이프 언어를 쓴다는 것도 옳바른 주장이 아니다.

프로그래밍 언어에서 안전의 중요성은 iOS 개발 툴이 진화하고 있는것의 진가를 인정하는 것이기도 하다.

오토 레퍼런스 카운팅이 Objective-C에 처음 나타났을때, 여러분의 iOS 프로젝트에서 그 옵션을 끄고 작업했을 수도 있다. 왜 ARC를 끄는게 나쁜 생각이었을까? 그 이유는, 이제 컴파일러가 당신의 오브젝트 라이프타임 계산을 할 수 있게 되었는데, 당신이 계산하는 것보다 더 빠르게 해준다. "컴파일러가 당신보다 더 똑똑해졌다"는 진언이고, 레퍼런스 카운팅에 관해서는 확실해졌다. 결과적으로 EXC_BAD_ACCESS 런타임 크레쉬를 줄였다는것을 알게되었을때 얼마나 만족했었는지 기억한다.

Obejctive-C는 한 변수 타입을 id로 설정할 수 있는데, 이것은 "어떤 것이든 모든 타입"의 의미이다. 그러나 컴파일러가 막을 수 있는 크레쉬임에도, 이러한 습관때문에 런타임 크레쉬가 일어날 수 있다. 컴파일러가 해결할 수 있는 문제라면, 컴파일러가 해결하게 놔두고 당신은 다른 문제를 해결하러 가면 된다.

여러분은 unrecognized selector sent to instance 크레쉬를 기억할 것이다. 응답하지 않는 오브젝트에 메소드를 호출할 때 생기는 크레쉬이다. 타입에러. 내 버그에서 3번째이다.

당연하게도 Swift를 사용하고나서 나의 첫 반응은 "Objective-C에서 런타임 크레쉬를 막고 싶었던 누군가가 만들었구만"이었다.

Swift는 안전하다. String을 받길 예상한 함수에 Int값을 넣도록 컴파일러가 허용하지 않는다. 사실 컴파일러가 타입 추론을 할 수 없었다면, 명시적으로 그렇게 할 수 있었을 것이다. 

그러나 자바스크립트는 프로그래머 에러에 대비한 안전장치가 부족하며, 여러분의 루틴에서 런타임 크레쉬를 막아야하고, 프로그래머 에러를 막아야한다.

타입 에러
자바스크립트는 변수나 함수에서 파라미터의 타입을 정해주지 않는다.

어떤 변수라도 언제든 무엇이든 될 수 있다.

자바스크립트는 class, typeof, instanceof와같은 키워드로 타입과 클래스 개념이 있다고 믿게 만든다.

여기서 우리는 다음을 보자.
  • 자바스크립트에서의 "class", "type", "instance" 개념은 주요 프로그래밍 세계에서의 개념과 완전히 다르다.
  • 자바스크립트에서는 타입을 너무 신뢰할 수 없게 정의해서, 이것들이 유용한 용도를 제공하지 못한다.

이  unrecognized selector sent to instance 크레쉬가 기억나는가? 이것이 이제 여러분 곁을 떠날것이라 생각하는가? 아래에는 리액트 네이티브의 것들이 있다.


옵셔널 결핍
Objective-C 코드(혹은 다른 수많은 오래된 언어)에서 엄청나게 많은 양의 버그들은 프로그래머가 부주의하게 nil에다가 메소드를 호출해서 생긴다.

리액트 네이티브와 자바스크립트 세계에서는 아래 에러를 종종 볼 수 있다.

그리고 이론적으로나 실무적으로나 막을 수 있다.

Swift는 옵셔널을 만들어냄으로서 이 문제를 해결했는데(옵셔널이란 nil일 수도 있고 값이 들어있을 수도 있는 타입이다), 이것을 사용할 때는 여러분에게 강제로 nil 체크를 거쳐서 사용하게 만든다.

함수 기호(function signature)의 결핍
자바스크립트에서는 함수가 리턴타입을 가지고 있지 않으며, 이 함수가 어떤 타입을 반환할지 모르거나 함수가 어떤것이든 반환할 수 있다.

좀 더 재미있게 만들어보자. 자바스크립트에서는 어떤 식이든 어떤 함수에의해 어떤 시간에든 계산될 수 있다. 자바스크립트 표준 라이브러리에 있는 map과 parseInt 함수를 사용한 예제를 생각해보자. map은 아마 Swift의 map과 동일한 것이고, parseInt는 문자열을 숫자로 파싱해주는 함수이다.
parseInt 함수는 2개의 파라미터(val, radix)를 받는데 반해, map은 3개의 파라미터(currentValue, index, array)를 보내기 때문에 이런 엉망의 결과가 생긴다. 이런것들이 여전히 합법적인 자바스크립트이다(일부 사람들이 함수형 프로그래밍 언어에 유용할것이라고 생각하는 언어).

불변성
자바스크립트가 지원하는 불변성은 매우 안 좋다.

const 연산자가 있는데, 이것은 기본타입(primitive)이 바뀌지 않는 것을 보장하는데 도움을 준다. 그러나 기본타입이 아닌 나머지 모든 것은 젤리처럼 유연하다.

유일한 복사? 아니다. 어떤 유일한 객체라도 언제든지 여러분의 앱에의해 수정될 수 있다. 멀티스레딩에 행운을 빈다. 정말로.

앱 개발을 힘들게하는 많은 것들은 가변을 쫓고 상태를 유지하는 것이다.

페이스북의 Immutable.js 문서에서 말한 내용이다(자바스크립트에서 불변 데이터 구조를 만들기위해 설계된 프레임워크이다).

그러나 아래는 리액트에서 어떻게 불변성을 만드는지이다.

이 컴포넌트에 넣는 인풋을 props라 부르는데, "properties"의 준말이다. 그들은 JSX 문법으로 속성들을 보낸다. 여러분은 이것을 컴포넌트에 불변할 것이라 생각할 수 있는데, 이것은 절때 this.props에 덮어 씌울 수 없다.

여러분은 상태를 바꾸지 말자고 공손히 물어본다. 그렇다 위 글은 리액트 네이티브 문서에서 가져온 것이다.

배열을 믿을 수 없다.
여러분은 배열이 "행과 열로 된, 비슷한 객체들의 질서정연한 배치"라 생각하는가? 아래를 보고 다시한번 생각해보아라.
자바스크립트에서 배열은 우리가 원래 배열이라 부르는 그것보다는, 보통의 자바스크립트 객체에 더 가깝다. 정확한 수서성의 결여와 가변성은 잘 동작하기 힘들게 만든다.

에러 핸들링하기 힘들다
자바스크립트에서는 경고 없이 런타임이나 예외를 던지는 함수를 만들 수 있다.
예외로 당신이 원하는 무엇(문자열, Date, 함수 등)이든 던질 수 있다. 팀원의 코드를 잠제적으로 크레쉬할 수 있다는 표시를 함수에 한다던가, 예외의 과정이 어떻게 되는지 명시할 메커니즘이 따로 없다. 문서에서는 대신에 if문을 사용하라고 권장한다.

당신의 예상하지 못한 예외 처리를 다루기 위해서는 마지막 라인의 방어로 예외를 남겨놓는게 최고이다. 그리고 예상되는 에러를 컨트롤 플로우 문으로 관리하기에도 최고이다.

디시멀(Decimal)을 지원하지 않는다.
하드웨어에서 대부분의 소수 자릿수들은 이진수로 정확하게 표현되지 않으며, 많은 프로그래밍 언어(자바스크립트와 Swift를 포함한)는 종종 수학적으로 잘못된 소수 계산을 내놓을 것이다.
이것이 왜 다른 언어의 표준 라이브러리가 소수 자릿수를 지원하는지 이유이다(예를들어 Swift에서는 Decimal을 사용할 수 있다). 자바스크립트에서는 서드파티 라이브러리나 여러분이 직접 만든 코드에 의지해야한다.

믿지 못하는 수학
초등학교 산수에도 주의를 기울여야함을 기억해라. 자바스크립트는 0과의 관계가 복잡하며 이것은 숫자가 아니다.

언세이프한 초기화
자바스크립트는 속성을 초기화 할 필요가 없으므로 객체를 만든 후 모순된 상태로 둘 수 있다.

if 다음에 선택적으로 가능한 커리 중괄호(curly brace)
if문 뒤에 커리 중괄호는 선택적이다.
여러분의 컨트롤 플로우 문에 모험의 취향을 추가한다.

모호한 커리 중괄호
프로그래머의 의도가 정확하게 추론된 것이 아니라면, 이 언어는 커리 중괄호를 선택적으로 할 수 있게 놔두지 않는다.

fallthrough 전환
switch  절에서 break를 깜빡했다면, 아래로 쭉쭉 떨어질 것이다. 또한 Swift에서는 케이스 철저성을 확인하지 않아도 된다.

어떤것이 '무(없음)'일까? (What's nothing)
 모든 의도나 목적에서 null이 아닌 것이 무가 될 수 있다. 도움이 되지 않는 구별이다.

그리고 변수에 어떤 데이터가 담겨있는지 아닌지 알고 싶다면 어떨까? 음, 확인이 너무 거추장스럽게 null과 undefined 둘 다 체크해야한다.


빈약한 표현력
  • 열거형이 없다. 연관타입을 가진 열거형은 말할것도 없다. 신뢰성 높은 상태 표현에 행운을 빌 수 있을까.
  • guard문이 없다.
  • 제네릭이 없다.
  • 컨트롤 플로우문의 표현력을 늘리기위한 where이 없다.

지극히 느린 진화
ES2016 때 자바스크립트에 새로 추가된 기능을 보라.
1. 배열을 위한 includes 메소드
배열이 특정 값을 가지고 있는지 확인한다. 아래에 어떻게 사용하는지 나와있다.
이건 사용하지 말자.
2. **연산자
지수 연산에 사용하기위한, a**b는 Math.pow(a, b)의 축약이다.

한번 생각해보면, 파이썬의 **연산자는 파이선1에 있었고, 루비의 **연산자는 20년도 전에 있었던 연산자이다.

따라서 자바스크립트는 이 기본적인 산수 연산자와 배열을 위한 꽤 제한된 맴버쉽 체크 메소드를 추가하는데 20년이나 걸렸고, 이 특징이 한 해중에 가장 의미있는 것이었다.

Flow로 구조받자!(Flow to the rescue!)
Flow는 위의 수많은 불평으로부터 내놓은 페이스북의 대답이다. 이것은 자바스크립트를 위한 정적 타입 체커이고, 여러분 코드에서 변수의 타입을 추론하고 추적할 수 있는 능력이 있으며, 여러분에게 일어날 운명(옮긴이: 실행되었을 때 일어날 오류)을 경고한다.

위에서 보았던 함수 기호의 결핍으로부터 나온 문제의 예제를 다시 보자.(number가 들어오길 예상한 divideByFour 함수는 문자열을 받는다) 여기엔 Flow가 이것을 어떻게 다루는지 보여준다.

함수 기호 부재 때문에 생기는 많은 문제를 고쳐준다.

제네릭 배열에도 동일하게 할 수 있다.

Flow는 nullability를 잘 다룰 수 있게해주고, 값이 null이되면 안되는데 null일 수 있을때 여러분에게 경고를 띄워 줄 것이다.

만약 j가 string으로 가정하지만, 언제든 null이 될 수 있으면, Flow는 그 사실을 우리에게 적절하게 알려주려 할 것이다. 따라서 인자로 string을 받되 null이면 안되는 곳에서 인자를 넘겨주기 전에 굳이 null 체크를 할 필요가 없다면 불만을 가질 수 있을 것이다.

Flow의 기능들은 타입체킹과 주석을 달아주는 것을 넘어, 새로운 구성체(new construct)를 지원한다. 그 중 하나는 리터럴 타입인데, 우리가 만드는데 사용할 수 있으며, 그 예로 열거형이 있다.

그리고 Flow는 direction이 "North"도 아니고 "South"도 아닌 것을 반환하려하면 뭔가 불평하고 있을 것이다.

내가 찾은 또다른 유용한 구성체는 유니온(Union) 타입이다. 이것은 딱 하나의 값으로 제한하고, 미리 정의해놓은 타입들 중에서 하나로 제한한다. 아래 예제는 Flow 문서에서 온 것이다.

Flow은 도움이되는한 꽤 도움이 되는 리액트 네이티브와 훌륭한 동반자이다. Flow 문서에서는 구성 요소의 유형에 올바르게 주석을 추가 할 때 어떤식으로 동작하는지 보여주는 좋은 예시를 제공한다.

저 주석은 Flow가 어디서 경고를 내는지 알려주며, 여러분 계약에 만족스럽지 않다고 충분히 꾸짖는다.

Flow는 전적으로 강력한 도구이다. 여기에는 여러분이 해볼 수 있는 유용한 커멘드라인 인터페이스 예제가 있다.
  • suggest는 주어진 파일에대해 타입 주석 제안을 보여준다(suggest Shows type annotation suggestions for given files)
  • type-at-pos는 주어진 파일과 위치에 타입을 보여준다(type-at-pos Shows the type at a given file and position)
  • get-def는 변수나 프로퍼티의 선언 위치를 받는다(get-def Gets the definition location of a variable or property)

Flow는 flossing와 비슷하다
이제 자바스크립트가 고쳐졌을까? 아니다.

Flow가 했을 공학의 노력에 감명받았고, 이것은 자바스크립트의 부모집합(superset)으로 남았으며, 따라서 당신을 태생적으로 약한 파운데이션에서 만들게 한다. 자바스크립트의 결점을 치료하는 어느 종류로서 Flow가 빗발치는 것은 (Monty Python and the Holy Grail으로부터) Swamp Castle and its King을 떠올리게 했다.

내가 여기 처음 왔을때, 여기는 전부 늪이였어. 여기 늪에 성을 세울거라는 말에 모두가 나를 얼간이라 했지만, 똑같이 만들어서 그냥 보여주었지. 그것이 늪으로 가라앉았고, 나는 두번째 것을 지었어. 그것도 늪으로 가라앉았고 세번째것을 또 지었어. 그것은 불에타고 쓰러져서 늪에 가라앉았어. 그러나 네번째는 버티고 있어. 그리고 이게 너가 얻어야 할 것이고, 영국에서 가장 강한 성을 얻어냈지.

안전하지 않은 파운데이션을 만들수 있을거라는 말이 파운데이션을 더 안전하게 만들진 않고, 더 효율적인 과정이지도 않을 것이다. 그리고 이 행동을 저지하려는 주장은 그 불합리함을 놓치게 만든다.

아마 더 의미있게, 자바스크립트 부모집합, 린터(linters), 정적 분석기는 당신이 더 안전한 언어를 고를 수 없는 플랫폼을 다룰때 완화시키는 방법으로서 일시적으로 억제해줄 것이다. 그게 된다면, 그것들을 사용하면서 도움을 구하지 않아도 될 것이다.

이런 일시적인 조치에는 또다른 근본적인 문제가 있다. 이것이 실제로 그 권위자가 쓰지도 않고 커뮤니티에서도 존경받지 못하면 법같은 이 안전장치는 아주 조금만 의미있게 된다.

Flow는 당신이 코드바운드로 리액트 네이티브 앱을 만들고 실행시키는 것부터 런타임 크레쉬를 생성하는 것까지 당신의 작업을 멈추게 하지는 않는다. 그리고 그것은 프로그래밍 언어에대한 기본적인 안전 요구사항이다. 만약 에러를 막을 수 있으면, 그 언어는 능동적으로 막으려고 할 수 있고, 안전하지 않은 코드를 짜고 실행시키는 것을 디폴트에의해 방해할 것이다(그 후에 하는게 아닌).

리턴값이 모호한 함수를 짜라고 팀에게 말할 수는 없을 것이고, 그것에 응답하지 않는 객체에 메소드를 호출하라고 나에게 말할 수도 없으며, 컴포넌트에 proptypes을 정의하지 않고 proptypes이 정의되있다는 코드 리뷰를 하는 동안 나에게 손수 끄집어내라고 할 수 없을 것이다.

유닛 테스트와 flossing과같은 Flow는 유익하고 선택적이고 지루한것의 저주를 낳는다. It’s in your next year’s resolutions.

그리고 여기에 현실이 있다. 깃헙에 공개된 .js 파일에 Flow를 사용하는 파일(즉, Flow가 체크할 수 있는 @flow가 포함되있는 파일)이 몇개나 될까? 어림잡아 80,000,000개의 .js파일에서 1,400,000개정도이다. 안전한 자바스크립트를 짜기 위해 2%보다 작은 곳에서 이 툴을 사용하고 있었다.

여기에 관련된 노트인, awesome-react-native에서 100개가 넘는 리액트 네이티브 저장소들이 있는데, 여기서 Flow를 제대로 사용하고 타입 주석을 사용한 저장소는 한개도 발견할 수 없었다. 오직 리액트 네이티브 튜토리얼에만 Flow를 사용하고 있었다.

자바스크립트 생태계: 속박과 굴레
자바스크립트 개발자를 제외한 나머지 모두는 자바스크립트의 결핍에대한 깊은 인상을 받은것 같다. 위에서 내가 설명한 자바스크립트는 흉측한 사마귀가 아니라는 사람들에게는, they’re “quirks” or “gotchas” that you, not your language, have to be on the lookout for.

왜냐하면 자바스크립트 개발자들은 자바스크립트가 불충분하다고 믿지 않기 때문이다.

언어에서 불변성을 지원하지 않는다? 그럼 만들면 된다. 언어에서 typing을 지원하지 않는다? 그럼 만들면 된다. 언어에서 decimal을 지원하지 않는다? 그럼 만들면 된다. 언어에서 안전한 함수형 프로그래밍 언어를 허용하지 않는다? 그럼 만들면 된다. 언어에서 nullability를 지원하지 않는다? 그럼 만들면 된다.

혹은... 당신이 이 기능들이 근본적으로 중요하다고 인정하면, 여러분은.. 바깥세상에서의 그것을 지원하는 언어로 전환해버리면 될까?(잘 모르겠다. 그냥 생각난것을 말한거다)

내 생각엔, 자바스크립트가 태생적으로 결핍하다고하여 그것을 계속 갈고 닦는게 아니라 다른것으로 대체하려 한다면 그것은 일반화된 고집의 거부(generalized obstinate refusal)이다. 갑자기 증가한 개선이나 버팀목의 결과는 살아있는 생태계의 신호로 보이나, 실제로 이것이 의미하는 것은 이 언어가 근본적으로 중요한 기능의 결핍이 있다는 것이다.

아래 그림은 이러한 상황을 만화로 잘 표현했다.

삽질으로부터의 자유는 당신이 필요한 것이 내장된 언어를 선택하는 것에 달렸다. 이러한 삽질은 에너지 낭비이다. 자바스크립트는 다음의 이유로 좋은 소프트웨어를 제작하는데 도움이 되지 않게 하고 있다: 자바스크립트는 우선 당신이 개발하게 만들고, 다른 언어가 제공하는 것에 의존적이게 만든다.

사슬(Chains)
자바스크립트는 튀긴 커다란 생선을 가지고 있다. 이 언어는 다양한 버전의 인터넷 브라우저의 몇십억 사용자 입맛에 맞춰주어야한다. 이것이 언어의 발전을 더디게 만든다.

typeof(null)==='object'를 기억하는가? 음, 예전에 type of null을 null로 바꾸자고 제안했었다. 그러나

이렇게하면 현존하는 수많은 사이트가 망가지게 될 것이다. 자바스크립트의 스피릿에 적합하지 않다.

그리고는 이 제안이 거절되었다. ES06에의해 null은 여전히 object이다.

자바스크립트의 발전 과정은 필요에의해 입맛에만 맞추고 있다.
  • 수많은 구식 버전의 브라우저 사용자들
  • 각색의 브라우저 벤더 집합
  • 수십억의 사이트와 그 사이트의 각 개발자들
이것들은 훌륭한 민주주의이긴하나 한편으로는 다른 언어에비해 굉장히 더디게 발전한다. 그리고 이것은 이식성이 좋은 만큼 개발자의 인간환경공학은 좋지 않을 것이다.

넓은 시각(Wider angles)
역사적인 관점에서, 한 언어가 인기를 얻고나서, 그것을 넘겨받고싶은 점유자들이 거부하게되는 패턴은 겉으로보기에 불충분한 언어에게 나타나는 익숙한 패턴이다.

여기 따뜻한 모닥불 옆에 앉아서 내 얘기를 좀 들어봐라.

1994년으로 돌아가보자. Richard Stallman은 "왜 Tcl을 쓰면 안되는가"라는 다소 모호한 타이틀로 comp.lang.tcl 뉴스그룹(news group)에 글을 하나 썼다. Stallman은 프로그래밍 언어로서 Tcl의 단점을 심술궂게 꾸짖었고, 목적에 맞지 않는 Tcl을 고발했다. 자바스크립트에대해 내 불평과 유사하게 매우 장관이었다.

Emacs의 주요 수업에선 확장을 위한 언어는 단지 "확장 언어"가 될 수 없다고 한다. 실제 프로그래밍언어가 되야하고, 상당한 양의 프로그램을 짤 수 있어야하며, 그것을 유지보수할 수 있게 설계되야한다. 왜냐하면 사람들이 그것을 바라니까! [...] Tcl은 진지한 프로그래밍 언어로 설계되지 않았다. 이것은 "스크립트 언어"가 되기위해 설계되었으며, "스크립트 언어"라 가정하는 것은 실제 프로그래밍 언어가 되려고 할 필요가 없었음을 의미한다. 따라서 Tcl은 하나의 능력을 빼먹었는데, 배열이 부족하다; 링크드리스트를 만들 수 있는 구조체를 빼먹었다. 그것은 가짜로 넘버를 가지고 있는데, 매우 느리지만 동작은 한다. Tcl은 작은 프로그램을 만드는데 적합하나, 그 넘어를 하기엔 적합하지 않을 것이다.

이 글은 1994년에 Tcl 전쟁을 촉발시킨 글이다. 그가 들었던 대답들중에 가장 인상적이었던 John Ousterhout(Tcl을 만든 사람)의 대답이다.

언어 설계자로서 이 언어가 왜 선천적으로 더 낫고 더 나쁜지 우열을 가리는 토론을 사랑한다. 그러나 이런 건방진 주장에는 많은 문제가 있다. 궁극적으로 모든 언어의 이슈는 그 사용자가 원하는 것을 투표할 때 합의가 생긴다. 만약 Tcl이 그들이 사용하는 것보다 더 생산적이게 만들었다면(혹은 이미 그렇게 되었다), 다른 언어 사용자들이 이것이 더 낫다고 하면서 온다면, 그때 사람들은 언어를 바꾼다. 이것이 법칙이고 이게 맞다.

Outsterhout의 반격은 내가 활동하고 있던 많은 사람들 앞에서 이루어졌고, 그의 일격은 더 가까이서 볼 수 있었다.

(몇몇 독자들은 Ousterhout가 순회적으로 말했다는 것을 알아차릴 것이다. "왜 사람들이 언어를 바꿀까? 그게 더 낫기 때문이다. 왜 그것이 더 나을까? 사람들이 많이 쓰기 때문이다." 그의 주장은 더 많은 설명이 필요함에도 불구하고 조금도 하지 않았는데, 우리는 종종 특정 기술 스택(예를들어 여러분이 생각하고 있는 리액트 네이티브)에 여러 주장에서 이런 추런 라인을 보았었기 때문이다)

Ousterhout에 따르면, 프로그래밍 언어는 그 언어가 더 낫다고 말할 수 있는 두가지 특징을 가지고 있는데, 바로 선천적인 특징들(프로그래밍 패러다임, 문법, 표준 라이브러리. 이식성과같은 그 고유의 특성)과 후천적인 특징들(수많은 개발자들이 폭넓게 채택한것)이다. 만약 언어A가 언어B보다 후천적으로 낫다면, 선천적으로 언어B가 언어A보다 더 낫다고해도 별로 소용없어질것이다.

다른말로 하자면, 채택되는 것이야말로 기술 우선순위에서 최고봉이다.(언어 설계자가 어떤것을 우위로 정의했는지 상관없이 말이다)

Ousterhout 주장에 딱 맞아보이는 앨범 커버이다.

더 많은 팬을 가진 가수가 그를 더 낫게 만든다. 요점은 팬이나 히트 기록의 양에의한 인기이며, 이것이 가수를 평가하는 좋은 기준이다.

그러나 언어가 널리 채택되고 그것으로 만들어진 멋진 앱들을 자랑할 수 있다는 사실로는 그 메리트로 당신의 판단에 설득시킬 수 없을지도 모른다.

복잡해보이거나 보기에 멋진 앱이 기술X로만 만들었다는 의미는 기술X가 복잡해보이거나 보기에 멋진 앱을 만들기에만 최적화되있고, 보통 일에는 최적화되있지 않다는 뜻일 수도 있다.

전세계에서 열리는 모래성 대회는 당신도 만드는데 쓸 수 있는 모래와 물로 아름다운 구조물을 만들어서 보여준다. 이것은 모래가 좋은 장난감임을 주장할 수 있는 근거가 될 수는 있으나, 모래성 대회를 근거로하여 모래로 여러분의 집을 지을 수 있다고 납득하기는 어려울 것이다. 그러나 많은 이들이 납득되어 버렸다.

The King of Swamp Castle은 수많은 해커의 왕이다.

비슷하게, 인기는 프로그래밍 언어를 평가하기엔 신뢰할 수 없는 기준이다. 아주 기본적인 이유로, 인기는 벤더를 속박시키거나(브라우저에서만 돌아가는 프로그래밍 언어가 된다던지), 벤드웨이건 효과나, 레거시(legacy) 코드베이스의 결과를 매우 잘 만들지도 모른다.

프로그래밍 언어의 고유 특징들(프로그래머 에러에 대비한 안전장치, 표준 라이브러리, 이식성 등)은 여러분의 기준에 기반해야한다. 이것은 생산성 향상의 기본요소이다.

사실은 많은 것들이 부정적인 생산성을 이끌기도 한다. 그 예이다.
  • 피할 수 있는 에러를 만드는 당신을 막는 안전장치의 부족
  • 많은 목적에 맞지 않는 작은 표준 라이브러리
  • 당신의 의도를 명확하게 표현하지 않는 문법과 의미론(syntax and sementics)
  • 더딘 발전
  • 위의 모든 것들(자바스크립트)

의존성
리액트 네이티브는 총 648개의 의존성을 가지고 있다.

특별히 놀랄 것 없게도, 의존성 체인은 세상의 npm 중에 길수 있으므로, 리액트 네이티브의 패키지 매니저이다.(원문: Not particularly surprising, as dependency chains can be long in the world of npm, React Native’s package manager.)

이 광경은 오픈소스의 동료관계이다. 여러분의 앱은 600명 이상의 사람들의 지속된 노력으로 만들어졌다.

이게 함정이기도한데, 여러분은 648명의 자원 봉사자가 그 라이브러리를 유지해주어야 앱이 잘 돌아가며, 그 자원봉사자들로부터 어떤 약속 따윈 없다.

그들의 라이센스가 당신의 소프트웨어에 맞게 유지될까? 희망적으로?

그리고 그 구현이 모두 보안면에서 최고의 실천을 하고 있을까? 혹은 648개의 별개의 잠재적인 보안 리스크를 묵인할 수 있겠는가?

더 나은 제안들
크로스-플랫폼 개발 때문에 리액트 네이티브를 선택하게 되었다면, 다른 옵션도 생각해볼 필요가 있다.

리액트 네이티브는 지금 Xamarin과 Appcelerator라는 두개의 크로스 플랫폼 개발과 경쟁하고 있다.

Xamarin과 Appcelerator 둘 다 iOS, 안드로이드, 윈도우 폰을 지원한다. 그리고 둘 다 아래의 것들도 지원한다.
  • 더 종합적인 API
  • 더 성숙한 IDE
  • 더 나은 문서
  • 명확하고 친숙한 라이센싱
  • 동등한 퍼포먼스(더 낮진 않더라도)

Xamarin 개발은 C#으로 하는데, 이것은 자바스크립트에 비해서 버그를 줄여주는 경향이 있고, 더 표현력이 좋다(more expressive). 만약 자바스크립트를 쓰는 개발자라면 Appcelerator 개발이 자바스크립트로 할 수 있다.

리액트 네이티브와 비교하여 Xamarin과 Appcelerator 둘 다 멀리 보았을때 더 나은 가능성을 가지고 있다. Appcelerator(Titanium의 아버지)(3억  5천만 기기에 앱으로 실행 되었었다)는 2016년 1월에 인수되었고 Xamarin(포춘지 선정 500 대 기업에서 100 기업 이상 사용했다)는 마이크로소프트에의해 2016년 2월에 인수되었다. 둘 다 이전보다 8천만달러 이상의 주식형펀드가 올랐다.

나에게 지원해주기 위한 비즈니스의 회사가 빽으로 있다면, 프로젝트들은 이 크로스 플랫폼이라는 레드오션에서 신뢰할 수 있는 소프트웨어 개발 플랫폼으로 살아남고 성장해야한다(최근에 세어보니 10개 이상의 활발한 개발 프로젝트들이 있었다).

(의미있는 언급으로 Flutter(이하 플루터)에 대해 이야기 하겠다. 이것은 구글이 만든 크로스-플랫폼 개발 플랫폼이다(iOS, 안드로이드 둘다 지원한다). Dart라는 언어를 사용하는데, 이 언어는 자바스크립트보다 안전하고 표현력도 더 좋다. 또한 메트리얼 디자인 원칙을 따르게 도와주고, 네이티브하게 컴파일 된 코드를 만들어준다. Xamarin과 Appcelerator와는 다르게 오픈소스이며, 아직 제품으로 준비되지는 않았지만 그럼에도 불구하고 약속해놓았다.)

결론
좋은 소프트웨어 개발 플랫폼은 4가지 필수 특징을 가진다.
  • 이식성 - 한가지 이상의 플랫폼을 타겟으로 한다.
  • 생산성 - 성숙한 IDE와 다른 개발 툴, 문서, 그리고 표현력 있는 그 개발 언어
  • 안전 - 당신이 만들 수 있는 실수를 그 플랫폼이 얼마나 막을 수 있는지의 정도
  • 지속성(longevity) - 당신의 앱이 살아있는 동안 플랫폼이 얼마나 오래 가는지
비록 우리 산업이 아직까지는 이것을 가늠하기위한 확립된 오픈 표준이 없지만, 나는 내 경험과 조사를 바탕으로 내 평가를 공유하고 싶다.


리액트 네이티브의 강점을 이식성과 생산정의 면에서 보자면 너무 과하고, 안전, 장기간 프로젝트에는 불확실성, 위압적인 특허 라이센스에는 부족함이 있다.


Xamarin과 Appcelerator와같은 성숙한 플랫폼은 최고의 이식성(윈도우 폰을 지원한다), 성숙된 개발 툴, 프로그래밍 언어를 선택할 수 있게 해주는 것을 제공하는데, 언어 선택으로 생산성과 안전성을 얻어낼 수도 있다. 그리고 이것들이 잘 투자받은 회사의 핵심 제품이라는 사실이 장기간의 면에서 안심시켜준다.


Swift 개발은 안전성, 지속성, 생산성 면에서는 좋다. 이식성의 면은 상대적으로 낮으나 무시해도 되는 수준이다. 안드로이드 버전을 개발 할 수 있게 해주는건 아니지만 macOS 버전이나 백엔드 서버 앱을 만들 수 있게 한다.





여기까지가 내가 왜 리액트 네이티브 개발자가 되지 않는지의 그 이유였다.

감사
이 글의 초안에 도움을 준 Rami Chowdhury와 Alkis Papadakis에게 감사를 표한다. 그리고 난해한 IP 텍스트들을 해독하는데 도움을 준 Greg McMullen에게도 감사하다.

바뀐 이력
이 글이 바뀐 이력은 여기에서 확인할 수 있다.

커멘트
이 글에 대한 토론 에 팔로우하여 참여하고 싶으면 Hacker News, Hacker News(Again)/r/programming로 와라.

참조
 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

여러분 앱이 탭, 스와이프, 팬, 다른 인터렉션을 사용한다면 화면 뒤에서 이벤트를 사용하고 있는 것이다. 이러한 이벤트들은 잘 만들어진 과정을 거쳐 이동하며, 우리는 이것들이 어떻게 동작하는지 한번 볼 것이다. 텍스트 인풋이나 원격 제어 이벤트를 중심으로 트리키한 문제를 디버깅하면, 이것이 어떻게 동작하는지 쉽게 이해할 수 있다. 또한 이 지식을 이용하여 여러분 앱을 통해 커스텀 데이터 흐름을 만들 수도 있을 것이다.

이 글은 이벤트 전달에 관한 시리즈의 첫번째 글이다. 터치 핸들링에대해 다룰 것인데, 그냥 터치가 어덯게 여러분의 앱을 통해 받은 이벤트가 될 수 있는지, 그 이벤트를 다루기 위해 어떻게 요소들에게 기회를 줄 수 있는지 다룬다.

터치 핸들링
터치 이벤트는 iOS 앱에서 가장 주된 형태의 이벤트이다. 어떤식으로 터치를 처리하는지에대한 세부적인 내용은 우리가 매일 사용하는 API들에의해 가려져있다. 앱에서 이 이벤트들을 어떻게 분배하는지 이해하면 여러분의 앱을 어떻게 구성할지 정하는데 도움이 될 것이다. 또한 커스텀 이벤트를 전달하는데 이런 하부조직을 사용할 수도 있다.

Hit Testing
이벤트 전달에대해 이야기할 때 가장 먼저 다룰 것은 시스템이 어떻게 터치 이벤트를 다루고 어떻게 앱을 통해 이 이벤트를 받아드리는지이다. 이벤트 사용자가 화면을 탭하면 발생한다. 적절한 요소가 터치를 다루기 전에, 시스템은 어디에서 터치가 발생했는지, 누가 먼저 터치에 응답을 받을 것인지 정해야한다.

이것이 hit testing이 필요한 곳이다.

hit testing 과정과 관련이 있는 메소드에는 hitTest:withEvent:pointInside:withEvent:가 있다. hitTest:withEvent:는 hit test한 점이 그 바운드 안에 들어오는지 알아보기 위해 pointInside:withEvent:를 사용한다. 만약 바운드 안에 들어가지 않으면 nil을 반환하고 뷰 계층의 모든 브런치를 스킵한다.

테스트한 점이 바운드 안에 들어간다면 각 서브뷰마다 pointInside:withEvent:를 호출한다.  pointInside:withEvent:에서 YES를 받은 서브뷰는 hitTest:withEvent:를 호출한다. 최초의 hitTest:withEvent: 호출로부터 최종 결과는 서브뷰 중 하나이거나 자기 자신이다. 자기 자신을 반환하는 경우는 그 서브뷰가 모두 nil을 반환할 때이다.

아래의 뷰 계층을 보자.

사용자가 View E를 탭했다고 생각해보자. View A에서부터 hitTest:withEvent:로 시작한다. View A에서 pointInside:withEvent:YES면 View B나 View C의  pointInside:withEvent:를 호출한다. View B가 NO를 반환한다. View C가  YES를 반환하고 거기의 hitTest:withEvent:를 호출한다. View C도 View D와 View E로 똑같은 과정을 따른다. View D의 pointInside:withEvent:NO를 반환한다. View E는 YES를 반환하고 더이상 다른 서브뷰가 없으므로 hitTest:withEvent:에서 자기자신을 반환한다.

View D가 View C의 서브뷰라 가정하고, 사용자가 View C 바운드의 View D를 탭했다면 무슨일이 일어날까? (이것은 clipToBoundsNO로 설정되있을 때 가능하다) View A가 위 과정을 시작한다. View B와 View C 둘 다 pointInside:withEvent:NO를 반환하므로 결국 View A가 터치를 받는다.

이제 우리는 hitTest:withEvent:로부터 받은 뷰를 가지고 있다. 이 뷰는 hit-test 뷰이다. 이제 이것은 터치와 연관되있고 터치가 활성화되는 동안 터치 이벤트를 응답하는 어떤 제스처 이후에 첫번째 기회를 줄 것이다.

주어진 터치에 커스텀 구현이 따로 없으면 무슨일이 일어날까? 상황에 따라 다르다. 만약 뷰가 뷰컨트롤러에의해 관리되고 있으면 뷰컨트롤러에게 응답할 기회를 준다. 뷰컨트롤러가 응답하지 않으면 hit-test 뷰의 부모뷰가 그 응답 기회를 가져간다. 이러한 과정은 "Responder Chain"이라 불리는 방법으로서 반복에서 진행된다.

Responder Chain
Responder Chain은 chain-of-responsibility 디자인 패턴으로 구현되있다. Responder Chain의 각 요소는 UIResponder를 상속받는다. UIResponder는 여러 종류의 이벤트를 다루기 위한 메소드들을 가지고 있다. 터치 이벤트를 넘어서 UIResponder는 인풋 뷰, 모션 이벤트, 누름 이벤트, 원격 제어 이벤트를 다루기 위한 메소드들을 정의해 두었다.

여기에서 많은 이벤트 중 firstResponder가 중요하다. firstResponder는 이벤트를 다룰 수 있는 첫번째 기회가 주어진 오브젝트이다. 첫번째 응답자가 이벤트를 다루지 않으면 nextResponder에게 그 이벤트 핸들링 기회를 준다. 그리고 현재 오브젝트가 nil 일때까지 nextResponder를 반복한다. 보통 체인에서 마지막 오브젝트는 어플리케이션 델리게이트이다.

제스처
iOS에서 현재 있는 곳에 제스처 recognizer가 오기 전에, 터치 핸들링 프로세스는 어떻게 앱을 감지하고 제스처를 다룰지의 방법이다. UIGestureRecognizer가 나오면 그들이 감자한 제스처에 따라 터치를 다루고 행동을 취하도록 프로세스를 조금 바꾼다. 우리는 제스처 recognizer를 어떻게 사용하는지 살펴보지는 않겠지만 대신에 제스처를 어떻게 터치 이벤트 전달 시스템에 맞추는지 볼 것이다.

제스처는 항상 한 터치 이벤트를 다루기 위해 첫번째 기회를 얻는다. 디폴트로는, 제스처가 터치를 얻고 그 다음 뷰를 얻는다. 제스처 상태가 "recognized" 일때 차이점은 뷰에 있는 터치가 취소된다는 것이다. 여러분은 UIGestureRecognizer:, cancelsTouchesInView, delaysTouchesBegandelaysTouchesEnded에 프로퍼티로 뷰에 이 터치 이벤트를 어떻게 전달할지 정할 수 있다. 이 프로퍼티를 잘 써서 응답하지 않는 뷰에도 응답하는 것처럼 만들 수 있다.

요약
첫번째 파트는, 터치 이벤트가 어떻게 윈도우에서 뷰까지 탐색하는지 보았는데, 뷰는 responder chain을 통해 비슷한 경로로 돌아와 터치가 일어난다. 또한 제스처 recognizer가 어떻게 이 터치 시스템에 맞춰지는지 다루었다. 두번째 파트에서는 지원하는 다른 이벤트와 responder chain이 어디에 들어맞는지 이야기해볼 것이다.

설계와 개발에 대해 더 알고 싶으면 BPXL Craft를 구독하거나 Blick Pixel 트위터를 팔로우 해달라. 


관련 링크



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

매년 우리의 Buffer iOS 앱은 애플의 World Wide Developer Conference(WWDC) 이후에 새로운 iOS 기능과 변화를 지원하기 위해 크게 업데이트 해왔었다.

올해에 큰 변화는 없었지만, 이번 포스팅에서는 퍼포먼스와 미래에대한 대비에대해 이야기 할 것이다.

새로운 버전의 Buffer 앱의 새로운 변화
WWDC가 끝나고나면 우리는 보통 앱을 실행해보고 이전 iOS 릴리즈 버전을 최소 타켓으로 조정한다. 이번의 경우 iOS9를 최소로 조정할 예정이었으나 iOS8 기기까지 지원하게 되었다. 그리하여 앱을 정리하고 이전 호환성에 더이상 필요없는 오래된 iOS8 코드를 제거할 수 있었으며, 몇년간 기술적인 빚을 갚을 수 있었다.

Buffer iOS의 6.0버전에서 증진시키고 싶은 부분중 하나는 바로 테이블이다. 특히 나중에는 레이아웃 부분을 더 유연하게 하고 싶었다. 우리는 2011년부터 UITableView를 사용하고 있었다. 이것은 컨텐트의 목록에는 좋지만, 나중에 v6에는 컨텐트를 그리드(grid)형식으로 보여주고 싶었다.

목표 : 부드러운 스크롤링을 위해 초당 60프레임을 지원
우리는 항상 부드러운 스크롤을 위해 마법처럼 초당 60프레임을 달성하려고 노력해가며 테이블을 업데이트 했다. 몇년간 아이폰이 더욱 강력해졌음에도 이것은 매우 어려운 목표치였고, 스마트폰이 싱글 코어 디바이스일때 설계된것같은 UIKit은 아직또 메인스레드에 묶여있다.

사용자가 프레임이 떨어지는 것을 알아차리지 않으려면 모든 레이아웃과 코드 랜더링이 16ms 안에 실행되야하는데, UIKit은 이것을 개발자에게 떠넘겼다. 시스템이 오버헤드하면 가끔 10ms 정도 휘청거린다.

우리의 UI는 매우 튼튼하고 다채로우므로 이 과제는 매년 포퍼먼스 유지보수를하게 만들었다. 이 기술적인 과제는 사용자가 "로딩" 화면을 보지 않으면서 스크롤 하는동안 어떻게 부드럽게 업데이트를 서버에서 검색하는지와 연관이 있다.

우리의 셀(cell)들은 사용자가 만든 소셜미디어 업데이트의 다양한 높이의 UI를 지원하는데, 이것이 꽤 복잡하다. 그래서 올해 애플이 UICollectionView의 퍼포먼스를 올렸다는 발표를 들었을 때 우리는 매우 흥미로웠다. 테이블 스크롤링을 하면서 처리할게 많을때 우리의 기대치는 60프레임을 유지하는 것이었고, 적어도 55프레임 이상 유지하길 바랬다.

우리는 먼저 앱에서 UICollectionView를 어떻게 이용할지부터 찾아보았지만, 현재의 UITableViews와 비슷한 레이아웃을 만들어내는데 몇 가지 장애물이 있었다. 컬랙션 셀의 오토사이징에대한 새로운 추가 사항과 함께 몇 가지 문제가있었다. 우리는 iOS가 높이를 계산하고나면 스크롤이 점프해서 왔다갔다하는 모습을 보고 있었다. (v5.0에서 가능하면 미리 높이를 계산해두는 방법을 찾았었다) 더 나아가 오토 레이아웃의 편리함은 무거운 처리의 비용으로 돌아오고, 이것은 메인스레드에서 일어났다.

전략: 페이스북의 AsyncDisplayKit을 사용하기
얼마전에 아직 보진 못했지만 페이스북이 AsyncDisplayKit(ASDK)를 만들었다는 소식을 접했다. 페이스북은 그들의 제스처와 애니메이션 특징의 앱(Paper)를 위해 2014년 10월부터 ASDK를 커스텀하여 만들고 있었다. 그들은 UIView의  스레드-세이프 추상화를 통해 메인스레드에서 뷰를 랜더링하는데, 초당 60프레임이 유지되도록하여 앱이 부드럽게 동작하게 만들었다. 추가로 ASDK는 "Intelligent Preloading"이라 불리는 강력한 API를 가지는데, 이것은 서버로부터 응답이 필요한 컨텐트 "목록"을 사용자가 스크롤하는 곳에 네트워크 호출과 불러오기를 효율적인 방법으로 다룬다.


우리는 이번 기회에 이것을 확인하여 사용해보자고 마음을 먹었고, 몇 시간 안에 텍스트만 갱신하면서 스크롤이 점프해서 왔다갔다 하지 않는 v6.0을 만들었다.

AsyncDisplayKit이 셀 랜더링을 처리해줌으로서 heightForRowAtIndexPath:나 estimatedHeightForRowAtIndexPath: 구현 없이 정확한 높이 값을 받아냈다. 이 컨셉 증명을 통해 아이폰5C에서 부드러운 60프레임을 확인할 수 있었다(아이폰6+에서는 이미 더 좋은 퍼포먼스를 냈다)

AsyncDisplayKit은 UI를 만들기위해 "Nodes"를 사용하는데, 우리의 바뀐 셀이 ASCellNode가 되도록 다시 만들어야했다. 또한 다른 요소를 새로운 클래스로 추상화하여 셀의 구조를 정리하는데 시간을 썼으며, 2800줄이나 되던것을 945줄까지 줄이게 되었다. ASCellNode는 또한 컬랙션 뷰나 테이블 뷰 둘다와 함께 양립해서 사용할 수 있다는 이점도 얻을 수 있었는데, 이것이 미래의 기능을위해 리팩토링 한 후에 얻은 레이아웃 유연성이다.

ASDK의 레이아웃은 CSS Flex Box 모델을 기반으로 하는데, 이것은 현재 오토 레이아웃 코드의 시작점중 일부이기도 하다. 그러나 Inset, Overlay, Center, Stack등을 사용하여 레이아웃 기능을 꽤 쉽게 사용할 수 있다. 우리는 빠르게 바뀐 셀을 레이아웃에 구성할 수 있었으며, 아이패드에서 읽을 수 있는 레이아웃 가이드로 방향 전환과 고정된 폭 지원까지 해줌으로서 오토 레이아웃보다 더 깔끔하게 보이게 되었다.

AsyncDisplayKit의 소개때부터 핀터레스트는 페이스북과 협력하여 AsyncDisplayKit v2.0 작업을 함께 하고 있었다. 우리는 항상 페이스북과 핀터레스트 iOS 앱 둘다의 퍼포먼스에 놀랐었다. 핀터레스트 앱에서 순식간에 핀을 통해 스와이핑할때는 정말로 인상적이고, 좀처럼 로딩화면에 도달하지 않는다. 우리가 보았던 그들의 포퍼먼스 증진을 큰 인상을 받아왔는데, Buffer iOS앱의 다른 영역에서 ASDK를 사용하여 탐험할 수 있음에 흥분을 가라앉힐 수 없었다.

아래 비디오는 개발하는 중에 찍었는데, 새로 코딩한 버전의 v5.6 큐를 보여준다. 여러분은 셀 높이를 계산하는 iOS가 스크롤이 점프해서 왔다갔다하는것이 적어지면서, 건더뛰는 프레임도 줄었다는 것을 알 수 있을 것이다.

다음으로 할것 + 여러분의 생각
다른 오픈 소스 애플리케이션을 ASDK 예제와 곧 공유하여 다른 사람들이 자체 애플리케이션에서 ASDK를 사용할 수 있게 도움이 되면 좋겠다. 첫 번째 버전은 Buffer iOS 앱에서 업데이트를 재정렬하는 데 사용되는 코드이다.

프로젝트에서 ASDK를 사용해볼 기회가 있다면, 우리는 여러분의 경험을 들어보고 싶다. 부드러운 스크롤링에대한 다른 팁이나 질문이 있다면 코멘트를 달아달라. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

당신은 시뮬레이터 빌드 속도를 올리기 위해 CrashlyticsHockeyApp와같은 써드파티 크레쉬 리포팅  서비스를 사용하는가? 오늘 나는 이것이 그냥 이상한 속임수가 아님을 알려주겠다(원문: I've got just the right not-so-weird trick for you today).

이론
지난 몇년동안 빌드 시간을 단축시키는 것에 대한 많은 조언들이 있었다. 많은 글쓴이들은 Debug 구성에서 Debug Information FormatDWARF로 바꾸어라고 제안했다. 오늘날의 Xcode는 이것이 디폴트로 한다. 이 설정으로 디버그 심볼(생각: 클래스와 메소드 이름)은 바이너리에 직접 들어간다.

그 대안으로는, dSYM 파일로 DWARF하는 선택지가 있다. DWARF에 비해 두가지 장점이다.
  • 바이너리를 난독화 시킨다
  • 바이너리 크기가 작아진다

그리고 커다란 단점에는
  • 따로 분리된 dSYM 파일은 각 빌드마다 생성되기 때문에 긴 빌드시간이 걸린다.

이것은 release 구성에서 디폴트이며, 이 경우는 이해해줄 수 있다.

문제점
위에서 말한 설정은 많은 경우에 완벽하게 괜찮다. 그러나 디버거에 붙이지 않고 DWARF 구성으로 앱을 빌드하면 실패한다. 시도해보면 아래처럼 심볼화되지않은 크래쉬 리포트를 보게 될 것이다.
0   YourApp                         0x00000001001e1594 0x100058000 + 1611156
1   YourApp                         0x00000001000ed74c 0x100058000 + 612172
2   UIKit                           0x000000018f2b67b0 -[UIApplication sendAction:to:from:forEvent:] + 96
3   UIKit                           0x000000018f42a5ec -[UIBarButtonItem(UIInternal) _sendAction:withEvent:] + 168

우리는 이것은 symbolicatecrash 툴을 이용해서 손수 심볼화시켜주어야한다.

symbolicatecrash YourApp\ \ 17-10-16\ 23-15.crash YourApp.app/YourApp > symbolicated.crash

크러쉬 난 빌드를 위해서는 .ipa(혹은 .app) 파일에 접근해야할 때가 있음을 인지해라. .ipa 파일은 아카이브 하지 않고는 자동으로 생성되지 않는다.이것은 이 크래쉬 상황의 심볼화 프로세스를 만든다. 그래서 왜 Crashlytics가 각 빌드마다 dSYM 파일을 생성하도록 지향하는지의 이유이다.


(우리가 코드를  수정했다면) 디폴트 설정은 매 빌드마다 앱의 dSYM 파일을 생성하는 것을 보장하는 프로세스이다. 이것이 더 손호하는 구성이므로 개발중이나 내부 테스팅 중에 크레쉬가 나면 그 크레쉬가 난 정확한 라인을 짚어줄 것이다.

내 생각엔 기기에 직접 크레쉬를 심볼화시킬 수 있을지도 모르지만, 더이상 그렇게 할 수 없을 것 같다. 그 이유는 다음과 같다.
왜 써드파티 크레쉬 리포터들이 매 케이스마다 dSYM 파일을 필요로하는지에대한 이유일 것이다. 따라서 이제 왜 dSYM 파일이 필요한지 알았으므로 귀중한 빌드 시간을 어떻게 절약할 수 있는지 보자.

비결
dSYM 파일을 생성하든 말든 그 선택은 확실해 보인다. 모든 희망을 잃었다...

타겟 SDK에따라 Debug Information Format 값을 다르게 설정할 수 있게 되었다!


이 설정이 우리에게 주는 것은:
  • 시뮬레이터 빌드 속도가 더 빨라진다(우리는 거의 항상 연결되있는 디버거를 쓰므로 이제 크레쉬 리포트는 필요하지 않다)
  • 기기 빌드에대해 모든 크레쉬 리포트(디버거 외부에서 동작할 확율이 높은)

요약

우리는 매우 쉬운 방법으로 시뮬레이터 빌드 시간을 단축시켰다. 내 경우는 점점 증가하는 빌드(incremental build)에서 1.5초정도 떨어졌다(전체 빌드 시간의 15%). 큰 차이가 아닌것 같아 보여도, 몰입의 순간에는 매 초가 중요할 것이다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

때때로 앱은 크레쉬가 난다. 크레쉬는 사용자의 워크플로우를 방해하는데, 데이터가 손실되거나 백그라운드에서 앱 오퍼레이션이 어떠한 손상을 주기도 한다. 개발자에게는 해결하기 어려운 크레쉬는 다시 재현하기 힘들거나 찾아내기 힘든 것들일 것이다.

나는 최근에 Castro에서 발견하기 힘든 많은 크레쉬들 때문에 생긴 버그를 찾아다가 고쳤는데, 이 이야기를 공유하면서, 여러분이 경험하는 비슷한 이슈에대해 도움이 되길 바란다.

Oisin과 나는 9월에 Castro2.1을 배포했다. 그리고 곧 아이튠즈 커넥트를 통해 Castro 크레쉬가 높게 치솟는다는 보고를 받았다.

2.1버전 이후 Crastro2 크레쉬 리포트를 보여주는 차트2.1버전 이후 Crastro2 크레쉬 리포트를 보여주는 차트


아이튠즈 커넥트 크레쉬 리포트
흥미롭게도 이 크레쉬들은 우리가 주로 쓰는 크레쉬 리포팅 서비스(HockeyApp)에 나타나지 않았다. 따라서 실제로 그 문제를 발견하는데까지 시간이 좀 걸렸다. 또한 모든 크레쉬를 알아내기 위해 개발자들은 아이튜즈 커넥트나 Xcode를 통해 크레쉬 리포트를 확인했다. (업데이트: "써트파티 크레쉬 리포터는 기록을 남기기위해 in-process 핸들러를 사용한다. 그러나 OS가 밖에서 여러분 앱을 죽이면 핸들러는 절대 동작하지 않을 것이다"고 Greg Parker가 알려주었다. 추가적으로 HockeyApp의 공동 창시자인 Andreas Linde가 한 아티클을 소개해주었는데, 글은 HockeyApp이 어떤 종류의 크레쉬를 찾아내고, 찾아내지 못하는지 설명해주는 글이었다.)

여러분이 만약 앱 개발자이고 여러분의 계정으로 로그인 했다면 Xcode가 애플에의해 사용자로부터 모은 크레쉬 리포트를 검토할 수 있게 해준다. 이 기능은 Organizer 윈도우의 크레쉬 탭에서 찾을 수 있다. 앱 버전을 고를 수 있고, 개발자와 정보를 교환하고자 동의했던 사용자로부터 애플이 모은 크레쉬 리포트를 다운받을 것이다.

Xcode에서 이 부분을 사용할 때 크레쉬가 나는 경향이 있다는 것을 발견했다. 특히 크레쉬 리포트에서 쓰레드 방향의 디스클로저를 열고 닫는 토글링시 크레쉬가 나타났다. 쉽게 하는 방법은 리스트 한편에 크레쉬를 오른쪽 마우스로 클릭하고, "Show in Finder"를 선택한다. 나타난 번들을 더 자세히 보면 일반 텍스트 파일로 크레쉬 리포트를 볼 수 있다.

크레쉬 조사하기
크레쉬는 원래 코드 경로에의해 발생되지만, 항상 데이터베이스 쿼리 실행 메소드에서 끝난다.

먼저 나는 스레드 이슈가 아닐까 의심했었다. 지난 몇년동안 스레드 버그러부터 나온 고통을 많이 겪었기 때문에, 나는 항상 그 고통을 먼저 떠올린다. 텍스트 파일로 된 크레쉬 리포트를 열어보니 Xcode가 보여준 것보다 훨씬 세부적인 내용을 볼 수 있었다. 예외타입은 EXC_CRASH (SIGKILL)이고, 노트(note)는 EXC_CORPSE_NOTIFY이며, 종료 원인은 Code 0xdead10cc였다. 나는 0xdead10cc가 무엇을 의미하는지 찾아보았다. 구글과 애플 개발자 포럼에는 이것에대한 이야기가 그리 많지는 않았지만 Technical Note 2151에서 이야기 해주었다.

0xdead10cc 예외코드는 백그라운드에서 돌고있는 앱을 시스템 자원의 이유로 iOS에의해 종료시킨 경우를 말한다.

여기서 내가 알아낸 점은 iOS가 앱을 종료시킬때 우리 코드상에서 뭔가 실수때문만은 아니며 정책 침해 때문일 수도 있다는 것이다. 그러나 Castro는 Adreess Book database를 사용하지도 않고 시스템 리소스와 비교가능한 어떤것을 사용하지 않는다고 생각했다. 나는 앱이 백그라운드에서 너무 오래 동작해버리면 어떻게할지 고민했는데, 우리가 받는 크레쉬 리포트의 몇 경우는 고작 2초정도 동작하고 말았다.

나는 데이터베이스에서 일어나는 크레쉬를 쫒아 수많은 stack trace pointing으로부터 우리의 데이터베이스 SQLite 파일에 문제가 있음을 결국 찾아냈다. 그러나 왜 이러한 크레쉬가 2.1버전이 되서야 나타났을까?

공유된 앱 컨테이너
Castro의 2.1 릴리즈에서 최근 재생한 에피소드를 쉽게 공유하기 위한 기능인 iMessage 앱을 지원했다. 메시지 앱이 데이터베이스에 접근하는 과정에 우리는 공유된 앱 컨테이너로 데이터베이스를 옮겼다.

파일이 공유된 공간에 있으면 그 팔일이 락이 걸리는 정책을 가진다면 어떨지 고민해보았다. 아마 iOS가 앱을 멈출때 다른 프로세스가 그 파일을 사용할 수 있는지 체크하고, 만약 그렇다면 iOS는 그 앱을 종료시킨다. 이것은 무리없이 있을법한 이야기이다.

어떻게 크레쉬를 낼까
크레쉬를 고치기위해 크레쉬를 재현해보는 것은 프로그래머에게 좋은 습관이다. 그 크레쉬를 흉내내기 위해 어느 부분을 임시로 다시 짤 수 있다. 만약 크레쉬가 확실히 일어나게 해보았으면, 이런 의심을 확인해보는것과 어느정도 거리가 있으며 우리에게 이 잼제적인 문제를 테스트 할 수 있는 기회를 제공한다. 다른 대안으로는, 무턱대고 일단 고친다음 배포하여 크레쉬 리포트를 받아보는 것이다. 이떨때는 정말 이 방법밖에 없을 수도 있는데, 너무 장황한 프로세스이며 여전히 사용자에게 크레쉬를 만들고 있다.

특정 어떤 크레쉬는 다시 만들어보기 매우 어려운 경우가 있다. iOS 개발에 대한 적당한 평가가 여기에 있다고 생각한다. OS는 적극적으로 이 정책을 시행한다. 대부분의 경우 보안상이나 베터리 사용량 등에서 이 정책을 시행하면 좋은점이 많다. 그러나 이런 상황에서 테스팅이나 디버깅을 하면 더 힘들다. 정책을 살짝 바꾸고 손수 그 가능한 라이프 사이클 상태를 테스트 하는 것은 매우 번거로울 뿐더러 때론 불가능한 경우도 있다.

이번의 경우 디버거를 연결한 상태에서 앱 정지를 시키는 방법이 없음을 알아냈다. 사실 디버거는 앱 정지를 막으며 시뮬레이터는 정확하게 시뮬레이팅하지 않는다. 나는 디버거 없이 앱을 실행하고 기기의 로그를 살펴보는 방법밖에 없었다.

macOS 시에라의 새로운 콘솔 앱은 연결된 아이폰의 시스템 로그를 볼 수 있게 해준다. 기에라 이전에는 이 기능을 위해 Lemon Jar의 iOS 콘솔에 의존했으나, 애플이 로그에 접근할 수 있게 하면서 이 기술이 애플에게 받아드려지고 지원하게 되었다는 것을 알게되어 매우 기쁘다. 새로운 콘솔 앱을 어떻게 쓰는지 배우는 것도 의미있는 시간이 될 것이다. Xcode 디버거가 혼자서는 할 수 없는, 많은 iOS 동작을 보여준다. 이 로그는 시스템 전체의 로그이기때문에 디버깅에 무의미한 로그들도 많지만, 필터를 적용해서 간편하게 당신의 앱에 관련된 메시지만 뽑아낼 수 있다.

0xdead10cc 크레쉬를 찾기 위해
  • 몇백개의 데이터베이스 쿼리에 applicationDidEnterBackground 메소드를 설정한다.
  • 내 맥에서 콘솔앱을 열고 Castro를 언급한 메시지로 걸러낸다.
  • Xcode를 통해 앱을 설치하되 앱 아이콘을 눌러 앱을 실행한다.
  • 앱을 백그라운드로 보내기위해 홈버튼을 누르고 바로 포켓몬고를 열어 메모리 압박으로 인한 Castro의 정지를 기대한다.
이 단계를 몇번 거치고나면 크레쉬를 다시 만들어내어 콘솔에서 다뤄볼 수 있다. 이 흔적은 실제 크레쉬와 비슷하며 이제는 크레쉬의 원인을 찾아보기 훨씬 수훨해졌다.

그러고나서 데이터베이스에 접근할 수 있는 백그라운드 동작이 있는 앱에서 한번에 찾고 고칠 수 있게 되었다(네트워크 reachability 변화에서 앱은 백그라운드 작업 없이 리프레쉬 하였다). 리프레쉬하는 과정에서 데이터베이스에 접근할때 앱이 일시정지해 있었다면, iOS가 앱을 죽일 것이다.

백그라운드 패치를 해냄
한가지 더 놀라운 사실을 공유하고 싶다. Castro2에서는 우리의 서버가 새로운 에피소드 배포를 알려주는데, 이것이 사용자 피드의 리프레쉬를 발생시킨다. iOS가 이 메시지를 앱에 보내면 완료 블락을 가지고있는 didReceiveRemoteNotification 메소드를 호출한다. 아래는 문서에서 발췌했다.

여러분의 앱은 알림을 처리하고 특정 완료 핸들러 블락을 호출하기 위해 최대 30초까지 시간이 있다. 실제로는 알림 처리가 끝나자마자 핸들러 블락을 호출할 수 있을 것이다. 시스템은 백그라운드 경과시간, 베터리 소모량, 데이터비용을 여러분 앱에서 추적하고 있을 것이다.

이 부분에 이상한 것이 있었다. 앞에서 언급했듯 Castro는 가끔씩 2초만에 죽어버렸다. 스택 추적에서 보면 아직 완료 블락이 호출되기도 전이었다. 따라서 문서에서는 30초까지 안전하다 했어도 어쨌든 정지되어버렸다.

무슨일인지 알아보기 위해 개발자 지원 사건(developer technical support incidents)을 사용하여 꽤 놀라운 점을 발견했다. 굉장히 도움이 된 Kevin Elliott 대답을 찾았는데, 그 엔지니어의 경우가 내 경우와 비슷했다.

dead10cc 이슈가 파일 락 때문에 일어난 것이라하여
무엇이 실제로 이것을 발생시켰냐하면, 당신의 앱이 정지했을때 iOS가 당신의 앱 컨테이너에서 락이 걸린 파일(이 경우에는 SQLite 락)을 발견했기 때문이다. 그렇게 체크하는 이유는 앱 내에서 데이터가 오염되는 것을 줄이고, 관리하기 위한 방법으로 추가된 것이다. 여기서의 문재는, 락이 걸린 파일은 아직 수정중에 있고 합칠 수 없는 상태로 있을 것이다. 다른 말로는, 앱이 주어진 파일에 락을 거는 유일한 이유는 그 파일에대해 추가적으로 일련의 읽기/쓰기를 할 수도 있으며, 그 사이에 다른 쓰기가 끼어들어오지 않게 쓰기를 완료하기 위한 보장을 해주기 위함이다. 좀 더 확장하여, 파일이 아직 락이 걸려있다면 아직 쓰기를 완료하지 못했다는 의미이다. 이러한 상태의 한 파일은 몇몇 잠재적인 문제를 가진다.
  • 앱이 정지해있을때 앱을 죽이면 데이터 쓰기가 이루어질 수 없고 데이터를 오염시킨다.
  • 파일이 두 앱 간에 공유되고 두번째 앱/extension이 실행되면, 두번째 앱은 강제로 락을 풀고 일관된 상태에 파일을 복구하려 할 것이다. 첫번째 앱은 일관되지 않은 상태를 빠져나오거나, 공유된 파일을 완전히 무시한다.

백그라운드 시간의 30초에 관해서는 
... 여기서 올바른 대답은 문제를 완전히 해결하는 것이다 - 만약 델리게이션으로 여러분의 작업을 다 못끝내면 백그라운드 작업을 시작하고 앱을 정지하기 전에 iOS가 (완료 블락에서) 알려줄 것이다 ...

백그라운드에서 앱이 돌아가는 동안 공유된 컨테이너를 통해 파일을 접근할 수 있는 앱은 백그라운드 작업을 만들 수 있으며, 완료 블락이 30초동안 커버할 수 있다고 가정한다. 이런식으로 작업하기 위해 개발자들은 UIApplication에서 beginBackgroundTaskWithName:expirationHandler 메소드를 사용해 백그라운드 작업을 만들고, 백그라운드 작업이 끝나면 endBackgroundTask를 호출한다.

추가로 kevin은 한가지 더 제안을 해주었는데, 치솟는 데이터 처리가 끝났음을 보장하고 좀 더 확실하게 이상한 버그를 해결하기 위한 방법으로, 백그라운드로 넘어갈 때 앱은 데이터베이스를 닫을 것을 제안했다.

일반적으로 오퍼레이션으로 파일을 닫는 것은 불가사의한 버그(백그라운드에서 뭔가 동작하지 않아요)를 좀 더 명확한 버그(내 앱이 백그라운드에서 동작하지 않아요)로 바꿀 수 있다. 이 점이 문제를 직접 해결할 수 있게 도와줄 것이다.

이것은 꽤 스마트해보인다. 백그라운드로 갈 때 앱의 일부를 종료하는 생각이 들지 않았지만, 확실히 의미가 있다. 나는 백그라운드에서 데이터베이스를 닫는 방법을 Castro의 다음 업데이트에서 찾아볼 것이다.

결론
백그라운드에서 계속해서 동작하는 백그라운드 작업을 추가하여 우리의 베타버전에 이 이슈를 고쳤다. 우리는 곧 고친 것을 포함한 업데이트를 배포할 것이다.

아래에는 내가 배운 것들의 요약본이다.

  • 애플은 다른 서비스들이 발견하지 못한 크레쉬를 보고해준다. 아이튠즈 커넥트나 Xcode, 외부 서비스들까지도 크레쉬 리포트를 확인해보자.
  • 파일 락의 정책은 당신의 데이터베이스가 공유된 공간에 있을 때 더욱 엄격해진다.
  • 백그라운드 패치 완료 블락에 의존하는 것으로는 충분하지 않다. 활성 백그라운드 작업없이는 백그라운드에서 어떤 작업도 하지마라.
  • 앱 라이프 사이클의 특정 부분에서만 나타나는 이슈는 디버깅하기 힘들다. 아직 재현해보지 못했다면 새 시에라의 Console.app을 배워서 사용해보아라.
  • 기술 지원 사건들(Technical Support Incidents)을 기억하며, 이것도 우리가 매년 지불하는 개발자 맴버쉽에 포함된다(원문: Don’t forget about Technical Support Incidents, you get 2 included in your developer account payment every year).



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

이전 포스팅에서 NSScreencast iOS 앱을 위한 다운로드 시스템을 어떻게 설계하는지에대해 이야기했다.

여기서 사용자에게 강제로 포그라운드(foreground)에 붙잡아두고 다운로드 받게 할 필요가 없었으므로 자연스럽게 백그라운드 다운로드를 지원하게되었다.

겉으로 보았을때는 꽤나 쉬워보였다. 세션을 백그라운드 세션 구셩으로하고, id를 부여해서, 앱과는 분리된 진행으로 다운로드가 일어날 것이다.

백그라운드 세션을 사용할 때, 세션과 델리게이트가 주어진 다운로드에서 나중에 업데이트를 받기 위해 다시 생성될 필요가 있어보였는데, 블락 기반의 작업 API를 쓸 수 없었다. 여러 시나리오를 생각했지만, 먼저 이상적인 방식을 이야기해보자.
  • 사용자가 다운로드를 시작하고 앱을 멈춘다.
  • 몇 초뒤(내 경험으로는 10-30초이다) 앱은 꺼진다.
  • 다운로드는 다른 프로세스에서 계속 진행된다.
  • 다운로드가 끝나면, 앱은 다시 켜지고 당신의 앱 델리게이트가 다운받기 시작할때 사용한 id와 함께 application(handleEventsForBackgroundSessionWithIdentifier:)에서 받는다.
팁: Xcode에서 디버깅할때, 사실상 디버거는 백그라운드에 있는 동안 앱이 꺼지는 것을 막는다. 따라서 나는 Wait for Launch 옵션을 켜고, 수동으로 앱을 키고 다운로드를 시작한 뒤, 디버거를 켜기 전에 앱을 백그라운드에 놔두었다.

이 메소드가 호출될 때, 같은 id로 새로운 세션 구성을 만들어야한다. 그리고 델리게이트 인스턴스를 심는다. 이 시스템은 다운로드 상태를 즉시 여러분의 델리게이트에 알릴 수 있다.

그러나 어떤 다운로드일까?

실제로는 모른다. 당신이 얻은 모든 것은 원래의 요청 URL인데, 충분한 정보일 수도 있고 아닐수도 있다. URL은 가끔 바뀌기도 하므로 유일한 값이 아닐 수도 있으며, 가장 좋은 키 값이 아니다. http와 https 둘 다 가지고 있을때, 이런 경로들은 같은 리소스를 가리킬 수도 있고, 아마 할쪽이 리다이렉트 할것이다. 왜 이런게 불편한지에대한 여러 이유가 있다. 내 경우 일반적인 에피소드 URL들을 연관지어서 가지고 있는데, 이 URL은 아마존 클라으드프론트 URL로 리다이렉트하여, 유일하지도않고 임시적이기까지했다. 따라서 알림을 받은 에피소드 모델로 돌아갈 방법이 없게 되버린다.

이것이 API에서 좀 이상한 부분인데, 문서에 명확하게 명시되있지 않았다. 그러나 내가 찾은 해결책은 각 다운로드마다 유일한 세션 id를 부여하고 모델에 저장해 놓는다. 그러면 어떤 다운로드가 알림이 왔는지 쉽게 찾아낼 수 있게 된다.

좋다. 이상적인 상황에서는 확실하다. 그러나 이상적이지 않은 상황은 어떨까? 만약 다운로드가 실패한다면? 셀룰러 접근을 해제하고 다운로드가 백그라운드에서 일어나는데 Wi-Fi 존을 벗어나면 어떻게 될까?

마지막 경우는 어느정도 방법을 알고 있다. 만약 보통 세션 구성으로 Wi-Fi 에서 다운받기 시작한 뒤, Wi-Fi를 끈다면 여러분은 셀룰러 다운로드가 허용되지 않았다고 즉시 에러를 받을 것이다. 그러나 백그란운드 세션을 사용하고 있다면 시스템은 똑똑하게 Wi-Fi존에 다시 들어올때까지 기다리다가 그때 요청을 다시 시도한다.

다른 에러로 나타날 수도 있다. 실제로 내 로컬 서버가 동작하지 않을때 커넥션 에러가 나는데, 다운로드는 계속 진행되는것 같지만 계속해서 0%를 가리켰다. 로컬 서버를 켜니 마치 아무 문제 없던것처럼 다운로드를 시작했다.

요청을 재시작할때까지 얼마나 기다려야하는지에대한 이야기가 문서에 명확하게 나와있지 않았다. 사실 사용자가 앱을 다시 실행하면 다운로드는 어떤 상태이야야할까? 우리는 어떻게 알 수 있을까? 개발을 하면서 가끔 주인없는 다운로드를 발견했다. 다운로드 정보의 상태는 .downloading인데 완료나 성공이나 다른 콜백을 받지 못했다. 그때 내가 한 것은 실패로 다시 표시하는 것이다. 그런데 사실 '언제' 그것을 해야할까? 다운로드는 시간이 좀 걸릴 수 있고 몇분 뒤에 다시 시작할 수도 있으므로 단지 x분 뒤에 실패로 표시하는것처럼 간단하지는 않ㄴ았다.

그런식으로 처리하긴 했지만 아마 옳은 방법은 아닌것 같다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,



NSScreencast iOS 앱에서 비디오를 다운받아  오프라인에서도 사용할 수 있는 기능을 넣고 싶었다. 보통 비디오는 80에서 200MB 크기인데, 이것에는 실패시 다시 복구하는 다운로드 시스템을 만들기 위해 신경을 조금 쓸 필요가 있었다.



첫번재로 해야 할 일은 에피소드 화면에 진행상태가 보이는 다운로드 버튼을 넣는 것이었다. 여기서는 iTunes에서 음악을 다운 받는 것과 비슷하게 구현했다. 다운로드는 백그라운드에서 일어나며 진행 노티피케이션을 받아서 UI에 퍼센테이지로 나타낸다.




NSOperation 서브클래스를 통해 실제 다운로드가 끝난다. 이런 방식은 동시성을 제어하고, quality of service과 비행기 모드 다운로드에서 취소시키는 메커니즘을 쉽게 구현할 수 있게 해준다. 다운로드 진행은 에피소드 id를 가진 노티피케이션을 통해 보내지는데, 이 부분과 관련된 UI는 이것을 가져와서 갱신할 수 있다.


물론 다운로드 되는 동안 사용자가 화면을 응시하고 있을 필요는 없으므로, 앱을 둘러보든 기다리든 다운로드는 계속 진행될 것이다.

다은으로 어떤것이 다운중인지, 다운받은 비디오를 오프라인에서 보고, 저장공간을 확보하기 위해 다운로드 중인 것과 예전에 다운로드 받은 것들을 한 곳에서 볼 수 있게 하고 싶었다.

이 화면은 각 줄이 에피소드로서 테이블 뷰 안에 데이터를 보여준다. 현재 다운로드 받은 셀들은 진행 노티피케이션을 받아서 빨리 다시 불러와야 한다.

이 모든것들을 하기 위해 상태를 코어데이터 모델로 저장했다. 나의 DownloadInfo 모델은 아래와 같이 생겼다.


이것을 위해 코어데이터를 활용함으로서 그 라이프 사이클 내내 다운로드 상태를 추적할 수 있다. 이전에는 plist를 사용했었는데, 간단한 저장소로 plist보다 코어데이터가 더 간편했다.

모델에 다운로드 진행 상태 퍼센테이지를 저장하는 것을 볼 수 있지만, 이것을 반복적으로 코어데이터에 저장하지는 않았다. 빠른 커넥션에서 다운로드 진행 변화가 빠르게 일어날 것이고, 코어데이터를 매번 저장할 필요는 없었다. 요청이 취소되거나 다운로드가 더이상 일어나지 않음을 UI에 보여주고 싶을 때만 데이터를 저장했다.

코어데이터를 사용해서 얻은 또다른 장점에는 빠르게 DownloadsViewController를 구성하기위해 NSFetchedResultsController의 유용한 점을 사용할 수 있다는 것이다.

실패 처리하기
네트워크는 항상 뭔가 잘못될지도 모른다는 가능성을 가지고 있다. 이러한 가능성은 큰 파일 다운로드시 더 커진다. 사람들이 Wi-Fi 존을 벗어나거나, 터널로 들어간다던지, 비행기 모드를 켠다던지, 다른 다운로드들과 함께 한꺼번에 많이 다운받는다던지 할 수도 있다. 최고로 좋은 사용자 경험을 보장하기 위해서는 이것을 다루고 싶었고 사용자가 빨리 재시도를 할 수 있도록(때때로는 자동으로 재시도 하도록) 하고 싶었다.

실패가 일어나면 Download의 state 프로퍼티를 .failed로 바꾸고 UI를 적절하게 갱신한다. 그 셀을 다시 불러오고 사용자는 다운로드를 재시도 하기위해 탭할 수 있다.

일시정지와 재개
NSURLSession API가 시작되면, 요청을 취소하고 resume data라 불리는 애매한 오브젝트를 만들어내는 기능을 추가한다. 이것을 이용하면 끊긴 곳에서 요청을 시작할 수 있으며, 한가지 의문은 나중에 다시 시작할 수 있도록이 데이터를 유지하는 방법이다. NSScreencast 모델에 추가하는 것이 딱 알맞았다. 사용자가 진행중에 다운로드를 누르면 downloadTask.cancel(byProducing:)을 호출하고 나중에 사용하기위한 재개 데이터를 모델에 저장한다.

다운로드가 시작되고 모델 데이터가 재개 데이터라면 어느 시점에서 끊겼든지 그 곳에서 요청을 재개하는데 사용된다. 이 기능은 추가하기 쉬운데다, 큰 파일 다운로드시 굉장히 유용하다.


셀룰러 다루기
나는 다른 사람들의 데이터 요금을 불태우고 싶지 않았으므로 NSURLSessionConfigurationallowsCelluarAccessfalse로 설정해두었다. 그릭고 사용자가 원할때 셀룰러로 다운받을 수 있게 토글을 추가했다.




Fx Reachability를 사용하여 커넥션 상태를 모니터링했다. 사용자가 셀룰러일때 에피소드를 다운받으려 한다면 토글을 띄워 설정하고 다운로드 할 수 있게 하였다.

모델과 파일시스템의 동기화 유지하기(Keeping the Model and FileSystem in Sync)
파일에대한 메타데이터는 디스크에 저장하기 때문에 이것이 항상 동기화 되어있음을 보장해야한다. 에피소드를 하나 삭제하면 코어데이터 모델 뿐만 아니라 디스크에 파일도 삭제해주어야한다. 이 동기화를 보장해주기위해 나는 CleanupDownloadsOperation을 가지고 있는데, 이것은 앱이 켜질때 실행되며, 각 저장된 DownloadInfo가 디스크에 옳바른 파일을 가지고 있는지(혹은 삭제되었는지) 확인하고, 다운로드 폴더의 각 파일이 코어데이터에 저장되 있는지(혹은 삭제되었는지) 확인한다.

이렇게하여 뭔가 잘못되거나 두가지 상태(데이터페이스/디스크)가 싱크가 맞지 않을때의 대비책을 마련해 둔 것이다.

백그라운드 다운로드
겉으로 보기엔 간단해 보이지만 백그라운드 다운로드는 혼란과 복잡함의 근원으로 표현된다. 이 주제에 대해서는 나의 다음 포스팅에서 다루겠다.

그들이 말하는 그냥 오프라인 다운로드 추가하기
처음에 오프라인 다운로드를 앱에 넣어보기 전까지는 그냥 하루 이틀정도 더 걸리는 작업이라 생각했지만, 어마무시하게 복잡한 작업이 되버렸다.

역시 이런것이 소프트웨어라 생각된다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

올해 초에 우리는 속도, 명확한 경험, 그리고 미국 바깥의 우리 앱을 사용하는 피너(pinner)들을 위해 우리 앱의 새 구조를 짜게 되었다. 앱의 새로운 구조 조정 목표중 하나는 우리 앱을 완전히 불변 모델(Immutable model)로 바꾸는 것이었다. 이 포스팅에서는 이런 것에 대한 동기에대해 이야기하고, 우리의 새 시스템이 어떻게 모델 갱신을 다루는지 탐구해보며, API로부터 새로운 정보를 어떻게 불러오는지, 데이터 일관성에대해 이야기해 볼 것이다.

왜 불변 모델인가?
"불변  모델"이라는 말은 많은 앱들이 불변하게 바뀌면서부터 많이 들어본 용어이다. 불변성은 한번 초기화 되고 나면 더이상 수정되지 않음을 의미한다. 왜 우리는 이것을 선택하게 되었을까? 음, 가변성에서의 주된 문제는 공유된 상태에 있다.

이런식으로 생각해보자: 가변 모델 시스템에서 A와 B 둘 다 C를 참조하고 있는다.



만일 A가 C를 수정하면 A와 B 둘 다 바뀐 값을 보게 될 것이다. 여기까지는 괜찮지만 만일 B가 이러한 것을 예상하지 못하고 있다면 의도치 않은 일이 일어날 수 있다.

예를들어 내가 두명의 사용자와 함께  메시지 스레드에 있다. 나는 '사용자' 프로퍼티와 함께 메시지 오브젝트를 가지고 있다.

내가 이 화면에 있을 동안 앱의 다른 부분에서 Devin 대화를 제거하기로 했다(아마 바뀐 서버 응답을 받고 모델을 변경할 것이다). 이제 두번째 사람을 탭할때, 나는 두번째 오브젝트를 찾기 위해 message.users 배열을 확인해 볼것이다. 그러면 Devin 대신 Stephanie를 반환하고 잘못된 사람을 보게 된다.

불변 모델은 태생적으로 스레드 세이프하다. 이전에는 다른 곳에서 그것을 읽고 있는 동안 한 스레드가 그 모델에 쓰기라도 하는 것을 항상 신경써야했다. 우리 새로운 시스템에서는 오브젝트가 한번 초기화되고나면 더이상 수정될 수 없으며, 따라서 언세이프한 값을 읽는 것을 걱정없이 다중 스레드로 동시에 읽을 수 있게 되었다. 이것이 우리 iOS 앱을 더 동시적이고 다중스레드에 강하게 해줌으로서 쉽게 작업할 수 있게 해주었다.

모델 갱신하기
생성된 후에는 우리 모델이 완전히 불변하므로, 모델을 갱신/변경하는 유일한 방법은 새로운 오브젝트를 생성하는 방법 뿐이다. 이것을 위해 두가지 방법이 있다.
  • (보통 JSON 응답으로부터) 딕셔너리를 사용하여 모델을 초기화한다.
  • "builder" 오브젝트를 사용하는데, 이것은 그냥 모든 프로퍼티를 받아다가 필요한것만 변경하는 방식이다. 기존에 만들어놓은 모델로부터 빌더를 만든 뒤, 우리가 필요한 프로퍼티만 변경하고, initwithBuilder를 호출하여 새로운 모델을 받는다. (이것에 대해서는 나중에 새 포스팅에서 설명하겠다)

 API 데이터를 불러오고 캐싱하기
우리 API는 서버로부터 모델 필드의 부분집합과 함께 부분적인 JSON 모델을 요청할 수 있게 해두었다. 예를들어 핀 피드 화면에서 이미지 URL과 설명과 같은 필드가  필요하지만, 모든 정보가 필요하지는 않았다. 요리법 요소에서 사용자가 그 핀 화면에 들어가기 전까지 말이다. 이러한 방식은 우리가 보내는 엄청난 양의 데이터를 자르는데 도움을 주고, 게다가 백엔드에서 처리 시간도 단축된다.

우리는 PINCache로 만든 주요한 모델 캐시를 가지고 있는데, 이 오브젝트 캐시는 iOS를 위해 오픈소스로 만들었다. 이 캐시의 핵심은 유일함인데, 모델의 서버 쪽 ID이다. 우리가 새로운 서버 응답을 받을 때 현재 모델의 캐시를 확인한다. 만일 현재 모델이 발견되면 현재 모델의 프로퍼티들과 서버 응답의 필드들을 합쳐서 새로운 모델을 만든다. 이 새로운 모델은 캐시에 저장되있던 것과 대체된다. 이렇게 하면 캐싱된 모델은 항상 우리가 받은 가장 최신의 필드를 유지할 수 있다.



데이터 일관성
모델이 수정된 후(새로운 모델이 생성되고나면), 그 수정은 모델을 보여주는 그 화면에 반영된다. 이전에는 Key-Value Observing를 사용하였지만 KVO는 오직 한 모델 인스턴스의 변화만을 관찰하기 때문에 불변 오브젝트에는 적용되지 않았다. 이제 우리는 NSNotificationCenter-기반 시스템으로 그 변화를 알리고 있다.

변화를 관찰하기
뷰나 뷰컨트롤러는 모델에 갱신 알림을 등록해 놓을 수 있다. 이 예제에서는 MessageViewController가 그 메시지 모델의 갱신을 등록한다. 새로운 모델에 프로퍼티 갱신이 일어날 수 있기 때문에 새 메시지 모델이 생성되면 알림을 받고 싶다.


아래는 메시지 모델의 이름+고유id로 갱신된 모델을 듣고 있는(listen) 옵저버를 만드는 코드이다. 메소드는 block-기반 NotificationCenter API를 사용함으로서 옵저버 라이프타임 관리를 더 잘 할 수 있었다.
notificationManager는 등록된 옵저버들을 강타입으로 붙잡아 둔 NSObject이다. 뷰컨트롤러 프로퍼티여서 뷰컨트롤러의 소멸자(dealloc)가 호출되고 나서 그것을 소멸시키는데 모든 옵저버의 등록을 해지시킨다.

변화들을 보내기
메시지 모델이 갱신될 때, 갱신 알림이 보내진다.
postModelUpdatedNotificationWithObject:는 같은 클래스+서버id의 가장 최신 모델을 위해 모델 캐시를 확인하고, 캐시되어있는 모델 인스턴스를 보낸다.

UI 갱신 만들기
알림이 발생하면 NSNotification의 "object" 필드로 새로 모델을 보낸다. 그러면 뷰컨트롤러는 갱신된 모델을 사용해 그것을 갱신시킨다.

마지막으로
규모가 있는 앱의 전반적인 모델을 바꾸는 것이 쉬운 작업은 아니었다. 그리고 우리는 이러한 방법을 도와줄 좋은 도구를 만들어냈다. 다음 포스팅에서 우리가 어떤 방식으로 모델 클래스와 그 이상을 자동 생성했는지 설명할 것이다.

감사: 이 새로운 모델을 사용해주고 피드백을 준 모든 iOS 개발자에게 감사의 말을 전하면서, 특히 내 팀의 Rahul Malik, Chris Danford, Garrett Moon, Ricky Cancro, Scott Goodson, 그리고 Bella You, Rocir Santiago, Andrew Chun에게 고맙다. 




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


요즘 앱에서 멀티 스레딩이나 컨커런시(concurrency)는 거의 필수이다.. 그리고 동시성을 관리해주는 시스템단 라이브러리인 Grand Central Dispatch는 iOS SDK에서 아직까진 다루기 까다롭고 친숙하지 않은 API를 제공해왔었다.

하지만 더이상은 아니다.

Swift3에서 Grand Central Dispatch의 문법과 사용법이 많이 개선 되었다. 이 글은 그것의 새로운 사용법을 빠르게 훑어볼 것이다.

dispatch_async
이전에는 dispatch 메소드(동기적이든 비동기적이든)를 고른 뒤, 우리가 dispatch 하고싶은 작업을 큐에 넣었다. 새로 바뀐 GCD는 이 순서가 반대이다.  ― 큐를 먼저 고른 뒤에 dispatch 메소드를 적용한다.

일반적인 GCD 패턴은 글로벌 백그라운드 큐에서 작업을 수행하고, 작업이 끝나는대로 메인 큐에서 UI를 갱신하는 방식이다. 아래 코드는 새 API의 모습이다.


큐 속성들
이제 큐가 초기화 시점에서 속성을 받는다. 이것이 Swift OptionSet이며, 순차적vs동시, 메모리, 엑티비티 관리 옵션과 서비스 품질(.default, .userInteractive, .userInitiated, .utility and .background)과 같은 큐 옵션을 설정할 수 있다.

서비스의 품질(The quality of service)은 앞서 iOS8부터 디프리케이트(deprecated)된 이전 속성 대신으로 사용된다. 만약 예전 방식으로 큐를 사용하고 있었다면, 어떻게 바뀌었는지 확인해보기 바란다.

* DISPATCH_QUEUE_PRIORITY_HIGH: .userInitiated
* DISPATCH_QUEUE_PRIORITY_DEFAULT: .default
* DISPATCH_QUEUE_PRIORITY_LOW: .utility
* DISPATCH_QUEUE_PRIORITY_BACKGROUND: .background



메모리 엑티비티 관리 옵션은 올해(2016년) 애플OS를 릴리즈하면서 새로 나왔다. .initiallyInactive를 사용하여 비활성 상태에서 큐를 시작하게 한다던지, .autoreleaseInherit, .autoreleaseNever and .autoreleaseWorkItem를 사용해여 커스텀된 오토릴리즈를 설정할 수 있다.

Work items
큐들은 GCD에서만 Swift OptionSet을 필요로 하는게 아니다. 새로 바뀐 work item의 Swift 문법에서도 쓰인다.

한 work item은 이제 퀄리티나 서비스를 정의하고 (혹은) 초기화때 flags를 줄 수 있다. 이 둘다 선택적으로 가능하며 work item 실행시 영향을 준다. flags는 barrier, detached, assignCurrentContext, noQoS, inheritQoS, enforceQoS 옵션들이다.

dispatch_once
dispatch_once는 한번만 실해오디는 코드나 함수들을 초기화하는데 매우 유용했다.

Swift3에서는  dispatch_once가 디프리케이트 되었고 이것은 글로벌, 스태틱 변수와 상수를 사용하는 것으로 대체되었다.


dispatch_time_t
dispatch_time_t는 큐에서 사용할 수 있는 UInt64로 특정 시간을 변환하는 함수이다. 새로 바뀐 GCD는 이것에대해 좀 더 친숙한 문법을 소개했다.(NSEC_PER_SEC여 안녕!) 아래 코드는 바뀐 dispatch의 예제이다:

.second는 DispatchTimeInterval에서 불려진 새 열겨형 중 하나이다. 이 열겨형은 카운트를 표현하기위한 값들을 갖는다. 현재 지원하는 것들이다:
* .seconds(Int)
* .milliseconds(Int)
* .microseconds(Int)
* .nanoseconds(Int)


dispatch_assert
또한 이번에 새로 발표한 애플OS에서는 dispatch precondition이 있다. 이것은 dispatch_assert를 대체하며, 이것은 코드가 실행되기 전에 스레드를 생각하고 있는지 아닌지 체크할 수 있다. 이것은 특히 UI를 업데이트하고 메인 큐에서 반드시 실행되야할 함수에 유용하게 쓰인다. 예제코드를 한번보자:


추가적인 자료들

여기 Swift3을 포함한 더 많은 GCD 개선에 관한 이야기들이 있지만, 아직 공식적인 문서는 완성되지 않고 있다. 더 심화된 자료들이다:
  • https://github.com/apple/swift-evolution/blob/master/proposals/0088-libdispatch-for-swift3.md
  • https://developer.apple.com/videos/play/wwdc2016/720/
  • https://github.com/apple/swift-corelibs-libdispatch



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,