우리가 Viable을 최신 iOS 앱의 토대를 만들어갈때, 이전 iOS 앱으로부터 배우려 했다. 우리는 2가지 목표를 정했다.
Massive View Controller(MVC를 비꼬는 약자) 증후군 피하기
가능한 적은 중복
초기에 디자인팀이 만든 Viable 화면에는 수많은 비슷한 화면이었다. 아래에 간단하게만든 예시를 한번 보자. 두 화면은 모두 상단에 UILabel이 있고 검색 결과를 보여주는 UITableView가 있다. 각각의 결과에대한 UITableViewCell도 매우 비슷했다. 이들은 다소 레이아웃을 공유했고 데이터만 달랐다.
Viable은 화면에 표시되는 6가지 타입의 데이터가 있었으며, 각 타입마다 새로운 뷰 컨트롤러를 만들어서 코드 중복이 많았다. 그리하여 우리는 6개의 데이터 타입을 모두 표시할 수 있는 SearchResultsViewcController를 만들었다.
데이터 타입에따라 다르게 렌더링하기위해 제일 처음 떠오른 방법으로는, tableView:cellForRowAtIndexPath:에 거대한 if/else문이었는데, 코드 규모가 잘 정연되지 못했고 결국 길고 못난 메소드가 되버렸다.
MVVM와 프로토콜을 사용하여 해결하기
테일러 구이던(Taylor Guidon)은 MVVM(Model-View-ViewModel) 패턴에대한 입문의 글을 포스팅했는데, 여기서 확인할 수 있다. 이 글은 그 요약 버전인데, 데모 프로젝트에 적용한 것을 깃헙에서 확인할 수 있다.
모델(Models)
모델 그룹에서의 모델은 데이터를 담고 있는다. 우리는 DomainModel과 ProductModel을 가지는데, 둘 다 구조체이다. DomainModel은 이름(name)과 그 상태 도메인을 가질것이고, ProductModel은 제품이름(product name), 제품평점(product rating), 제품로고(product logo), 제품가격(product price)을 가진다.
struct Product {
var name: String
var rating: Double
var price: Double?
}
뷰모델(View Models)
모든 데이터 모델은 해당되는 뷰모델을 가진다. 그 말은, 우리 예제에서는 DomainViewModel과 ProductViewModel을 가진다는 뜻이다. 뷰모델은 모델로부터 데이터를 받아서 사용자에게 보여주기전에 뷰에 적용시킨다. 예를들어 ProductViewModel은 4.99 가격의 부동소수점을 받아서 $4.99라 읽히는 문자열로 변형한다.
class ProductViewModel: CellRepresentable {
var product: Product
var rowHeight: CGFloat = 80
var price: String {
guard let price = product.price else {
return "free"
}
return "$\(price)"
}
init(product: Product) {
self.product = product
}
func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
// Dequeue a cell
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductTableViewCell
// Pass ourselves (the view model) to setup the cell
cell.setup(vm: self)
// Return the cell
return cell
}
}
뷰(Views)
우리 예제에서 뷰는 두가지 UITableViewCell이다. DomainTableViewCell과 ProductTableViewCell를 가진다. 레이아웃은 앱의 스토리보드에 만들어놓았따. 두 클래스 모두 간단한데, 뷰모델을 인자로 받는 setup 메소드 하나만 가지고 있다. 뷰모델은 셀에 정보를 옮길때 사용되는데, 예를들자면 읽을 수 있는 가격($4.99)을 받아서 UILabel의 테스트 프로퍼티에 할당한다.
3가지 큰 기둥을 만들었으니 합쳐보자. 뷰 컨트롤러와 뷰모델을 합치기위해 프로토콜을 사용할 것이다. 프로토콜은 이것을 따르는 클래스나 구조체가 어떤 변수와 메소드를 가질지 정의한다. 계약서를 생각해보자. 여러분이 X라는 프로토콜을 따르고 싶다면, 여기에 명시된 모든것을 구현해야한다. 간결하게 만들기위해 한 프로퍼티와 한 메소드만 넣어놨다. DomainViewModel과 ProductViewModel 둘 다 이 프로토콜을 따른다.
protocol CellRepresentable {
var rowHeight: CGFloat { get }
func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell
}
스위프트에서 프로토콜은 일급 객체(first class citizen)이므로 SearchResultsViewController 파일은 화면에 표시할때 필요한 뷰모델 배열을 가진다. [DomainViewModel]()이나 [ProductViewModel]()처럼 배열을 초기화하는것 대신, 프로토콜을 사용하여 뷰모델을 담아둘 수 있다. var data = [CellRepresentable](). DomainViewModel과 ProductViewModel은 CellRepresentable을 따르기 때문에 배열은 둘 다 담아둘 수 있다.
이제 배열에 있는 모든 요소를 CellRepresentable을 따르게하여 UITableViewCell을 반환하는 cellInstance(_ tableView: UITableView, indexPath: IndexPath) 메소드를 가진다고 확신하게 만들자. 고맙게도 tableView:cellForRowAtIndexPath:는 cellInstance 메소드만 호출하면 된다.
로컬에 MongoDB, MySQL, Jenkins, Minecraft을 설치하고 싶지 않으면 Docker가 그것에대해 혹은 그 이상으로 도움을 줄것이다. Docker를 사용하면 여러분의 프로젝트나 프로토타입을 위해 백엔드, 데이터베이스, 배포된 앱을 빌드할 수 있다. 스위프트를 위한 Docker를 확인해보자.
iOS 프로젝트를 마무리할때, 디바이스에 올려 테스트하여 크래쉬가 없이 잘 돌아가면 만족하고 끝낸적이 있을것이다. 그게 과연 올바른 마무리일까? Instruments를 사용하여 프로파일링(profiling)을 하지 않고 끝냈다면 잘 마무리 했다고 할 수 없다고 생각한다. 그 이유는 개발자 디바이스에서 잘 동작한다해서 실유저에서 크래쉬가 안난다는 보장은 없기 때문이다.
Xcode에는 Instruments라는 퍼포먼스 튜닝 어플이 포함 되 있다. 이 프로그램은 다양한 기준으로 개발자의 앱을 프로파일 할 수 있게 해준다. Instruments는 CPU 사용량, 메모리 사용량, 메모리 누수, 파일/네트워크 활성, 베터리 사용량 등을 측정할 수 있는 툴을 포함한다. 이것들은 Xcode에서 바로 시작할 수 있어서 쉽게 켤 수 있다. 그러나 지금 보고있는 프로파일 자료가 뭘 프로파일링 한건지 잘 모를수도 있으며 이런 상황은 개발자들이 이 깊은 잠재력을 가진 툴 사용을 저해할 수 있다.
이 많은 프로파일 툴 중에 어떤걸 선택해야할까? 먼저, 느려진 네트워크 요청이나 버벅거리는 스크롤과 같은 눈어띄는 퍼포먼스 이슈 해결을 위한 선택지들이 있을 수 있다. 그러나 나는 당신이 생각하기에 모든것이 정상적으로 돌아가는것으로 보이는 부분에서 CPU나 메모리 사용량을 체크해보는걸 추천한다.
프로파일을할때
이 이야기를 하기 전에 한가지 우리가 염두할 것은, 모든 개발자들이 항상 그들의 앱에 프로파일을 하지는 못한다는 것이다. 대부분은 데드라인과 예상 결과물이 존재한다. 어떨땐 앱을 프로파일링이고뭐고 마감을 빠듯하게 지키는것도 힘들 때가 있지만, 우리는 이런 상황까지 고려하여 '어느 시점에 프로파일링을 하면 좋을까'에 대해 얘기해보고자한다.
최소한 만든 앱을 앱스토어에 제출하기 전에 한번은 프로파일링을 해줘야한다. 당신은 앱스토어의 심사를 통과 한 후 유저들이 앱을 사용하면서 안 좋은 일들이 일어나길 바라지는 않을것이다. 프로파일링을 하지 않았다가 문제가 생긴다면, 좋지 않은 리뷰들이 많아질 것이고 다운로드 수는 감소할 것이다.
Xcode는이전엔Instruments안에묻어둔많은정보를 ‘Debug Navigator(Xcode 기능의일부)’에포함하기위해위해 Instruments밖으로확장해왔다. 만일당신이⌘6 단축키를누르면, 앱에 대한퍼포먼스정보를볼수있다. 여기서 CPU/Memory/Energy/Disk/Network 엑티비티의빠른요약 정보를볼수있고즉각적인이슈를볼수도있다. 여기서상단에보이는 ‘Profile in Instruments’ 버튼을눌러서Instruments를실행할수도있는데, Instruments를 누르면디버그세션으로이동할지아니면새것을새로시작할지물어볼것이다.
CPU 프로파일링
제일 먼저우리가프로파일해볼것은 CPU 사용량이다. CPU 프로파일링을시작하기위해우리는 ‘Profile’ 이라는프러덕트를선택하고타겟은디바이스선택해야한다. CPU 프로파일링을할때, CPU 사용량에대한정확한정확한정보를얻기위해실제디바이스를사용하기를원할것이다. 만약시뮬레이터로타겟을정하면실제디바이스환경과는꽤많이다른머신의 CPU 정보를얻게된다. 이상적인테스트로는당신의작업물이가장느린디바이스에서잘동작하는지확인한뒤에빠른환경에서테스트하는것일것이다.
CPU 프로파일링은인터벌을줘서프로세스들이동작하는샘플을얻어냄으로써측정한다. 디폴트로샘플은 1ms 단위로얻어지지만, 원하면바꿀수도있다. 스냅샷들사이에서어떤프로세스들이아직돌고있는지봄으로써, 그것들이얼마나길게작동되고있는지측정할수있다.
프로파일빌드가끝이나면, Instruments가켜지면서어떤템플릿으로프로파일링을할지물어본다. 우리는 CPU 사용량 측정을위해 “Time Profiler”를쓸것이다.
이것은 Timer Profiler 셋업을위한초기 Instruments 화면이다. 이제실제로프로파일링을하기위해녹화버튼을누른다. 가끔몇몇이유로녹화버튼이비활성되있는경우가있다. 그럴때는오른쪽상단에비활성된이유가나타나있을것이다. 나는 ‘device is offline’이라는상태에서멈춘적이있는데, 보통디바이스를재부팅하면해결된다. 이제녹화버튼을누르면당신앱에서무슨일이일어나는지정보를보여주기시작한다.
상단에는녹화가진행되는동안시간이지남에따라당신의 CPU 사용량그래프가보이며하단에는동작하는동안의프로세스들의 Call Tree가보일것이다. 프로세스의초기덤프(dump)는 Call Tree에서상단에보이는자료들은매우쓸때없이 많아 보인다. 또한이것은모든시스템라이브러리의활성상태를보여줘버린다. 당신은 아마 당신이 작성한코드에만집중하고 싶을것이다. 다행히중요한정보만빨리찾도록쉽게설정하는방법이있다.
오른쪽에톱니모양처럼생긴 ‘Display Settings’(⌘2)를누르면 ‘Call Tree’를 위한 옵션들이 나온다. 디폴트로는 대부분 옵션이 오프되 있을 것이다. 우리는 우리가 원하는 것만 켜면 된다. 이제 이 옵션들이 뭘 하는 놈들인지 보자.
Seperate by Thread — 많이사용된스레드순서로프로세스를보여준다.
Invert Call Tree — 스택을뒤집어서보여준다. 가끔유용하게쓰인다.
Hide System Libraries — 시스템라이브러리프로세스는숨기고당신의코드만 보여준다.
Flatten Recursion — 하나의개체에서재귀로호출된콜들을보여준다.
Top Functions — 함수호출이그함수로부터불려진함수에의해추가적인시간이쓰이는지시간순으로보여준다. 이기능은무거운메소드가어떤건지찾는데도움을준다.
필자는프로파일링할때대체로모두체크하고정보를얻는데, 이게꽤유용하다. 필터링옵션선택한뒤 Call Tree를보면, 당신의앱에메소드가 CPU를얼마나사용하는지쉽게확인할 수 있다.
이제 CPU 입장에서무거운메소드들리스트를 모니터링 하면서앱의정확한지점을최적화시켜볼수있다. 몇몇은건드리기힘들수도있지만최적화가능한것들도많이있을것이다. 필자는앱최적화를어떻게하는지에대해서는깊게보지는않을것이지만여기몇몇생각해볼만한것들이있다:
다음으로프로파일링할것은메모리프로파일링이다. 이것은직접적인 이슈가아닐때가많아서 iOS 개발에서는그냥지나쳐버리기쉬운것중하나이다. 만일당신앱에메모리누수가발생하고있는데유저가지속적으로사용한다면메모리는점점꽉 찬 뒤,결국아웃오브메모리상황이 발생하여앱이꺼지게될것이다. 이런상황은당신의앱한테만안좋은게아니라유저의디바이스 입장에서메모리부족으로다른앱들에게도안 좋은영향을끼치게될것이다. 그럼 이제 Instruments를이용하여어떻게메모리누수를방지를할수있는지, 그런상황에서어떻게고칠수있는지알아보자.
시간이지남에따라메모리사용량이점점증가하는걸원치않을것이다.
아래와같은상황은원치않는다.
아래와같은상황을원한다.
가장쉽게메모리사용량을확인하는방법은 Xcode 안에 있는 ‘Debug Navigator’에서보는것이다. ‘Memory’ 패널을선택하고실시간메모리사용량을볼수있다. 여기서는메모리사용량이계속증가하거나한번도낮아지지 않는문제와같은문제들을바로확인하는데도움을준다.
메모리사용량을더자세히보기위해 ‘Profile in Instruments’를누르면이세션에서 transfer할건지새로하나만들면서 (Instruments를)restart할건지물어볼것이다. restart를누른다. 필자는 transfer을눌러서뭔가정보를손실하거나그런좋지않은경험이많다. 그러면 Allocations and Leaks 템플릿을가지는 Instruments가열리고이것은모든메모리할당과모든발생하는잠재적인누수를눈으로볼수있다.
이번에도누수부분을깊게들어가진않겠지만, 메모리가할당은됬으나해제가되지않는것들을 보기위해특정시간 간격 안에서스냅샷을찍어볼것이다. 메모리 누수는 Objective-C나 C 라이브러리 같은 것을 사용하면 종종 나타난다. 웬만하면 ‘Analyze’ 빌드옵션을 사용하여찾을수있으나, 간혹이 Analyze 툴이아무것도찾지못할때도있다. 반면 Swift에서작업하고있으면 Objective-C를사용할때보다누수를적게겪을 것이다.
메모리프로파일러를쓰면서내가찾은유용한기능은 두가지이다.첫째로각순서가이후에동시에이벤트의순서를수행한다는것. 둘째로메모리 마킹을생성하는것이다. 이걸 사용하면당신은각스냅샷사이에메모리의변화를분석할수있다. 오른편 ‘Display Settings’ 섹션위에있는 ’Mark Generation’ 버튼을누르면바로 메모리 스냅샷을 찍을 수 있다. 진행되는 동안마크를만드는걸까먹었어도언제든지마크를추가한뒤에단지클릭하고표시를상단에놓고움직이면원하는지점으로마크를이동시킬수있다. 그러고나서 ‘Allocation Type’을 ‘All Heap Allocations’으로바꾸면, 우리가 손대기 힘든 시스템의 정보들은 숨기고실증가량에따라서정렬해준다. 이제당신은범위동안의메모리사용량을보기쉽게확인할수있을것이나, 이것은실제당신이생성한오브젝트자체를확인하기는좀어려울것이다.
이제메모리할당에따른리스트는가지고있으니,이것이제대로나타나있는지확인하야한다. 사실초기에표시되는데이터들은좀쓸모가없다. 만약 Swift를사용하고있다면, 모든 Swift 객체들의이름앞에앱이름이붙을것이고거기서앱이름으로필터링하면당신의객체를찾을수있다. 반면 Objective-C를사용하고있다면객체를찾아내는데좀특이한방법을사용한다. 당신이찾고싶은것의이름을알고있을것이다. 당신의파일이나객체 이름에접두로어떤특정이름을붙인다면찾을수있다. 예를들어당신의모든 view controller들이 *ViewController와같은이름을가진다면 ‘ViewController’로검색해도찾을수있을것이다.
당신의객체에서 다른 객체를참조하고있어서 그 다른 객체가 메모리 해제되지 못하는지일일히확인하기 좀 힘들다면, Instruments를사용하여더많은객체들의자료를얻을수있을것이다. Instruments에서 객체옆에작은화살표를누르면, 그객체의모든할당된객체들을보여줄것이며누가생성하였는지까지도 나온다. 그다음또작은화살표를누르면 retain과 release count 정보까지얻을수있다. 만일 count가 0이되지않는다면메모리해제가되지않았다는뜻이다. 여기서팁을주자면 ‘Responsible Library’는당신코드이니유심히보고 ‘libsystem_blocks’도유심히봐라. 반대로 ‘UIKit’은스킵해도된다. 필터링하기위해이아이템들을검색박스에쳐볼수있다. 그러고나면 ‘ Extended Detail’이보이고이것들이어떻게돌아가고있는지스택 trace가보이게될것이다.
왜사용하기쉬워야하나? 가장좋은코드가뭔지는한번언급해볼가치가있다: 하나도작성하지않은코드이다. 따라서적은 양의 코드는 버그가 적다. 게으른 개발자 말을 빌려 적은 코드를 작성 하기를 갈망하며 이것은 코드를 설명해야하면 안된다. 또한 당신이 눈을 감고 허우적대며 유지보수하는 솔루션을 원치도 않을 것이다.
Cocoa MVC는 View Controller를덩어리를작성하도록만들어버린다. 그이유는View들의 라이프 사이클 안에서 뒤엉키는데 그것들을 분리해내기가 어렵기 때문이라고 말한다. 너가 Model에 *비지니스 로직이나 데이터 변환같은 것을 없애는 능력을 가졌을 지라도 대부분의 View에서 반응하면 액션을 Controller로 보내게 될것이다. 뷰 컨트롤러는 결국 모든 것의 델리게이트(delegate)나 데이터소스(data source)가 될테고, 종종 네트워크 요청과같은 처리도 하고 있을지 모른다.
이런종류의코드를얼마나많이보았는가:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
사실 MVP 입장에서는, UIViewController의자식클래스에Presenter가아닌View들이있다. 이러한구분은좋은테스트용이함을제공하지만, 수작업의데이터나이벤트**바인딩을따로만들어야하기때문에개발속도에대한비용도따라온다. 아래예제에서확인할수있다:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
추가로 MVP의 Supervising버전에서처럼binding이있다; 그러나여기서는View와Model의관계가아닌View와View Model사이의관계이다.
그래서실제 iOS에서View Model이뭘 의미할까? 그것은기본적으로 UIKit인데그로부터 View의독립된표현이거나상태이다. View Model은 Model에서 변경을 호출하고 Model 자체를 갱신한다. 따라서 View나 View Model 사이에서 바인딩을 하며, 적절히 처음것이 갱신된다.
Bindings(바인딩)
MVP 파트에서 간당하게 언급한적이 있다. 그러나 여기서 좀 더 이야기 해보자. 바인딩은 OS X 개발을 위한 박스(역자주: 프레임워크나 툴을 말하는듯 합니다)에서 나왔으나 iOS 툴박스에서는 보지못한다. 물론 KVO나 notification을 가지고 있긴 하지만 그것이 바인딩만큼 편리하지는 않다.
사실 요즘엔 MVVM을 들으면 바로 ReactiveCocoa를 말하기도하며, 반대도 그렇다(역자주: 뭐라고??????). 비록 간단한 바인딩으로 MVVM을 만드는게 가능하기는 하나 ReactiveCocoa (혹은 siblings)으로는 최고의 MVVM을 만들수 있게 해준다.
Reactive 프레임워크에는 쓰디쓴 진실이 하나 있다: 큰 책임엔 큰 에너지가 필요하다. Reactive를 사용하게되면 굉장히 혼잡해지기 쉬워진다. 다른말로 설명하자면, 문제가 하나 생기면 앱을 디버깅하는데 시간이 굉장히 많이 걸리며, 아래와 같은 콜 스택을 보게 될것이다.
Reactive Debugging
우리의예제에서는 FRF 프레임워크나 KVO까지도배보다배꼽이더큰식이다. 그대신에showGreeting메소드를이용하여 갱신하기 위한 View Model을명백하게물어볼것이고greetingDidChange콜백함수를위해작은프로퍼티를사용할것이다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Distribution—우리의작은예제에서는명료하게나타나지않았지만, 사실 MVVM의View는 MVP의View보다더책임이많다. 왜냐면두번째것이Presenter로포워드(forward)하고그자신를갱신하지는않은 그 때, 바인딩을세팅함으로써View Model에서처음것의상태를갱신한다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
퍼포먼스 측정(이해 "측정"으로 줄여서 말하겠습니다)에있어서다음으로중요한것은 UI의반응성이다. 터치헨들링은메인쓰레드에서발생한다. 메인쓰레드에서시간이좀걸리는작업을하면, 앱은버벅거리게될것이다.
몇몇동작은 CPU를사용하지않는주제에시간을잡아먹기도한다. 만약메인쓰레드에서동기화콜을불렀다면, 이콜이얼마나시간이걸리는지알아내는것이 문제를 해결하는 방법일것이다.
시간을측정하기위해로그를찍어볼수도 있다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters