'Swift와 iOS'에 해당하는 글 94건


이전 포스팅에서 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

,

두 프로젝트를 동시에 진행하면서 앱 아키텍처에 관한 중요한 경험을 얻을 수 있었다. 내가 공부했던 것이나 생각했던 것에서 적용해보고 싶은 개념을 두 프로젝트에 적용시켜 진행했다. 그중 하나는 최근에 내가 공부하기도 했고 가장 의미있다고 생각되는, 네트워크 레이어를 어떻게 만드는지에 관한 이야기이다.

요즘 모바일 앱은 클라이언트-서버 지향이고, 앱이 크든 작든 앱 어디에나 네트워크 레이어를 사용한다. 나는 수 많은 네트워크 레이어 구현을 보았지만, 다들 뭔가 단점들이 있었다. 내가 제일 마지막으로 만든것에서 단점이 아예 없을거라 생각하진 않지만 두 프로젝트에서 굉장히 잘 동작하는듯 보였고, 현재까지도 작업을 하고 있다. 또한 테스트 커버리지가 거의 100%에 이른다.

이 글에서는 그렇게 복잡한것 까지는 다루지 않을 것인데, 인코딩된 요청을 JSON으로 보내는 한 백엔드의 네트워크 레이어만 다룰 것이다. 이 레이어는 나중에 AWS와 함께 이야기해보고 거기에 몇몇 파일도 보내볼 것이지만 기능적으로 확장하기 쉬울 것임을 확신한다.

프로세스를 생각해보자
이 레이어를 만들기 전에 던졌던 질문들이다.
  • 백엔드 url에관한 정보는 코드 어디에 놔둘까?
  • url 끝부분은 어느 코드에서 알아야할까?
  • 요청을 어떻게 만드는지는 코드 어디에서 알아야 할까?
  • 요청을 보내기 위해 준비해야하는 파라미터는 코드 어디에 둘까?
  • 인증(authentication) 토큰은 어디에 저장해 둘까?
  • 어떻게 요청(request)을 실행할까?
  • 언제 그리고 어디서 요청을 실행할까?
  • 요청 취소에 대해서도 고려해야할까?
  • 잘못된 백엔드 응답이나 백엔드 버그도 고려해야할까?
  • 써드파티 라이브러리를 사용해야하나? 어떤것을 사용할까?
  • 코어데이터도 처리해야할 것이 있나?
  • 이 솔루션을 어떻게 테스트해볼까?

백엔드 url을 저장하기
먼저, 어디에 백엔드 url을 저장해 두어야할까? 어떻게 우리 시스템에서 어떤 다른 시스템에 요청을 날릴지 알까? 나는 BackendConfiguration 클래스를 만들어 그 정보(url)를 담아둔다.

테스트하기도 쉽고, 구성하기도 쉽다. shared라는 스태틱 변수에 할당해놓고 파라미터로 전달할 필요 없이 네트워크 레이어 어디에서나 접근할 수 있다.

끝부분(Endpoints)
이 토픽은 내가 준비-시작(ready-to-go) 솔루션을 찾기 전에 경험했던 것이다. NSURLSession을 구성하는 동안 끝부분을 하드코딩하려 했고, url의 끝부분을 알고 쉽게 객체로 만들 수 있거나 주입될 수 있는 더미의 Resource스러운 오브젝트를 시도했다. 그러나 내가 찾던 해답은 아니었다. 

결국 *Request라는 객체를 만들기로 했는데, 이 객체는 다음과 같은 것들을 가진다. 어떤 메소드를 사용하는지, GET, POST, PUT 혹은 다른 어떤것인지, 요청의 바디(body)가 어떻게 구성되는지, 헤더에는 무엇을 담아 보내는지 이 객체에 담겨있다.

프로토콜로 구현된 이 클래스는 요청을 만들때 필요한 기본 정보를 제공한다. NetworkService.Method는 GET, POST, PUT, DELETE를 표현하는 열거형이다.

url 뒷부분이 저장된 예제 요청은 아래와같이 생겼다.

헤더 어디에도 딕셔너리를 생성하지 않으려고 BackendAPIRequest를 위한 extension을 정의할 수 있다.

*Request 클래스는 요청에 성공하기위해 필요한 모든 파라미터를 받는다. 적어도 당신이 필요로한 모든 파라미터는 보내야하고, 그렇지 않으면 요청 객체는 생성할 수 없다.

url 끝부분을 선언하는 것은 간단하다. 만약 끝부분에 id를 포함해햐한다면 아주 쉽게 추가할 수 있다. 왜냐하면 프로퍼티로 저장해둔 id를 가지고 있기 때문이다.

요청의 메소드는 절때 바뀌지 않으며 파라미터 바디나 헤더가 쉽게 구성되고 쉽게 수장할 수 있다. 테스트하기에 모든것이 쉬워진다.

요청을 실행하기

백엔드와 소통하려면 써드파티 프레임워크가 필요한가?

사람들이 AFNetworking(Objective-C)나 Alamofire(Swift)를 사용하는 것을 보았다. 나도 이것을 오랫동안 써왔으나 어떨때는 사용하지 않았다. NSURLSession이라는 것이 있는데, 이것이 굉장히 잘 되있기 때문에 나는 여러분이 굳이 써드파티 프레임워크를 사용하지 않아도 된다고 생각한다. 내 생각에는 이런 의존성이 여러분의 앱 구조를 더 복잡하게 만든다.

현재 솔루션은 NetworkService와 BackendService의 두 클래스로 구성된다.
  • NetworkService — 내부적으로 NSURLSession과 구성되어 HTTP 요청을 실행할 수 있게 한다. 모든 네트워크 서비스는 한번에 하나만 요청할 수 있고, 요청을 취소할 수도 있으며(큰 장점이다), 성공하거나 실패한 응답을 위한 콜백을 가지고 있는다.
  • BackendService — (좋은 이름은 아니지만 꽤 잘 들어 맞는다)백엔드와 관련있는 요청(위에서 설명한 *Request 객체)을 받는 클래스이다. NetworkService를 사용한다. 내가 현재 사용하는 버전에서는 응답 데이터를 NSJSONSerializer를 이용하여 json으로 만든다.

위에서 볼 수 있듯 BackendService는 인증 토큰을 헤더에 세팅할 수 있다. BackendAuth 객체는 NSUserDefault에 토큰을 저장하는 간단한 저장소이다. 필요에따라 토큰을 키체인에 저장할 수 있다.

BackendService는 request(_:success:failure:) 메소드의 파라미터로 BackendAPIRequest를 받고 request 객체로부터 필요한 정보를 얻어낸다. 이렇게 하면 캡슐화면에서도 좋고 백엔드 서비스도 꺼네온것만 사용한다.

NetworkService, BackendService, BackendAuth 모두 테스트하거나 유지보수하기 쉽다.

요청을 큐하기
어떤 방식으로 네트워크 요청을 날려야할까? 한번에 많은 네트워크 요청을 날려야 한다면 어떻게 할까? 일반적으로 요청의 성공과 실패를 처리하려면 어떻게 할까? 이 파트에서 위 질문들을 해결할 수 있을 것이다.

네트워크 요청을 실행하는 NSOperation과 NSOperationQueue를 사용하자.

나는 NSOperation으로 서브클래스를 만들고 asynchronous 프로퍼티를 오버라이드하여 true를 반환하게 한다.

다음으로, 네트워크 콜을 실행시키기위해 BackendService를 사용하고 싶으므로 NetworkOperation을 상속받은 ServiceOperation을 하나 만든다.

이 클래스 내부에는 BackendService를 생성하므로 이제 모든 서브클래스마다 이것을 생성할 필요가 없다.

이제 로그인 동작이 어떻게 구현되는지 보자.

서비스는 이 오퍼레이션의 초기화때 만들어놓은 요청을 start 메소드에서 실행시킨다. handleSuccess와 handleFailure 메소드는 서비스의 request(_:success:failure:) 메소드에 콜백으로 전달된다. 내 생각엔 이렇게하면 코드가 더 깔끔해지고 가독성도 좋아진다.

오퍼레이션들은 싱글톤 오브젝트인 NetworkQueue로 전달되며 모든 오퍼레이션이 이 큐에 들어갈 수 있다. 이제 나는 가능한 간단하게 유지한다.

한 곳에 오퍼링션 실행을 모아두면 어떤 이점이 있을까?
  • 모든 네트워크 오퍼레이션을 간편하게 취소할 수 있다.
  • 이미지를 다운받거나 열악한 네트워크 환경에서 많은 데이터를 소모해야하지만 앱 동작에는 크게 상관없는 다른 오퍼레이션들을 취소할 수 있다.
  • 큐를 필요한 순서대로 실행할 수 있고 빨리 답변 올 수 있는 것부터 실행할 수 있다.

코어데이터와 함께 작업하기
사실 이것 때문에 이 글의 발행이 늦어졌다. 이전 버전에서는 네트워크 레이어 오퍼레이션이 코어데이터 객체를 반환했었다. 응답을 받고 파싱한 뒤, 또다시 코어데이터 객체로 변환시켰다. 이 방법은 그다지 이상적이지 않았다.
  • 오퍼레이션에서 어떤 코어데이터인지 알고 잇어야했다. 왜냐하면 프레임워크를 분리하기 위해 떼어놓은 모델을 가지고 있고 네티워크 레이어 역시 프레임워크로부터 분리되 있었다. 네트워크 프레임워크는 모델 프레임워크에대해 알고 있어야 했다.
  • 각 오퍼레이션은 어떤 컨텍스트에서 동작하는지 알아야 했으므로 NSManagedObjectContext 파라미터를 받아야 했다.
  • 응답을 받고 성공(success) 블럭을 호출할 때마다 먼저 컨텍스트에 객체를 찾으려 하거나 디스크에 객체를 패치하기위해 디스크 검색을 해야만 했다. 내 생각엔 이 점이 매우 큰 단점이었고, 아마 당신도 항상 코어데이터 객체를 생성하고 싶지는 않았을 것이다.

그래서 나는 네트워크 레이어를 코어데이터로부터 완전히 떼어냈고, 응답을 파싱하여 오브젝트를 만드는 중간 레이어를 만들었다.
  • 이렇게 파싱하고 오브젝트를 생성하면 디스크에 접근하지 않으므로 빠르다.
  • 오퍼레이션에 NSManagedObjectContext를 전달할 필요가 없다.
  • 성공 블럭에서 파싱된 아이템으로 코어데이터 객체를 갱신할 수도 있고, 오퍼레이션을 생성하는 곳 어디에든 있을 수 있는 코어데이터 객체를 참조할 수 있다. — 오퍼레이션이 큐에 추가될 때의 내 경우이다.

응답을 맵핑하기
유용한 아이템을 위해 JSON을 파싱하는 로직과 매핑하는 로직을 분리해주는 응답 맵퍼(response mapper)가 있다.

우리는 두가지 타입으로 파서를 구별할 수 있다. 첫번재는 특정 타입의 한 객체를 반환한다. 두번째는 어떤 항목의 배열을 파싱하는 파서이다.

먼저 모든 항복에 해당하는 일반적인 프로토콜을 선언하자.

이제 모델과 맵핑하는 몇 객체이다.

그리고 파싱하다가 에러가 발생시 던지는(throw) 에러타입을 선언하자.
  • Invalid — 파싱된 json이 nil이고, nil을 반환하면 안될때나, json이 하나의 객체가 아니라 객체의 배열일때 던지는 에러타입이다.
  • MissingAttribute — 이름이 의미하는 바와 같다. json에서 키를 잃어버리거나 파싱 후에 값이 nil이면 안되는데 nil일때 던지는 에러타입이다.

ResponseMapper는 이렇게 생겼을 것이다.

ResponseMapper는 백엔드로부터 받은 obj(우리의 경우 JSON이다)와 parse라는 메소드를 받는데, parse는 obj을 이용해 ParsedItem을 따르는 A 객체를 반환한다.

이제 우리가 세부적으로 구현한 제네릭 매퍼를 만든다. 아래 매퍼는 로그인 오퍼레이션에대한 응답을 파싱하는데 사용되는 매퍼이다.

ResponseMapperProtocol은 세부적인 매퍼에의해 구현된 프로토콜로서 메소드를 공유하여 응답을 파싱한다.

그러면 이 매퍼를 오퍼레이션의 성공 블락에서 사용하고, 이것을 딕셔너리 대신에 특정 타입의 객체로 사용할 수 있다. 이전보다 훨씬 사용하기 쉽고 테스트하기도 쉽다.

마지막으로 배열을 파싱하기위한 응답 매퍼이다.

모든것이 정상적으로 파싱되면 매핑할 함수를 받아 항목의 배열을 반환한다. 매퍼의 결과가 당신이 예상한 것에 따라 한 아이템이 파싱되지 않을 경우 에러를 던질 수도 있고, 문제가 있을 때는 빈 배열을 반환할 수도 있다. 매퍼는 백엔드로부터 받은 응답인 obj가 JSON 요소의 배열이라 예상한다.

이 도표는 네트워크 레이어의 구조를 보여준다.




예제 프로젝트
여러분은 내 깃헙에서 예제 프로젝트를 확인해 볼 수 있다. 프로젝트에서 사용된 백엔드 url은 모두 가짜이며 모든 요청은 실패할 것이다. 나는 여러분에게 네트워크 레이어의 파운데이션이 어떻게 생겼는지 보여주기만을 위해 이것을 만들었다.

요약
나는 이러한 네트워크 레이어 방식이 굉장히 유용하고 간편하며 작업에 편리하다는 것을 깨닭았다.

  • 가장 큰 이점은 비슷한 설계의 다른 새 오퍼레이션을 쉽게 추가할 수 있고, 이 레이어가 코어데이터에대해 전혀 모른다는 점이다.
  • 큰 노력없이 코드 커버리지를 거의 100%에 근접하도록 만들 수 있다. 완전 어려운 케이스를 어떻게 커버할지 생각할 필요도 없다. 어디에도 케이스가 없기 때문이다(원문: because there is no such cases at all).
  • 이 네트워크 레이어의 핵심은 비슷한 복잡성을 가진 다른 앱에서도 다시 사용할 수 있다는 점이다.



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

,

델리게이트도 괜찮지만, 더 나은 방법이 있다.

당신은 iOS앱을 만들고 있는 개발자라 가정하자. 당신은 당신이 할 수 있는 최선의 견고한(SOLID) 아키텍처로 앱의 구조를 만들어 놓았다. 앱은 모델, 네트워크 레이어, UI 레이어 또는 그것을 돕는 것들로서 구성된다. 이 레이어들 사이에 데이터를 주고 받을때 책에서는 델리게이션을 이용해라고 알려준다. 실제로 iOS 개발에서 일반적으로 사용되는 유용한 패턴이기도 하다.

델리게이션은 간단히 말하자면, 어떤것의 변화로부터 알림받기 원할때 그 어떤것에 알림받기 원하는 대상을 등록해놓는 방식의 패턴이다. 이렇게하면 그 어떤것으로부터 반응(react)할 수 있다. 예를들면 ViewController가 네트워크 서비스에게 말을 걸어서 (ViewController를 네트워크 서비스 델리게이트로 등록해서) 어떤 요청이 완료될때 자신에게 알려달라고 한다. 이때 ViewController를 네트워크 서비스의 델리게이트로 만들어 가능하게 된다. 네트워크 서비스는 요청이 완료되었을 때 델리게이션 메소드를 호출 할 것이다.
델리게이트 참조를 보내는 것은 괜찮은 방법이고 기능적으로도 아무 문제가 없다. 그러나 Swift에서는 더 나은 방법이 있고, 왜 이 방법이 더 나은지 설명해 보겠다.


델리게이션을 위해 콜백을 사용
콜백은 델리게이트 패턴과 비슷한 기능을 가진다. 어떤 일이 발생할때 다른 오브젝트가 알게 해주고, 데이터를 전달하는 기능이다.

델리게이트 패턴과 다른점은, 응답받고 싶은 객체 자체를 넘겨주는 것 대신에 함수만을 넘겨준다. 함수는 Swift에서 클래스의 첫번째 요소이다. 따라서 함수를 프로퍼티로 가지고 있을수도 있다.
MyClass는 이제 myFunction 이라는 프로퍼티를 가지는데, 어딘가에서 호출할 수도 있고, 누구나 값을 바꿀 수도 있다(Swift에서 정의된 규칙에의해 프로퍼티는 디폴트로 internal이 된다). 이것이 델리게이션 대신 콜백을 사용하는 기본 아이디어이다. 아래 예제는 위 예제에서 델리게이트 대신 콜백으로 대체한 것이다:
콜백을 사용하는 다른 상황은 데이터가 바뀔때 알림을 받고 싶을 경우이다. 프로퍼티 옵저버에서 콜백을 호출함으로서 가능하다:
콜백에대한 간단한 노트 : 델리게이트에서는 리테인(retain) 사이클을 멈추기위해 weak 프로퍼티로 만들어야하는 것처럼, 여러분도 클로저 안에서는 self를 weak 변수로 해두어야한다.

그래서 왜 콜백이 더 나은가?
1. 분리됨(Decoupling)
델리게이트는 코드를 분리하는 경향이 있다. 프로토콜로 구현하는 한 NetworkService에게 누가 그것의 델리게이트인지 알 필요가 없다. 그러나 델리게이트가 프로토콜 구현을 가지고 있고, @objc 프로토콜 대신 Swift를 사용한다면, 델리게이트는 프로톸콜에서 모든 메소드 구현을 가진다.(따라서 옵셔널 프로토콜 일치도 필요없다)

다르게 말해보면 콜백을 사용할 때, NetworkService는 메소드를 호출하기위한 델리게이트를 가지고 있을 필요가 없고 누군가가 이 메소드를 구현할 것이라는 것을 알고 있으면 된다. 메소드가 호출되는 시점만 관리하면 되고, 이 메소드가 어떻게 구현되있을지는 알 필요가 없는 것이다.

2. 다중 델리게이션
요청이 끝나고 ViewController에 알림을 주고 싶은데, 그때 로그를 남기는 클래스나 통계를 남기는 클래스를 넣고 싶을 수 있다.

델리게이트로 구현하면, 델리게이트의 배열을 가지고 있어야 할 것이다. 아니면 세개의 서로 다른 프로토콜을 가지는 델리게이트 프로퍼티를 가질 것이다!

그러나 콜백으로 구현한다면 함수의 배열을 선언하고(Swift의 이런 점이 좋다!) 뭔가 처리가 끝날때 각각 호출하면 된다. 따라서 리테인 사이클의 위험을 감수하거나 어마어마한 양의 코드로 작성되거나 하는 수많은 오브젝트와 프로토콜이 필요없어진다.

3. 일을 분리하기에 더 명확하다.
내가 생각하는 델리게이트와 콜백의 차이는 이렇다. 델리게이트는 NetworkService가 델리케이트에게 "이봐, 나 갱신됐어!"라고 말하는 반면 콜백은 델리게이트가 NetworkService를 응시하고 바라보고 있는 느낌이다.

실제로 작은 차이 같지만, 후자의 방법으로 생각하면 NetworkService가 자신의 기능을 하지 못하고 화면 표시의 기능으로 변질되는 그런 것을 방지해주는 패턴으로서는 크게 도움이 될 것이다!

4. 테스트하기 쉬움!
유닛테스트와 함께 구현하면 항상 코드베이스가 두배로 늘어난다고 느낄것이다. 그 이유는 앱의 모든 델리게이트를 포함하여 매 프로토콜마다 목(mock)을 만들어야하기 때문이다.

콜백으로 구현하면 어떠한 델리게이트에 목을 할 필요 없을 뿐만 아니라 각 테스트에서 원하는 어떠한 콜백이든 사용할 수 있게 해준다.

한 테스트에서 콜백이 제대로 호출되는지 호출는지 테스트해보고, 다른 테스트에서 그것이 호출될 때 옳바른 결과를 내는지 테스트 할 수 있다. 그리고 어떠한것도 someFuncDidGetCalled 불리언(boolean)이나 비슷한 프로퍼티를 가진 복잡하게 목(mocked)된 델리게이트를 필요로 하지 않는다.

나는 개인적으로 콜백이 코드와 테스트를 명확하게 만들어주고, 더 Swift스럽게 데이터를 주고 받는 방법이라 생각한다. 여러분이 오늘 뭔가 새로운 것을 배웠기를 바란다! 



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

,

6개월전 우리는 PlanGrid iOS앱에 Flux 아키텍처를 적용시키기 시작했다. 이 포스팅에서는 우리가 왜 전통적인 MVC에서 Flux로 갈아타게 되었는지 이야기해보고, 지금까지 겪은 경험을 공유하고자한다.

실제 제품에 코드와 함께 이야기함으로서 나는 Flux 구현의 큼직한 부분들 위주로 설명해볼 것이다. 만약 당신이 단지 고수준의 결론만 알고 싶다면, 포스팅 중간 부분은 스킵해버려도 좋다.

왜 우리가 MVC로부터 갈아타게 되었을까?
어떤 맥락속에서 우리가 Flux를 결정하게 되었는지 설명하기 위해, PlanGrid 앱이 해결해야할 과제들을 먼저 이야기 해보고 싶다. 그 중 몇몇은 엔터프라이즈 소프트웨어에 의존적이고, 나머지 대부분 iOS 앱에 적용시킬 수 있어야했다.

우리는 모든 상태를 가지고 있어야 한다.
PlanGrid는 꽤 복잡한 iOS 앱이다. 사용자에게 청사진을 보여주고 사용자들이 서로 다른 양식의 주석이나 이슈, 첨부(그리고 특정 산업 지식을 필요로하는 수많은 요소)들을 이용하여 협업할 수 있어야 했다.

또한 이 앱의 중요한 기능은 오프라인이 우선이라는 점이다. 유저들은 인터넷 연결 여부와 상관없이 앱의 모든 기능을 사용할 수 있어야했다. 이 말은 즉, 우리는 그 수많은 데이터와 상태들을 클라이언트에서 관리하고 있어야 했다는 뜻이다. 또한 부분적으로 비즈니스 정책으로서 특정 기능을 따로 실행할 수 있어야 했다(e.g. 특정 유저는 주석을 지울 수 있다던지?).

PlanGrid 앱은 iPad, iPhone 기기 둘 다에서 동작하지만, UI는 테블릿의 큰 화면에 최적화 되어있다. 이 말은 많은 iPhone 앱들과는 다르게 종종 Multiple View Controller를 한 화면에서 보여줘야 했으며, View Controller끼리 상태를 공유해야 했다.

상태 관리의 상태
우리 앱은 상태 관리라는 곳에 상당한 노력을 쏟아붇고 있다. 앱에서의 갱신은 보통 아래의 순서를 따른다.
  1. 로컬 객체에서 상태를 갱신
  2. UI를 갱신
  3. 데이터베이스를 갱신
  4. 네트워크 연결이 가능해지면 서버로 보낼 그 변화를 큐에 넣기
  5. 다른 객체에 상태 변화를 알리기

나중에 또 한번 위의 과정을 담은 우리의 새 아키텍처에대해 포스팅을 할 예정이므로 오늘은 5번째 단계에 대해서만 이야기해보자. 우리는 어떻게 상태를 갱신받아 처리할 수 있을까?

이 질문은 앱 개발시 항상 나오는 질문이다.

PlanGrid를 포함한 대부분의 iOS 엔지니어들은 다음 대답들을 내놓는다:
  • Delegation
  • KVO
  • NSNotificationCenter
  • Callback Blocks
  • 소스의 신뢰로서 DB를 이용하기
위 접근법들은 수많은 시나리오에 걸쳐 검증되었을 것이다. 그러나 수년에 걸쳐 바뀔 수 있는 커다란 코드베이스에서 수많은 옵션들이 있다면 이것은 매우 부적합하다고 할 수 있을 것이다.

자유는 위험하다.
원리의 MVC는 데이터와 데이터 표현을 분리하는 것만을 추구했다. 다른 구조적인 가이드가 부족했으므로, 나머지 모든 것들이 개발자 개인에게 떠넘겨졌다.

오랜 시간동안 (다른 iOS 앱들 처럼) PlanGrid 앱도 상태 관리를 위한 패턴을 정하지 못해왔었다.

델리게이션이나 블럭과 같은 현존하는 수많은 현존하는 상태 관리 도구는 컴포넌트 사이에 강한 의존성을 만드는 경향이 있다 ― 두 View Controller가 서로 상태 갱신을 공유하고자하면 바로 단단히 엮여버린다.

KVO나 Notofication과 같은 다른 도구들은 눈에 보이지 않는 의존성을 만들어낸다. 거대한 코드베이스의경우 그것들을 사용하면 더더욱 예상치 못한 사이드 이팩트가 발생할 수 있고, 많은 코드 수정을 해야할지도 모른다.

이러한 수많은 구조적인 이슈는 작은 모순점에서 시작되어 시간이 점차 흐르면 심각한 문제를 초례한다. 반면 철저한 코드리뷰와 스타일 가이드 만이 이 문제를 잘 해결할 수 있다. 잘 정의된 패턴이 적용된다면 미연에 그 문제를 인지하기 훨씬 쉽다.

상태 관리를 위한 구조적인 패턴
PlanGrid 앱을 리팩토링하면서 우리의 가장 중대한 목표는 깨끗한 패턴들과 최고의 습관을 만들어 놓는 것이었다. 이렇게하면 미래에 훨씬 모순 없는 방식으로 코드를 짤 수 있고, 새로운 엔지니어가 투입 되었을 때도 매우 효율적이다.

이 앱에서 상태 관리는 가장 큰 복잡함을 제공하는 원인 중 하나였고, 우리는 앞을 계속 사용할 수 있게 완전히 새로운 패턴을 정의하기로 마음먹었다.

페이스북에서 처음 Flux 패턴을 소개했을때, 그들이 말한 문제점과 우리가 현재 코드베이스에서 느낀 수많은 고통들이 강하게 매칭되었다:
  • 예측불가능하고, 순차적으로(cascading)처럼 상태가 갱신됨
  • 컴포넌트 사이에 의존성을 이해하기 쉽지 않음
  • 정보의 흐름이 엉켜있음
  • 소스의 신뢰가 불분명함
Flux는 우리가 경험하고 있던 많은 이슈를 해결하기에 적합해 보였다.

Flux로 들어가기전에 가벼운 설명
Flux는 페이스북의 웹 어플리케이션 클라이언트단에서 사용하는 경량의 아키텍처 패턴이다. 비록 참조하여 구현하였지만, 페이스북은 Flux패턴의 아이디어가 특수한 이 구현보다 더 많이 연관되있다고 강조했다.

서로 다른 Flux 컴포넌트를 보여주는 다이어그램과 함께 묘사할 수 있다:


Flux 아키텍처에서의 store는 앱의 특정 부분을 위한 정확한 단일 소스이다. store에서 상태가 업데이트되는 즉시 store를 구독하는 모든 view에 change event를 보낸다. 그 viewstore에의해서만 호출되는 유일한 인터페이스를 통해 갱신되었다는 소식을 받는다.

상태 업데이트는 action을 통해 일어날 수 있다.

action은 상태 변화를 하게 해주는 트리거지만 스스로 상태변화를 구현해놓지는 않는다. 상태 변화를 원하는 모든 컴포넌트들이 글로벌 dispatcheraction을 던진다. 이 storedispatcher와 함께 등록하고 그것들이  어디 action에 필요한지 알아내준다. action이 dispatch되면 바로 관련된 store들이  이것을 받는다.

action에 응답하는 동안 몇몇 store들은 그들의 상태를 갱신하고 새로운 상태를 view에게 알릴 것이다.

Flux 아키텍처는 위 다이어그램에서 보듯 단방향의 데이터 흐름을 행한다. 또한 엄격한 분리가 가능하다:
  • view는 오직 store로부터 데이터를 받는다. store가 갱신되면 view에 있는 메소드를 이용해 불러낸다.
  • view는 오직 action을 dispatch 함으로서 상태를 바꿀 수 있다. action은 단지 의도(intent)를 표현하는 역할이고 비즈니스 로직은 view로부터 숨겨져있기 때문이다.
  • store는 action을 받았을 때만 그 상태를 갱신한다.
이러한 제약들 덕에 새 기능을 설계하고 개발하며 디버깅하기 쉽게 만들어준다.

iOS를 위한 PlanGrid에서의 Flux
PlanGrid iOS 앱에서 우린 Flux를 약간 벗어나 구현했다. 우리는 각 store가 Observable 상태 프로퍼티를 가지고 있다. 기존 Flux 구현과는 다르게, store가 갱신될 때 change event를 보내지 않았다. 대신에 view가 store의 상태 프로퍼티를 Observe하고 있다. view가 상태 변화를 Observe하면 그들 스스로 변화를 감지하며 갱신까지 한다.


이것은 Flux 참조 구현에서 굉장히 미묘한 변경이지만 다음 섹션에서 위해 유용하게 쓰일 것 이다.

Flux 아키텍처 기반을 이해하면서, 이제 구체적인 구현이나 PlanGrid 앱에 Flux를 적용시키는 동안 필요했던 질문의 답변들을 한번 살펴보자.

store의 번주는 어디까지인가?
각 개별 store의 범주(scope)는 Flux 패턴을 처음 사용할 때 가장 먼저 떠오르는 질문이다.

페이스북이 Flux 패턴을 발표하고부터, 커뮤니티에의해 다른 변화들이 개발되어왔다. Redux는 그 중 하나인데, 각 어플리케이션당 오직 하나의 store만 가지도록 함으로서 Flux 패턴에서 번갈아가며 한 store를 사용한다. 이 store는 앱의 모든 상태를 가지고 있는다(수많은 다른, 사소한, 이 포스트 영역을 벗어난 그런것들).

Redux는 단일 store 아이디어로 수많은 앱의 아키텍처를 단순하게 해줌으로서 많은 인기를 얻고 있다. 그러나 다중 store를 사용하는 기존의 Flux에서는 조금 다른데, 특정 view를 그려야(reder)하기 때문에 다른 store에서 저장되 있는 상태를 합칠 필요가 있고, 이렇게 해야 앱이 돌아갈 수 있다. 이런 접근법은 곧바로 Flux패턴이 풀어야할 문제로 다시 떠오를 수 있다(다른 컴포넌트들 사이에 복잡한 의존성 같은).

PlanGrid 앱에서는 여전히 Redux 대신 기존의 Flux를 사용하기로 결정했다. 우리는 우리 앱이 얼마나 큰 앱이 될지 예측하지 못했기에, 앱의 모든 상태를 담은 단일 store보다는 다중 store를 선택하였다. 게다가 우리는 가장 작은 inter-store 의존성을 가지는 것을 인지했는데, 이것이 Redux를 대안에서 제외시키게 된 이유가 되었다.

우리는 아직 각 개별 store의 범주를 견고하게 만들어가고 있다.

지금까지도 나는 우리 코드베이스에서 두가지 패턴을 알아냈다:
  • 기능/view 특정 store : 각 View Controller(혹은 View Controller와 가깝게 연관된 각 그룹들)는 그것의 store를 받는다. 이 store는 view에 특화된 상태를 만든다.
  • 상태를 공유하는 store : 우리는 수많은 view들 사이에서 상태가 공유되는데, 이 상태들을 저장하고 관리하는 store를 가진다. 우리는 이 어마어마한 양의 store들을 최소화시키기위해 노력중이다. IssueStore가 그 예시인데, 이것은 현재 선택된 청사진을 볼 수 있는지 없는지에 관한 모든 이슈 상태를 관리한다. 이 이슈들을 화면에 보여주거나 소통하는 수많은 view들은 이 store로부터 정보가 나온다. 이 store의 타입은 필수로 실시간 갱신되는 데이터베이스 쿼리처럼 동작한다.
우리는 현재 상태 store에 공유된 처음 것을 구현하는 과정이고 아직 이 store 타입에서 서로 다른 view의 다중 의존성을 만드는 최고의 방법을 모색중이다.

Flux 패턴을 사용하여 기능을 구현하기
이제 Flux 패턴으로 만드는 세부적인 구현 기능들 안으로 파고 들어가보자.

다음 두 섹션에 걸쳐 예제를 보여주는데, PlanGrid 앱 제품에서의 기능들을 예시로 들 것이다. 그 기능은 사용자가 한 청사진에서 주석들을 필터링할 수 있게 해주는 것이다.


우리가 토론할 이 기능은 스크린샷의 왼편에 나타나있는 popover안에 만들어져있다.

1단계 : 상태를 정의하기
보통 나는 그것의 적절한 상태를 정함으로서 새 기능의 구현을 시작한다. 그 상태는 특정 기능의 표현응ㄹ 그리기위해 UI가 알아야하는 모든것을 나타낸다.

아래 보이는 것처럼 어서 주석 필터 기능을 위한 상태를 둘러보면서 우리 예제 속으로 들어가보자:

이 상태는 여러 필터의 리스트, 현재 선택된 필터 그룹, 어떤 필터가 활성화됬는지 지시하는 boolean 플래그로 구성된다.

이 상태는 정확히 UI에서 요구한 것이다. 필터 리스트는 Table View에 나타난다. 선택된 필터 그룹은 각 개별로 선택된 필터 그룹의 세부사항을 표시/숨김 하기위해 사용된다. 그리고 isFiltering 플래그는 UI에 버튼을 보이게할지 말지 정하는데 필터가 enabled인지 disabled인지에 따라 정해진다.

2단계 : Action을 정의하기
특정 기능을 위한 상태를 정의하고나면, 나는 보통 다음 단계에서 다른 상태 변화를 생각해본다. Flux 아키텍처에서 상태 변화는 action의 모양에 의해 만들어지는데, action은 상태 변화가 의도하는 것을 담고있다. 주석 필터 기능을 위한 action 코드들은 꽤 짧다:

그 기능의 깊은 이해 없이도 이 action이 초기화하는 상태 이동이 어떤 것인지 이해할 수 있을 것이다. Flux 아키텍처의 장점중 하나는 action 리스트는 각 기능들에의해 트리거될 수 있는 모든 상태변화를 한번에 담아낸다는 것이다.


3단계 : store에서 action으로 그 응답을 구현하기
이 단계는 기능의 핵심적인 비즈니스 ㄹ직을 구현하는 단계이다. 나는 개인적으로 이 단계를 TDD를 이용하여 구현하려하고, 나중에 TDD에대해 다시 이야기할 것이다. store의 구현은 아래처럼 요약될 수 있다:
  1. 연관된 모든 action을 dispatcher와 함께 store를 등록한다. 이 예제에선 모든 AnnotationFilteringActions이 될 것이다.
  2. 각 action들별로 호출할 수 있는 핸들러를 만든다.
  3. 핸들러와 함께 필요한 비즈니스 로직을 동작하고 완성에 상태를 갱신한다.

구체적인 예제로서 AnnotationFilterStoretoggleFilterAction을 어떻게 다루는지 확인할 수 있다:
self.annotationFilterService.applyFilter()를 호출 함으로서 시트위에 표시되는 주석들의 필터링을 실제 동작시킨다. 필터링 로직 그 자체는 다소 복잡하나, 일부를 떼어내서 옮겨놓았다.

각 store의 역할은 UI와 관련된 상태 정보를 제공하고 현재 상태를 동일하게 만들어 놓는 것이다. 그러나 이 작업을 위해 모든 비즈니스 로직을 store 안에 다 구현해라는 것은 아니다.

각 action 핸들러의 마지막 작업은 상태를 갱신하는 것이다. _applyFilter() 메소드와 함께, 어떤 필터가 활성화되어있는지 체크하여, 우리는 isFiltering 상태값을 갱신한다.

여기서 특정 store에 대해 인지해야할 중요한 사실이 하나 있다: 추가적인 상태 업데이트를 예상할 수 있다는 점인데, 이 업데이트는 AnnotationFilter에 저장되있는 필터들의 값을 갱신한다. 일반적으로 이것은 store를 어떻게 구현할 것이야는 것지만, 이번 구현은 약간 특별하다.

AnnotationFilterState에 저장된 필터들은 이전에 존재했던 Objective-C 코드와 연결되야 하므로 그들을 새 클래스로 만들기로 했다. 이 클래스는 타입과 store를 참조하고, 주석 필터링 UI는 같은 인스턴스 참조를 공유한다. 즉 store 안에서 필터에 일어나는 모든 변화는 UI의 시각적인 부분과 관계돼있다. 상태 구조체에서 값 타입을 독립적으로 사용함으로서 원래는 이러한 상황을 피하려고 해야한다. ― 그러나 이 포스팅은 실제 세계에서의 Flux 이야기이고 이 특수한 상황에서 좀 더 쉽게 Objective-C를 연결하기 위해 어느정도 타협점을 찾을 수 밖에 없었다.

만약 필터가 값 타입이면, 변화를 관찰한 UI 순서에 따라 우리 상태 프로퍼티에 갱신된 필터 값을 할당할 필요가 있다. 우리는 참조 타입을 사용하기 때문에, 대신 실체가 없는(phantom) 상태 갱신을 실행한다:

_state 프로퍼티에 할당하는 것은 UI를  갱신하는 매커니즘을 필요 없게 만든다. ― 잠시 후에 이 프로세스에 관한 세부적인 이야기를 해볼 것이다.

우리는 세부적인 구현에서 꽤 깊게 쪼개었고, 그래서 나는 이 섹션을 마치면서 store의 역할을 고수준에서 다시 한번 상기시켜보고자 한다:
  1. 필요로 하는 모든 action을 위해 dispatcher와 함께 store를 등록한다. 현재 예제에서는 모두 annotationFilteringActions이 되어야한다.
  2. 각 개별 action들을 위해 불릴 수 있는 핸들러를 구현한다.
  3. 핸들러 안에서 해당 비즈니스 로직을 실행하고 그 결과의 상태를 갱신한다.
다음으로 어떻게 UI가 store로부터 상태 갱신을 받는지 이야기 해보자.

4단계 : store로 UI를 바인딩하기
Flux 개념의 핵심 중 하나는, 상태 갱신이 나타나면 자동으로 UI를 갱신한다는 점이다. 이로인해 UI가 항상 최신 상태를 보여줄 수 있고, 수동으로 이 갱신을 유지하기 위해 필요한 어떤 코드도 만들 수 있어야한다. 이 단계에서는 MVVM 아키텍처에서 View가 ViewModel에 바인딩하는 것과 굉장히 유사하다.

이걸 구현하는데에는 사실 많은 방법들이 존재한다. ― PlanGrid에서는 ReactiveCocoa를 사용하기로 했는데, 이것을 store가 Observable한 상태 프로퍼티를 제공한다. 아래 코드는 AnnotationFilterStore가 어떻게 이 패턴을 구현했는지 보여준다.

_state 프로퍼티는 store 안에서 상태를 바꾸기 위해 사용되었다. state 프로퍼티는 store에 구독하기 원하는 클라이언트를 위해 사용된다. 이것은 store 구독자들이 상태 갱신을 받을 수 있게 해주나 이것은 직접적으로 그 상태를 바꾸게 하지는 못하게 해놓았다(상태 변경은 action을 통해서만 일어난다!).

초기화 시점에서 내부의  Observable한 프로퍼티는 간단하게 외부 시그널 producer로 간다:

이제 _state로 가는 모든 갱신에서는 자동으로 state에 저장된 시그널 producer을 통해 최신 상태 값을 보낼 것이다.

남은 것은 새 state 값을 보낼때 UI가 갱신되는지 확인하는 코드이다. 이 부분은 iOS에서 Flux 패턴을 처음  사용할 때 만든 꼼수의 부분이다. 웹에서 Flux는 페이스북의 React 프레임워크와 굉장히 잘 동작한다. React는 상태가 갱신되면 추가적인 코드가 필요없이 UI를 다시 렌더링 한다는 특정 시나리오를 전제로 설계되었다.

UIKit과 함께 작업하는 상황에서는 이 부분을 깔끔하게 해결하지 못하고 손수 UI 갱신을 구현해야한다. 이 부분에 대한 이야기는 너무 길어질 수 있기 때문에, 이번 포스트에서는 더 깊게 설명할 순 없다. 대신 최하단에 우리는 UITableView와 UICollectionView를 위해 API 형태로 제공하는 React 컴포넌트들을 만들어 놓았다. 나중에 그것에 대해 가볍게 보여주겠다.

만약 이 컴포넌트에대해 더 배워보고 싶으면 최근에 내가 말한 것을 한번 확인해보거나, 두 Github 저장소(AutoTable, UILib)를 보아도 된다.

이제 주석 필터링 기능은 다시 접어두고 실제 세상의 코드를 보자(이번에는 약간 생략되었다. 이 코드는 AnnotationFilterViewController에 있는 코드이다:

우리의 코드베이스에서 우리는 각 View Controller가 viewWillAppear: 메소드에서 부르게 될 _bind라는 메소드를 들고 있는 규칙을 가졌다. 이 _bind 메소드는 store의 상태를 구독하고 상태 변화가 일어날 때 UI를 갱신하는 역할을 한다.
 
우리는 부분적으로 UI 갱신을 우리 스스로 구현해야 했고, React스러운 프레임워크에만 의존할 수 없었으므로 이 메소드는 어떻게 특정 상태 갱신이 UI 갱신과 맵핑되는지에 대한 코드를 담고있다. 여기 ReactiveCocoa는 이 관계를 설정하기 쉽게 만들어주는 여러 오퍼레이터(skipUtil, take, map 등)을 제공함으로서, 사용하기 쉽게 해준다. 만약 이전에 Reactive 라이브러리를 사용해본 적이 없다면 이 코드가 약간 생소할 수 있다. ― 그러나 우리가 사용하는 ReactiveCocoa는 작은 부분인데다, 배우려고하면 꽤 빨리 배울 수 있다.

예제에서 첫째줄의 _bind 메소드는 상태 변화가 일어날때 Table View를 갱신하게 만든다. 빈 상태일때 갱신이 먹히지 않도록 ReactiveCocoa의 ignoreNil() 오퍼레이터를 사용한다. 우리는 Table View가 어떻게 보여질지 표현에서 store로부터 최신상태를 매핑하기위해 map 오퍼레이터를 사용한다.

이 맵핑은 annotationFilterViewProvider.tableViewModelForState 메소드를 통해 발생한다. 이것은 실행에서, UIKit을 감싸는 우리 커스텀 React가 발생되는 곳이다.

더 깊게 구현에대해 볼 순 없지만, 여기 tableViewModelForState 메소드가 있다.

tableViewModelForState는 인풋으로 최신 상태를 받고, FluxTableViewModel의 양식으로 Table View의 표현을 반환하는 순수 함수이다. 이 메소드의 아이디어는 React의 render 함수와 유사하다. FluxTableViewModel은 전적으로 UIKit과 독립적이고 테이블의 컨텐츠를 담은 구조가 간단하다. 당신은 오픈소스로 구현된 예제를 AutoTable 저장소에서 확인해볼 수 있다.

이 메소드의 결과는 ViewController의 TableViewDataSource 프로퍼티로 넘겨준다. 그 프로퍼티 안에 저장되있는 컴포넌트는 FluxTableViewModel에서 제공하는 정보를 기반으로 UITableView를 갱신하는 역할을 한다.

다른 바인딩 코드는 많이 간단하다. 예를들어 isFiltering 상태에따라 "Clear Filter" 버튼을 enable/disable 하는 코드가 아래에 있다:

UI 바인딩이 UIKit 프로그래밍 모델과 완벽하게 들어맞지 않아서 이것을 구현하는데 꼼수를 조금 사용하였다. 그러나 좀 더 쉽게 커스텀 컴포넌트를 만드려고 아주 약간만 노력을 기울였을 뿐이다. 전통적인 MVC 방식은 수많은 장황한 구현과 수많은 양의 View Controller 구현으로 UI를 갱신하는데, 우리 경험에서는 MVC를 쓰는것 대신 이 컴포넌트를 구현함으로서 구현 시간을 절약할 수 있었다.

이 UI 바인딩이 잘 구현되있다면, 우리는 Flux 기능 구현의 마지막 파트를 이야기할 차례이다. 내가 너무 많은 것을 이야기 했었던 것 같으니 Flux에서의 테스트를 설명하기 이전에 앞에 것들을 빠르게 한번 요약하겠다.

구현의 요약
Flux를 구현할 때 나는 일반적으로 아래 순서에 따라 작업을 쪼개어 한다:
  1. 상태 타입의 모양을 정의한다.
  2. action을 정의한다.
  3. 각 action들의 비즈니스 로직과 상태 변화를 구현한다. ― 이것은 store 안에 구현되있다.
  4. view를 표현하기 위해 상태를 맵핑하는 UI 바인딩을 구현한다.
이것은 우리가 얘기했던 세부적인 구현의 모든것들을 포괄한다.

이제 드디어 Flux에서 어떻게 테스트 할 지에대해 이야기해보자.

테스트 작성하기
Flux 아키텍처의 큰 장점중 하나는 일들을 엄격하게 분리한다는 점이다. 이것은 비즈니스 로직이나 UI 코드의 커다란 부분을 테스트하기 쉽게 해준다.

Flux에서는 테스트 해야하는 두가지 부분이 있다:
  1. store에서 비즈니스 로직
  2. view 모델 프로바이더(이것은 우리 React이다 ― 입력 상태에 따라 UI 표현을 처리하는 함수 형태이다)

store를 테스트하기
store들을 테스트하는 것은 보통 아주 쉽다. 우리 테스트는 action에서 호출하여 store와 함께 상호소통하게 할 수 있고, store에 구독하는 내부 _state 프로퍼티를 Observe하든 하여 상태 변화를 지켜볼 수 있다.

추가적으로 우리는 특정 피처를 구현해보거나 store의 초기화에서 이것을 심어보기위해, store가 소통하는데 필요한 어떤 외부 타입을 모의 객체(Mock Object)로 만들어 볼 수 있다.(특정 피처: API 클라이언트도 될 수 있고 데이터 접근 오브젝트가 될 수도 있다.) 이런 방식은 그 타입들이 우리가 예상한데로 호출되는지 집중할 수 있게 해준다.

PlanGrid에서는 Quick와 Nimble을 사용하여 작업에 관한 스타일의 테스트를 작성하였다. 여기 이 예제는 우리의 주식 필터링 store 부분에서의 테스트이다:

다시한번 말하자면, store를 테스트하는 것은 많은 메리트를 가지고 있다. 이 특정 테스트를 당장에 깊게 다루지는 않을 것이나 테스팅 철학은 명확하다. 가짜로 만든 모의 객체에서 store로 action을 보내고 나서 상태변화된 형태의 응답을 확인한다.

(여러분은 dispatcher를 이용하여 action을 dispatch 하지 않고, 왜 store에서 _handleActions 메소드를 호출하는지 의아해할 것이다. 원래 우리의 dispatcher는 action을 전달할 때, 비동기적 dispatcher를 사용했다. 그렇기에 비동기 테스트가 필요했고, dispatcher의 구현이 바뀌어왔기 때문에 테스트를 진행하면서 dispatcher를 사용할 수 있었다.

store에 비즈니스 로직을 구현할 때 나는 내 첫번째 테스트 코드를 작성하였다. Quick 행동 스펙(spec)과 함께 store 코드의 구조는 테스트 기반 개발 프로세스와 아주 잘 맞게 되어있었다.

view를 테스트하기
선언된 UI 레이어와 Flux 아키텍처는 view를 테스트하기 간단하게 짜여져있다. 팀 내부적으로 우리는 view 레이어에 목표로 하는 커버리지의 양을 아직 의논중이다.

실제로 우리 view에있는 모든 코드는 꽤 직관적으로 짜져있다. view는 store 안에서 우리 UI 레이어의 서로 다른 프로퍼티에 상태를 묶는다. 우리 앱의 경우 UI 자동 테스트를 통해 대부분의 코드를 커버하기로 결정했다.

그러나 여기엔 많은 대안들이 존재한다. view 레이어는 주입된 상태를 렌더링하기위해 초기화함으로 스넵샷 테스트도 매우 잘 동작할 수 있다. Artsy는 다양한 말과 블로그 포스트를 통해 스넵샷 테스팅 아이디어를 소개했다. 이 objc.io 글까지 포함해서 말이다.

우리 앱에선 UI 자동 커버리지가 충분하다고 판단했고, 이 이상 추가적인 스넵샷 테스트는 필요없었다.

또한 나는 view 프로바이더 함수를 유닛 테스트하는 경험도 했다.(e.g. 이전에 보았던 tableViewModelForState 함수) 이 view 프로바이더는 UI 표현을 위해 상태를 맵핑하는 순수 함수들이다. 따라서 입력과 출력 값에 기반한 테스트를 매우 쉽게 할 수 있었다. 그러나 이 테스트들은 실제 구현한 양과 비슷한 양으로 작성되기 때문에 많은 값을 넣어 볼 순 없었다.(However, I found that these tests don’t add too much value as they mirror the declarative description of the implementation very closely.)

우리가 앞에서 본 것처럼 UI 테스팅에는 많은 대안의 솔루션들이 있고, 나는 우리가 긴 기간동안 사용할 솔루션을 모색하는 중이다.

결론
많은 세부적인 구현을 본 뒤에 고수준의 관점에서 우리의 경험을 말해주고 싶었다.

우리는 오직 6개월동안 Flux 아키텍처를 사용해왔지만, 우리 코드를 보면서 이미 여러 장점을 발견할 수 있었다:
  • 새로운 기능을 조화롭게 구현한다. store, view 프로바이더, view controller의 기능의 구조는 거의 동일하다.
  • 상태와 action을 잘 정렬함으로서 그 기능이 어떻게 동작하는지 이해하기 쉽고, BDD 스타일로 테스트 할 수 있다.
  • store와 view를 강력하게 분리해준다. 특정 코드가 모호하게 있는 것이 드물다.
  • 코드 읽기가 굉장히 간단해진다. view가 의존하는 것이 명확하게 보인다. 이게 디버깅하기 매우 수훨하게 해주기까지 한다.
  • 위의 모든 것들이 새 개발자가 투입될때 쉽게 적응하게 만들어준다.

명백하게도 여긴엔 단점들도 있다:
  • UIKit 컴포넌트와 통합하는 첫 걸음이 약간 고통스러울 수 있다. React 컴포넌트와 다르게 UIKit view들은 새 상태에 의해 그들 스스로 간단하게 업데이트 되는 API 지원이 미흡하다. 이것이 조금 힘든 점이고, 우리는 view 바인딩에서 손수 구현하던지 UIKit 컴포넌트를 감싸는 커스텀 컴포넌트를 만들어야 할 필요가 있었다.
  • 아직 우리 모든 새 코드가 Flux 패턴을 정확히 따르지 못했다. 예를들어 Flux에서 동작하는 네비게이션/라우팅 시스템이 아직 자리잡지 못했다. 그래서 Flux 아키텍처에 동등한 패턴을 통합시키던지 ReSwift Router를 사용하여 비슷한 실제 라우터를 사용할 필요가 있었다.
  • 앱의 큰 요소들을 거쳐서쳐 공유되는 상태를 위해 좋은 패턴으로 만들어야한다.(이 포스팅의 초반부에서 "store의 영역은 어디까지인가?"라는 주제로 이야기하였다.) 기존 Flux 패턴으로의 store 사이에 의존성을 만들어야할까? 다른 대안은 무엇이 있을까?

더 많은 실제 구체적인 구현에서 더 많은 이점 혹은 단점이 존재한다. 나는 여기에 좀 더 깊게 파볼 것이고 나중에 블로그 포스트에서 더 세부적인 양상을 확인할 수 있기를 바란다.

지금까지는 이런한 선택으로인해 굉장히 기쁘고, 이 블로그 포스트를 통해 여러분께 Flux 아키텍처가 적절한지 알아볼 수 있는 기회를 제공했기를 바란다.

이제 마지막으로, 여러분이 Swift로 Flux와 함께 작업하고 싶거나 큰 산업을 위해 중요한 제품을 만드는데 도움을 주고 싶으면, 우리는 지금 고용중이다.

이 글의 초안을 검토해준 @zats, @kubanekl, @pixelpartner에게 감사하다.

참고:

  • Flux - 페이스북의 공식적인 Flux 사이트. 원래의 소개가 들어있다.
  • Unidirectional Data Flow in Swift - Swift에서의 Redux 개념과 원래의 ReSwift 구현에대해 이야기한다.
  • ReSwift - Swift에서 Redux를 구현한 것.
  • ReSwift Router - ReSwift 앱을 위한 정의된 라우터



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

,
원문 :  https://medium.cobeisfresh.com/implementing-mvvm-in-ios-with-rxswift-updated-for-swift-2-51cc3ef7edb3#.jzmyljsky

iOS에서 MVVM를 적용시키는 수많은 글들이 존재하지만, 실제로 사용되는 MVVM는 어떻게 생겼는지, 사실상 어떻게 하는지에 초점이 맞춰져있는 글은 거의 없다. 이 글은 RxSwift를 사용하여 좀 더 실질적인 관점에서 MVVM를 살펴 볼 것이다.

ReactiveX는 시퀸스를 observable 함으로서 비동기와 이벤트 기반 프로그램으로 구성된 라이브러리이다.— reactivex.io

RxSwift는 ReactiveX의 Swift 버전이다. 이것은 리엑티브하게 프로그래밍 할 수 있게 도와주는 프레임워크다. 만약 이게 무슨 말인지 모르겠어도(아마 그럴것이다. 함수형 리엑티브 프로그래밍(FRP)은 최근에 각광받기 시작했다.) 멈추지 말고 한번 읽어보길 추천한다. 리엑티브는 여러분의 프로젝트를 더 간결하고, 유지보수 하기 쉽고, 다루기 쉽게 만들어 줄 것이다.

어떻게 iOS 컴포넌트들이 서로 소통할까?
RxSwift의 가장 큰 부분은 앱에서 서로 다른 컴포넌트 사이에서 간단하게 소통할 수 있다는 점이다. 예를들어 Model과 ViewController가 있다. MVC에서는 이들을 연결하기 매우 난잡함을 느낄 수 있었을 것이다.

ViewController에서 모든 outlet을 리셋시키기위해, 아마 model이 갱신될때 항상 Controller에서 updateUI() 함수를 호출해주어야 할 것이다. 이러한 흐름은 불필요한 갱신이나 이상한 버그들이 생기면서 Model과 ViewController 사이에 부조화가 일어나기 쉽다.

우리는 매 순간마다 Model의 옳바른 상태를 표시하는 View Controller가 필요하다. Model이 어떻든 Model 갱신되는 즉시 일치한 데이터를 보여주는 View Controller가 필요하다.

물론 바로 Model만 표시하는 대부분의 앱에서는 의미없는 고민이겠지만, 우리는 Model로부터 데이터를 뽑아와서 화면에 표시할 준비를 하는 과정이 필요하다. 이것이 왜 ViewModel 클래스를 소개하게 되었는지에 대한 이유이다. ViewModel은 화면에 표시할 모든 데이터를 준비한다.

그러나 조금 재미있는 부분이 있다: ViewModel은 ViewController에대해 아무것도 모른다는 사실이다. 절때 그 안에서 직접적으로 참조하거나 프로퍼티를 가지고 있지 않는다. 대신에 ViewController는 ViewModel의 모든 변화를 항상 Observe하고 있으며, ViewModel에서 변화가 일어나면 그것을 화면에 표시한다.

한 프로퍼티당 기반임을 기억하고 있자. 이 의미는 ViewModel 안에서 ViewController가 화면에 개별적으로 각 프로퍼티를 표시한다. 예를들어, 문자열과 이미지를 불러올때, 그 두가지를 다 불러올 때까지 기다리고 있는 것이 아니라, 불러와지는데로 바로바로 각 이미지를 화면에 표시할 수 있다.

ViewController는 화면에 표시하는 역할 뿐 아니라 유저의 입력을 받는 역할도 한다. 우리 ViewController는 단지 프록시(proxy)이고, 그 입력을 ViewController에서 따로 사용하지 않으므로 모든것을 ViewModel로 보내버리고 이것을 ViewModel이 알아서 처리할 것이다.

위 그림은 ViewController와 ViewModel 사이에 단방향 통신을 하는 방법이다. ViewController는 ViewModel을 보고 그것에게 말할 수 있지만, ViewModel은 ViewController가 무엇인지 전혀 모른다. 이 말은 앱에서 ViewController를 완전히 제거해도 모든 로직이 제대로 동작할 것이라는 뜻이다!

좋아보이지 않는가?! 그러나 어떻게 이게 가능할까?

RxSwift와 함께 MVVM
유저의 도시 입력에 따른 기상 예측을 표시해주는 간단한 날씨 앱을 만들어보자.

이 글은 RxSwift의 기본 지식을 가정하고 쓰였다. 만일 ReactiveX에 대해 전혀 모른다면, 마음가는대로 읽어도 상관없지만, ReactiveX 글을 읽어보길 추천한다.


우리는 도시 이름을 입력받기 위해 UITextFeild를 준비하고 현재 온도를 보여주기위해 UILabel을 준비했다.

Note: 이 앱에서는 OpenWeatherMap의 날씨 데이터를 사용했다.

도시의 이름과 날씨로 구성된 Weather 구조체가 우리의 Model이 될 것이다. 이 구조체는 받아온 값을 파싱한 뒤, 속성에 맞춰 만들어진 JSON 오브젝트로부터 만들어진다.


이제 public의 searchText 프로퍼티가 변경될 때, ViewModel이 새 Model을 요청해야한다. ViewController는 유저 입력을 보내기 위해 이 프로퍼티에 접근하게 된다.

searchText는 변수이다. 변수는 필수적으로 BehaviorSubject를 감싼다. 이것은 Observer할수도, Observable할수도 있다. 다시말해 그들이 다시 호출할 수 있는 항목을 그들에게 보낼 수 있다.

BehaviorSubject는 한번만 구독되야하기 때문에 유일한 존재이다. BehaviorSubject는 받았던 마지막 항목을 보낸다. MVVM에서는 이러한 방식이 필요하다. 앱의 라이프 사이클에 의존하며, 다른 클래스에서 Observable은 종종 그것들을 구독하기 전에 엘리먼트를 받기도 한다. ViewController가 ViewModel의 프로퍼티에 구독하면, 화면에 표시하기위해 마지막 항목이 무엇인지 보아야한다. 반대의 경우도 마찬가지이다.

이제 우리는 프로그래밍적으로 변하는 모든 UI 부분에 대해 ViewModel 안에 한 프로퍼티를 정의할 것이다.

ViewModel은 데이터를 출력할 수 있는 형태로 변환하는 역할을 맡고 있다. 이 경우 우리의 Model은 다른 Weather 객체의 Observe되어지는 한 순서이다. 위 프로퍼티(cityName, degrees)는 Weather Observable에 다른 맵핑이 일어날 것이다.

이 프로퍼티가 private로 선언된 이유를  기억하자. ViewController에는 비즈니스 로직에 대해 전혀 몰라야 하기 때문이다. ViewController는 화면에 표시하기 위한 데이터 밖에 모른다.

검색
이제 우리가 위에서 선언한 searchText 프로퍼티에 우리의 Model을 연결해보자.

우리는 searchText가 바뀔때마다 네트워크 요청을 만들 것이다. 그리고 우리의 Model은 그 요청을 구독하고 있을 것이다.

이 경우 searchText가 바뀔때마다 jsonRequest는 NSURLRequest와 통신하기 위해 스스로 갱신된다. 갱신마다 우리의 Model은 NSURLRequest로부터 어떤것을 받던지간에 세팅된다.

만약 JSON 요청중 에러가 나오면 그것을 출력하고 빈 값을 반환한다.

Note: rx_JSON() 메소드는 실제로 그 스스로 Observable 순서이다. 그러므로 jsonRequest는 Observable의 Observable이다. jsonRequest가 가장 최신의 것을 리턴하기 위함이 마지막에 .switchLatest()를 사용하는지에대한 이유이다. 또한 요청을 당신이 그것에 구독하기 전까지 패치되지 않을 것이라는 것을 기억해두자.

.shareReplayWeather에 구독하는 모든 것들이 정확하게 같은 결과를 받았는지 확신하기 위함이다. 그렇지 않을 경우 각 구독은 날씨의 개별 객체를 호출할 것이고 요청이 중복으로 일어날 수 있기 때문이다.

이제 남은 것은 ViewController를 ViewModel에 연결하는 것이다. ViewModel의 Observable을 Controller의 outlet에 바인딩하여 연결할 수 있다.(We’ll do this by binding the PublishSubjects in the ViewModel to outlets in the Controller.)

사용자가 텍스트 필드에 친 값을 ViewModel이 알고 있어야함을 기억하자! ViewModel의 searchText 프로퍼티에 ViewController의 textField 값을 바인딩하여 위 일을 할 수 있다. 따라서 viewDidLoad()에 아래의 코드만 추가하면 된다:


이제 됐다! 우리 앱은 유저가 타이핑하는 동안 날씨 데이터를 갱신한다. 그리고 유저가 어떤 것을 볼지라도 화면 뒤의 앱 상태를 보게된다.

이 앱에서 좀 더 확장되고 주석이 달린 코드의 버전에 관심이 있다면 내 Github의 Weather 앱을 확인해보아라.


여기 당신이 흥미있어할 법한 더 많은 글들이 있다.


용어 정리

  1. Observable, Observer, Subscribe
    : 옵저버 디자인 패턴에서 사용하는 용어로, Observer는 구독(Subscribe)하는 오브젝트, Observable은 구독당하는 오브젝트를 말한다. Observer는 
    Observable에게 자기 자신을 넘겨줘서 Observable에서 이벤트가 발생할때 Observer에 있는 메소드를 호출해줌으로서 구독할 수 있다. 이 글에선 Observer나 Observable에 적합한 한글번역을 찾지 못해, 그대로 표기하였다.



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

,


유닛 테스트에서 가장 힘든 시점은 시작 시점이다. 그 이유는 모든 아키텍처가 유닛 테스트 되지 않기 때문이다. 만약 유닛 테스트를 하고자 한다면(Part1(링크)에서 왜 해야하는지 설명했다.) 앱의 아키텍처를 주의깊게 만들어야 한다.


이 아키텍처에 관해 좀 더 세부적으로 들어가기 전에 먼저 한가지 키포인트를 강조하고 싶다:

우리 아키텍처를 보다 더 테스트하기 쉽게 만드는 과정은, 코드를 다른 방면으로도 더 낫게 만들어 줄 것이다. 보통 테스트 가능한 설계는 좋은 소프트웨어 설계와 직결된다.

테스트 가능한 아키텍처로 바꿀 때, 우리는 앱 컴포넌트들을 더욱더 독립적으로 만들어야 하는데 특히 외부 라이브러리로부터 분리해야한다. 이렇게 바꾸면 더 다루기 쉬워지고, 이 점은 소프트웨어 개발의 불변의 법칙이다. 또한 우리는 앱을 만들때 시작 시점에 어떻게 해야하는지, 긴 개발기간동안 어떻게 시간을 단축시킬 수 있는지에대해 주목해볼 것이다.

이제 이 일을 정확히 어떻게 하는지 보자

ViewController와 함께 다루기
모의 객체로 ViewController를 본따는 일은 쉽지 않다. ViewController를 테스트 하는것도 쉽지 않은 일이다. 따라서 무턱대고 작업해서는 안된다.

음... 뭐라고?

우리는 우리에 맞는 MVVM 아키텍처를 사용할 것이다. MVVM에 대한 글이 수도 없이 많기 때문에(아래 링크 참고) 이 부분에 대해 깊이 들어가지는 않겠지만 기본 원칙은 다음과 같다. 모든 로직을 ViewController 바깥에 두어 UIKit과 로직이 섞이지 않게 한다. 이 로직에는 모든 모델의 변화, 다른 서비스를 호출, 상태 변화, 예외 처리 등이 있다. 이 모든 로직은 ViewModel에 담겨있다. 모든 ViewController는 ViewModel로부터 데이터를 바인딩하고 유저 인터렉션에 그것을 보내며, 유저 인터렉션은 애니메이션이나 뷰를 준비하는 화면 표시의 코드이다. 아래에 MVVM에관한 글이 더 있다:


MVVM에는 각기 다른 관점들이 존재한다. 어떤 사람들은 ViewModel이 모델과 한께 초기화된 값 타입이여야한다고 하기도 하지만 우리는 조금 다르다. 우리 아키텍처에서의 ViewModel은 UI 상태를 가지고 있고, 다른 서비스를 호출하며, ViewController에 보여줄 가공되지 않은 데이터를 제공하는 역하을 한다.

이렇게 함으로서 ViewController는 굉장히 가볍고 간단한 UIKit 관련 레이어가 되므로 대부분의 앱 로직을 쉽게 테스트할 수 있다.

(Note: ViewController를 테스트 할 수도 있지만 솔찍한 내 의견은 MVVM이 MVC보다 더 나은 아키텍처라고 생각한다.)

의존성 주입(Dependency Injection)
유닛 테스트에서 각 '유닛'은 완전히 분리되어 있기 때문에 앱의 각 컴포넌트를 독립시킬 필요가 있다. 우리가 테스트 하고 있는 각 클래스에 '약한' 의존성을 제공할 방법이 필요하고, 의존성때문에 특정 테스트가 실패하는 것은 아닌지 알아야한다.(This means that we need a way to provide “sterile” dependencies to each class we are testing, to know for certain tests won’t fail because of the dependencies.)

의존성 주입이란 그냥 한 클래스에 외부적인 의존성을 제공한다는 의미의 그럴싸한 표현이다. 클래스가 자기 스스로 의존성이 생길 수는 없다. 다른 클래스를 호출하고자 한다면 그 객체를 초기화때 파라미터로 받아두어야한다. (이것을 constructor injection이라 부른다)

또 다른 한가지 양상은 모든 의존성은 프로토콜 객체로서 선언된다는 것이다. 이러한 방법으로 그 클래스가 필요로하는 오브젝트를 메소드에 담아 우리가 원하는 클래스/구조체의 객체를 초기화할때 쉽게 전달할 수 있다.


이렇게하면 우리의 통제된 의존성을 클래스에 제공할 수 있게 해주고, 나머지 앱 부분으로부터 완전히 독립적이게 만들어준다. 또한 모의 객체 의존성과 함께 깔끔하고 작은 일을 할 수 있게 해준다.

이런식으로 코드를 작성하면 꼭 테스트 뿐만 아니라 코드를 분리시킬 수 있다는 점에서 좋은 방법이다. 그 클래스는 구체적인 구현에 의존하지 않고 그냥 프로토콜로서 들고 있는 것이므로 다른 클래스의 구현을 변경하더라해도 그 클래스는 건드리지 않아도 된다.

외부 프레임워크
만약 클래스가 외부 프레임워크에 의존하고 있으면 어떻게 될까? 우리 클래스가 NSURLRequest나 CoreData에 의존하고 있으면 어떨까? 아마 꼬일 것이다.

우리는 외부 프레임워크를 감싸는 helper를 만들 것이다. 기본적으로 helper는 감싸고 있는 외부 프레임워크의 무엇이든 불러오고, 프레임워크에 함수 호출을 전하며, 외부 프레임워크에 의존없이 우리 코드베이스에서 사용할 수 있는 형태의 결과로 변형하는 역할을 한다. 한가지 일반적인 규칙은, helper는 한 프레임워크당 하나씩 불러온다.

helper 안의 로직은 가능한 작아야한다. ViewController처럼 테스트 하기 쉽지 않기 때문이다. 그리고 그 프레임워크의 같은 기능을 재정의하는것이 아니라 helper를 코딩 기준과 필요한 것에 맞추어야한다.

다른 의존성에도 같은 원리를 적용시켜 모든 클래스는 세부적인 구현을 하는게 아니라 helper에 맞춘 프로토콜의 객체를 받는다.

이렇게하면 커다란 이점이 있다. 예를들어 당신이 CoreData가 너무 복잡해서 Realm으로 바꾸려 할때, 오직 한 클래스만 고치면 된다.

그러면 자신의 의존성을 제공하지 않는 클래스는 무엇이 있을까? 바로 서비스 팩토리이다.

서비스 팩토리(Service Factory)
서비스 팩토리는 앱 전체에 의존성을 만드는 변수를 get-only로 모아놓은 집합체이다. 예를들어 ViewModel은 APIServiceProtocol 타입에 의존성을 가지는데, 그것이 ViewModel로서 컨스트럭트 될 것이다. (apiService: ServiceFactory.apiService)

클래스가 다른 것을 호출하는 장소는 한 곳에 모여있어야 한다. 이것은 마치 통제실 같은 느낌을 주며, 어떤 개발자가 보아도 한번에 이 클래스가 어떻게 돌아가는지 알 수 있어야 한다.

또한 의존성 구현을 맞바꾸는 유일한 장소이기도 하다. 예를 들어보자면 싱글톤에서 일반 객체로 바꾸는데 5초정도 걸린다. 그렇게 하기 너무 크다면 한 클래스를 두 부분으로 쪼개어 시간을 절약할 수 있다.

그리고 여러분의 클래스에 더미나 목(mock) 의존성을 제공하여 실행때 인자레 의존하므로 UI 테스트나 디버깅에 유용하다. UI 테스트시 네트워크로부터 독립적이거나 백엔드에서의 기능은 여전히 잘 동작하고 있을 것이다.

좋다. 이제 우리 앱이 실제 구현과 모의 객체를 맞바꿀 수 있다. 그러나 이것을 어떻게 할 수 있을까?

모의 객체(Mocks)
모의 객체는 여러분이 테스트하고 있는 클래스의 의존성으로서 같은 프로토콜을 따르는 클래스 혹은 구조체이다. 모의 객체는 클래스에 유닛테스트 할 수 있게 해준다.모의 객체는 모통 no-op 메소드나 유닛 테스트에 유용한 작은 기능들을 가지고 있다.

일반적인 역할은 모의 객체가 테스트할 타겟 속으로 들어간다는 점이다. 이렇게 하면 모의 객체에 구현된 코드들이 우리 앱을 더럽히지 않을 수 있다.

당신은 모의 객체 의존성만 생각해선 안된다. 완전한 테스트를 위해 종종 모의 객체 델리게이트 오브젝트를 만들어야한다. 정확한 원칙은 이것이지만 노력하고자하는 델리게이트 메소드를 검증해야 한다는 점을 잊어선 안된다.

Swift에서는 제한된 런타임 접근 때문에 아직 안드로이드용 Mockito나 Objective-C용 OCMock과 같은 좋은 모의객체 프레임워크가 나오지 않았다. 따라서 직접 모의객체를 만들어보자.

지루한 작업일 수 있으나 모의객체 프레임워크를 쓰는 것 보다 더 자유롭게 작업할 수 있을것이다. 대부분 모의객체가 보편적으로 다음과같은 설계를 가진다는 것을 알아냈다:
  1. 당신이 모의로 할 프로토콜을 구현한다.
  2. 각 메소드는 모의객체 안에 methodDidGetCalled 라는 불리언 프로퍼티를 가지고 있어야하고 requestedParameterX 프로퍼티는 옵셔널하게 가지고 있는다. 이 메소드 구현은 보통 마지막에 이 프로퍼티를 설정하는 것이다. 여러분은 테스트한 클래스에 그 메소드가 옳바른 파라미터와 함께 호출 되었는지 나중에 확인해 볼 수 있다.
  3. 만약 메소드가 리턴값이나 완료 핸들러의 뭉치를 가진다면 구조체에 methodXShouldFail이라는 불리언 프로퍼티가 있다. 이 메소드 구현은 불리언을 체크하고 성공하든 못했든 결과를 반환한다. 이렇게하면 테스트한 클래스에 실패가 생겼을 때 다루기 유용해진다.

테스트 작성하기
앱을 옳바르게 준비하고, 무엇을 테스트할지 안다면 이번에는 꽤 같단하게 끝날 것이다. 이번에는 단지 테스트의 동작을 확인한다.

테스트를 도와주는 수많은 라이브러리가 존재한다. 먼저 애플의 XCTest이다. Xcode와 연동하여 사용할 수 있고, 타이핑할 것이 좀 많기는 하나 꽤 좋은 테스트 라이브러리이다. 그리고 써드파티 라이브러리인 QuickNimble이 있다. 우리는 Nimble과 함께 XCTest를 사용한다.

다른 메소드를 호출하는 메소드를 위해, 그 메소드가 호출되었는지, 옳바른 파라미터를 전달했는지 체크한다.

당신의 클래스가 델리게이트에게 알리는지 혹은 옳바른 콜백을 호출하는지 확인한다.

이제 당신이 할 수 있는 모든 것을 테스트 할때까지 두 세번정도 이 일을 하면 된다.

이 두 포스트를 통해 왜 테스트를 해야하는지 아는데 도움을 주고, 유닛테스트를 어떻게 시작하는지에대한 가이드가 되면 좋겠다. 즐거운 테스팅하길 바란다!








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

,




당신이 TDD를 사용하든 하지않든, 코드를 검증하기 위한 테스트를 하면 많은 편리함을 느낄 것이다. 새 기능을 추가하거나 리팩토링을 할 때에 코드베이스의 일부분을 수정할 필요 없이 그것을 가능하게 해준다.

COBE에서 이전까지는 겉모습만 유닛테스트인 것을 만들다가 최근에 실제 유닛 테스트를 만들기 시작했다. 그리고 우리의 경험을 두 글을 통해 적어보기로 했다. 첫번째 글은 유닛 테스트를 왜 하고, 어떤 유닛 테스트를 할지에 대해 다룰것이고, 두번째 글은 좀 더 실질적으로 들어가서 우리 코드베이스를 어떻게 테스트하기 쉽게 짤 수 있는지, 실제 테스트는 어떻게 작성하는지에대해 이야기 해볼 것이다.

왜 테스트할까?
두려움은 성공의 적이다.
여러분의 코드베이스 속에는 한마리의 거대한 괴수처럼 생긴 클래스를 가지고 있을 것이라고 조심스럽게 예상해 본다. 그 클래스는 리팩토링이 필요하지만 모두가 그 코드를 건드리기 무서워 할 것이다.

유닛 테스트는 이런 클래스를 리팩토링하기 쉽게 해준다. 심지어 새 기능이 추가되어도 기존 코드베이스를 고치든 그렇지 않든 상관없이 원래 있던 테스트는 잘 돌아간다.

클래스의 동작을 확인하기 위해 테스트를 한번 만들면, 그 안에 있는 코드를 고쳐볼 수 있고, 그리고 코드를 분해하지 않아도 즉각적이고 꽤 정확하게 알 수 있다. 그러면 두려움은 사라지고 그 클래스를 당신이 원하는대로 리팩토링 할 수 있게 될 것이다.

두려움은 독창력을 압도하고 결과적으로 품질을 압도한다. 우리가 한번 작성한 코드는 항상 두려움이 없어야한다.


능동적 vs 수동적 기록
코드를 기록하는 데에는 두가지 타입의 문서화가 있다고 생각한다: 능동적 기록과 수동적 기록

먼저 수동적 기록은 주석이나 다른 외부 문서들을 말한다. 이때 다른 외부 문서간 당신을 포함한 팀원들이 당신의 코드를 위해 작성한 것이다. 수동적 기록의 단점은 다른이가 최신버전이 어떤지 모른다는 점이고, 누군가 강제로 읽게 할 수 없다.(호의를 배풀어가며 누군가에게 읽도록 강요해 볼 순 있다.)

능동적 기록은 개발을 진행하는 동안 가시적인 방법으로 문서를 보여주는 것이다. 이렇게하면 여러분의 메소드에 옳바르게 접근하는지 컨트롤할 수 있고 또 디프리케이트(deprecated)된 메소드라고 표시도 할 수 있는 등 다양하게 가능하다.

앱이 어떻게 동작 하는지 알려주고 만약 동작하지 않으면 그것 또한 모두에게 알려주기 때문에 나는 테스트가 능동적 기록에 속한다고 생각한다. 이러한 특징은 특히 많은 사람들과 복잡한 코드베이스에서 작업할때 유용하게 쓰인다.

테스트 작성하기
코더처럼 작성하지 말고 사용자처럼 작성하기
테스트는 구현한 것을 테스트하려 하는 것이 아니라 당신이 필요한 동작을 테스트해야한다. 나는 이 개념을 Orta Therox의 e북인 iOS testing에서 처음 접했다. 여기서 말하길, 메소드(혹은 클래스)들을 블랙박스라 생각하고 접근해야 한다고 한다. 당신이 아는 것이라고는 메소드나 클래스에 넣는 인풋과 결과로 나오는 아웃풋 뿐이다.

즉, 각 메소드와 클래스의 끝나는 지점이 어떨지 생각해보고 그것만 테스트한다. 절대 그 구현 사이에 것들을 테스트 하지 마라.

이렇게 하면 자유롭게 리팩토링 할 수 있다. 메소드 이름을 변경하지 않은채, 테스트하고 있던 클래스/구조체의 모든 구현이 바뀌어도 당신의 테스트는 돌아갈 것이다.

실질적으로 당신은 끝나는 결과를 테스트한다. 각 클래스를 작은 전자기적 요소로 비유하여 생각해보자. 한쪽끝은 -, 다른쪽 끝은 +를 띌것이다. 그리고 이제 관찰을 해보는데, 테스트는 이 두 끝점을 관찰하는 것이지 이 두 사이를 관찰하는 것이 아니다.

언제 테스트를 하고 어떤것을 테스트 할까
이상적으로는 모든 것을 테스트하고 모든 시점에서 테스트하는 것이다. 그러나 현실세계에선 그것이 불가능하다. 여러분들의 앱 레이어들은 생각해보자면 테스트는 이 레이어를 항상 커버 하도록 해야한다. 몇 레이어는 너무 고수준이고, 몇은 또 너무 저수준일 수도 있지만 말이다.

나는 외부 프레임워크를 유닛 테스트 할 수 없다고 생각한다. 대부분의 앱이 외부 프레임워크와 함께 작업할 수 밖에 없을 것이다. 이 말은 여러분의 앱이 데이터베이스부터 UIKit까지 외부 프레임워크를 항상 끼고 개발한다.

여기서 문제는 외부 프레임워크에 다이렉트로 호출하는 클래스들은 유닛테스트하기 힘들다는 점이다. 이것이 외부 프레임워크에 바로 호출하는 클래스를 가능한 가볍게 만들어야하는 이유이기도하며, 나중에 Part2에서 테스트가능한 아키텍처에 관해 좀 더 살펴볼 것이다.

외부 프레임워크 사이에 있는 것들은 모두 유닛테스트 해야한다. 그러나 때론 시간이 그렇게 넉넉하지 않을 수 있다. 이럴때 나는 아래와 같은 기준으로 테스트를 한다.

1) 더 복잡한 클래스일수록, 더 많이 테스트한다.
크고 복잡한 클래스는 버그가 넓게 퍼져 있을 가능성이 많으며, 그런 것들을 쪼개어서 관리하기 쉽게 만들어 주어야 한다. 그래서 이것을 우선순위로 테스트한다. 그 무서움을 빨리 제거하기 위함이다.

2) 이상한 버그를 발견하면 테스트한다.
이렇게 해놓으면 누군가 당신의 코드를 싹 뜯어 고칠 필요 없이 디버깅하는데 도움을 줄 것이다.

3) 클래스를 리팩토링할 때 누군가에게 당신이 고려하고 있는 것을 주석으로 남기기 보다는 테스트를 만든다.
처음 딱 보았을때 주석보다는 Xcode의 빨간 다이아몬드 표시(Xcode에서 테스트에 실패했다는 표시)가 더 눈에 들어온다. 또한 미래에 소스를 만지게될 개발자가 무언가 고칠때 단지 테스트를 돌려 보면서 고칠 수 있으므로 매우 유용할 것이다.

4) 개발자와 더 적게 마주치는 클래스는 더 많이 테스트해야한다.
UI 테스트는 시간을 절역하기에 유용하지만서도 사실 UI 버그는 꽤 쉽게 해결된다. 왜냐하면 개발자가 그 버그는 반복적으로 눈에 들어오기 때문이다. 그러나 다른 부분에서 유저는 겪었으나 당신이나 테스터는 겪지 못한 아주 작은 버그를 가지고 있을 수 있다.

5) 무언가를 리팩토링 하기 전에 테스트를 작성한다.
일단 믿어보아라. 이 방법은 삶을 순탄하게 해주고 시간을 절약하게 해줄 것이다.

기본적인 규칙은 가장 작게 테스트하고싶은 클래스를 테스트하는 것이다. 가장 유용한 테스트는 종종 가장 작성하기 어려운 테스트이다.

유닛 테스트 종류들
수학적 테스트
순수 함수를 생각해볼때 나는 종종 수학적인 함수를 떠올린다. 즉 A집합으로부터 각 요소를 가져다가 B집합에 있는 요소에 정확히 할당한다.

이것이 파라미터와 함께 호출하거나 예상된 결과를 비교함으로서 테스트 할 수 있는 함수이다. 이런 함수가 유닛 테스트를 만들기 가장 이상적인 함수이다. 안타깝게도 이런 함수는 작은 유틸리티 클래스에만 조금 구현되어 있다.

델리게이션 테스트
이 테스트는 한 클래스가 액션이나 어떤 정보를 다른 클래스에 보내는 것을 검증한다. 예를들어 로그인 버튼을 눌렀을 때 옳바른 파라미터와 함게 네트워크 클래스에 있는 메소드를 호출하는지 확인하고 싶거나, 혹은 어떤 설정 값이 바뀔때 나의 UserSettingManagerOrWhatever가 호출되는지 확인하고 싶을 때 이 테스트를 작성한다는 것을 발견했다.

이 테스트는 당신의 코드를 물려받은 프로그래머에게 굉장히 유용할 것이다. 또한 당신이 각 레이어마다 테스트를 만들어 놓으면 당신의 모든 클래스가 그들이 할 수 있는한 다 호출할테고, 버그는 구현속에 놓여있으며, 코드가 서로 엉겨붙어 있지 않게 해준다.

아웃풋 테스트
때론 함수들이 수학적인 함수가 아닐때도 많다. 때론 인풋 아웃풋이 한가지 이상 종류일 수도 있다. 예를 들어보자. 네트워크를 다루는 대부분의 메소드는 인터넷이 끊기거나, 서버가 터지거나 할때 다른 형태의 아웃풋을 내놓는다.

완료 핸들러를 가지는 대부분의 메소드들이 정확하게 이것을 처리했는지 보기위해, 성공적인 길을 갔는지 그렇지 않은지 반드시 테스트해야한다.

유닛 테스트의 구조
정의에의하면 유닛 테스트는 독립적이다. 유닛 테스트는 당신이 테스트 하고 있는 클래스 내부를 수정했을 때만 실패가 나타날 수 있으며, 그 테스트 외부의 클래스는 테스트 결과에 영향을 주어선 안된다. 이런 점이 테스트 중에 문제가 어디있는지 알게 해줌으로서 유닛 테스트의 장점이라 할 수 있다.(비록 무엇이 문제인지는 모를지라도 말이다.)

간단한 수학적 테스트의 경우, 각 순수 함수들이 당신의 코드로부터 완전히 독립적이기 때문에 문제가 없다.(옮긴이: 좀 더 크고 복잡한 수학적 함수라면 말이 다를 것이다)

간단한 수학적 함수간단한 수학적 함수


그러나 함수로부터 사이드 이펙트가 있다면 쉽지 않을 것이다. 만약 사이드 이펙트가 그 클래스 안에서 끝난다면 꽤 간단한 문제일 수도 있다—다시 처음부터 클래스를 설정하고 테스트를 돌려, 바뀐 클래스가 예상한대로 돌아가는지 확인한다.

클래스 내부에서 사이드 이팩트가 있는 메소드클래스 내부에서 사이드 이팩트가 있는 메소드


메소드가 다른 클래스를 호출한다면, 우리는 모의 객체(mock)를 만들어서 테스트할 수 있다. 모의 객체는 당신의 클래스 의존성을 위해 대리 역할을 하며, 테스트에서 만들어놓고 관찰(observe)할 수 있다. 당신은 테스트하는 클래스에 모의 객체를 제공하고 그 객체의 옳바른 메소드가 호출되는지 확인하면 된다.

다른 클래스를 호출하는 메소드다른 클래스를 호출하는 메소드



고려사항
상황별로 잠적으로 생기는 테스트의 수
여러분의 클래스에 불리언(boolean)타입의 프로퍼티가 있다고 생각하자. 자연스럽게 그 프로퍼티의 상태가 true인지 false인지 결과를 테스트 하려 할 것이다. 여기서 프로퍼티가 하나 더 추가되면 이 프로퍼티들이 조합되어 4가지의 결과 나올 수 있게 된다. 또 하나 더 추가되면 8가지나 된다!

당신이 테스트를 작성하든 하지 않든 프로그래밍을 할 때 상태(state)는 우리의 적이다. 확인할 것을 기억해두고 반드시 상태에 의존하는 것만 그렇게 해야한다.

코드 커버리지가 거짓일 수 있다.
우리는 가능한 많은 테스트를 한 코드를 가지고 있을지라고 다른 고려사항이 있다. 때론 테스트 메소드를 한 줄 더 적는것 보단, 새로운 것을 배워 적용하는게 더 나을 수도 있다는 점이다. 아래 설명을 보자.

Xcode는 테스트 할때 불려진 코드 매 라인마다 'coveraged'라는 표시를 등록한다. 어떤 라인은 테스트 되지 않고 지나쳤을 수도 있고, 어떤 라인은 여러번 호출되었을 수 있다. 테스트 할때에는 가능한 이런 모든 상황을 고려해야한다. 데이터가 없을때, 데이터가 많을때, 예외의 데이터일때 어떻게 동작하는지 메소드의 모든 경로를 테스트 해야한다.

커버리지는 당신 코드의 어디가 커버되지 않았는지만 말해주지, 당신은 어느 부분인지 말해주는 그것을 믿어선 안된다. (Coverage can only tell you which parts of your code aren’t covered. You cant trust it to tell you which parts are.)

한정된 상황에서만 테스트해서는 안된다.
어떤 경우 미래의 개발자가 당신이 의도하지 않은 방향으로 클래스를 사용할 수 있다. 이런 경우도 확실히 테스트 해주어야하고 클래스가 옳바른 결과를 내는지 확인해보아야한다.

만약 다중 델리게이트 메소드를 가지고 있다면 당신의 클래스가 옳바른 것을 호출하는지 확인해야한다. 혹은 너무 많은 시간이 걸려 호출되었는지도 확인한다. 또한 로그인 콜에서 빈 패스워드를 입력하게 되었는지와 같은, 당신의 클래스가 잘못된 데이터를 전달하진 않았는지도 확인해야한다. 때론 일어나지 않을 것 같은 것을 검증하는것이 유용할 때도 있다.



여기까지 유팃테스트의 일반적인 개괄이었고, 좀 더 추상화된 기본과 함께 우리가 어떻게 할지에 대한 이야기를 해보았다. Part2에서는 어떻게 유닛 테스트가 가능한 방법으로 우리 코드를 설계할지에대해 더 이야기 해보겠다.



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

,

약 4달전, 우리팀(Marco, Arne, and Daniel)은 새 앱의 모델 레이어를 설계하기 시작했다. 우리는 테스트를 개발 과정에서 사용하고 싶었고, 회의를 거쳐 XCTest를 테스트 프레임워크로 정했다.

(테스트도 포함한) 우리의 코드베이스는 190개의 파일과 18,000 라인의 소스로 544KB까지 커져있었다. 우리 테스트에 들어가보면 우리가 테스트할 코드의 2배정도 되는  1,200KB 크기나 된다. 아직 프로젝트가 끝난 상황은 아니지만 거의 마무리 단계에 있다. 이 글을 통해 우리가 무엇을 배웠는지, 일반적인 테스트에 관하여나 XCTest에 관한 주제를 공유하고 싶다.

이 프로젝트는 아직 앱 스토어에 올라가지 않고 진행중이기 때문에 몇몇 클래스 이름이나 메소드 이름은 계속 바뀌어오고 있는 중임을 유의하라.

우리가 XCTest를 고른 이유는 간단하고 Xcode IDE와 잘 결합되기 때문이다. 이 글이 여러분의 XCTest를 고르거나 다른 것을 고를 때 결정을 도와줄 수 있길 바란다.

우리는 이 이슈와 비슷한 주장으로 이어가려 노력했다.

왜 테스트 해야하나
article about bad practices in testing에서 언급했듯, 많은 사람들이 "우리가 코드를 바꿀 때만 테스트 할 가치가 있다"고 생각한다. 이것에 대해 더 명확하게 짚고 싶으면 위 글을 읽어보면 된다. 그러나 사실 첫 버전의 코드를 작성할 때는 코드를 수정하는데 많은 시간이 들 수 밖에 없음을 인지해야한다.—프로젝트가 진행됨에 따라 더 많은 기능들이 추가되며, 그러면 코드 여기저기를 조금씩 수정해야 할 것이다. 따라서 1.1버전이나 2.0버전의 작업이 아니더라도 여전히 수많은 변경할 부분이 있을 것이고, 이때 테스트는 많은 도움을 줄 것이다.

우리는 아직도 최초버전의 프레임워크를 완성하는 과정에 있으며 최근데 10 man months 이상동안 1,000개의 테스트 케이스를 통과시켜 왔다. 우리 프로젝트 아키텍처가 명확한 버전을 가지고 있지만, 여전히 그 방법으로 코드를 수정하고 맞추고 있다. 계속 증가하는 테스트 케이스들은 이렇게 우리를 도와왔다.(원문: The ever-growing set of test cases have helped us do this.)

테스트는 우리 코드의 품질을 안정적으로 만들 수 있게 해주고, 코드를 부수지 않고 리팩토링이나 수정을 할 수 있는 능력을 가지게 해준다. 그리고 모든 코드가 합쳐지지 않아도 매일 코드를 실제 돌려볼 수 있게 해주었다.

XCTest는 어떻게 동작할까
애플은 XCTest 사용하기라는 문서를 제공한다. 테스트는 XCTestCase 클래스의 서브클래스 안에 그룹되어 만들어진다. test로 시작하는 각 메소드들이 실제 테스트이다.

테스트는 간단한 클래스나 메소드이기 때문에, 우리가 원하는 것에 맞춰 @property나 필요한 메소드를 테스트 클래스에 추가할 수 있다.

우리는 코드를 재사용하기 위해 모든 테스트 클래스의 수퍼클래스는 일반적으로 TestCase이다. 이 클래스(TestCase)는 XCTestCase의 서브클래스이다. 모든 테스트 클래스는 TestCase를 수퍼클래스로 한다.

또한 TestCase 안에 다 같이 사용하는 헬퍼 메소드도 하나 넣겠다. 그리고 각 테스트에 필요한 프로퍼티도 넣겠다.(원문: And we even have properties on it that get pre-populated for each test.)

네이밍
test라는 단어로 시작하는 메소드가 하나의 테스트이고, 일반적으로 테스트 메소드는 아래와 같이 생겼다:

우리 모든 테스트들은 "testThatIt"으로 시작한다. 테스트 네이밍에서 자주 쓰는 또 다른 방법은 testHTTPRequest처럼 테스트된 클래스나 메소드 이름을 사용하는 것이다. 그러나 이것은 가볍게 보기만해도 그 테스트의 의미를 바로 알 수 있을 것이다.

"testThatIt" 스타일은 우리가 원하는 결과에 초점이 쏠리고, 대부분의경우 한번에 이해하기 힘들다.

각 제품 코드 클래스의 테스트 클래스가 있고, 어떤 것은 Test가 접미에 붙기도 한다. HTTPRequestHTTPRequestTests클래스가 커지면 이것을 토픽에 따라 카테고리로 쪼개는 작업을 한다.

앞으로 영원히 테스트를 할 필요가 없으면 접두에 DISABLED를 붙인다:

이렇게하면 검색하기도 쉽고, 더이상 메소드 이름이 test로 시작하지도 않음으로 XCTest가 알아서 이 메소드를 생략한다.

Given/When/Then
우리는 모든 테스트를 Given-When-Then으로 나누어 만드는 패턴 구조를 사용한다.

given은 모델 오브젝트들을 만들거나 테스트를 위한 특정 시스템 상태로 만들어 테스트 환경을 셋업하는 영역이다. when은 테스트 하고 싶은 코드를 가지고 있는 영역이다. 대부분 테스트할 메소드 하나를 호출한다. then은 액션의 결과를 확인하는 역역이다. 우리가 기대하던 결과가 나왔는지, 오브젝트가 변경되었는지등을 확인한다. 이 영역은 assertion으로 구성되있다.

아래에 꽤 간단한 테스트가 있다:

이 기본 패턴을 따름으로서 더 짜기쉽고 이해하기 쉽게 해준다. 가독성을 높히기 위해 해당 영역의 상단에 "given", "when", "then"을 주석으로 달아놨다. 이 경우는 테스트된 메소드가 즉시 눈에 띈다.

재사용 가능한 코드
테스트를 여러번 하다보니, 테스트 코드 속에 자꾸 자꾸 재사용되는 코드를 발견했다. 비동기적 처리를 완료할때까지 기다리거나, CoreData 스택을 메모리에 옮기는 그런 코드들을 중복해서 사용하고 있었다. 우리가 최근에 사용하기 시작한 또다른 유용한 패턴은 XCTestCase 클래스에서 직접 프로토콜을 델리게이트하는 것을 구현하는 것이다. 이렇게 하면 엉성하게 델리게이트를 모의객체로 만들지 않고, 꽤 직접적인 방법으로 테스트 할 클래스와 소통할 수 있다.

It turned out that this is not only useful as a collection of utility methods. The test base class can run its own -setUp and -tearDown methods to set up the environment. We use this mostly to initialize Core Data stacks for testing, to reseed our deterministic NSUUID (which is one of those small things that makes debugging a lot easier), and to set up background magic to simplify asynchronous testing. 

Another useful pattern we started using recently is to implement delegate protocols directly in our XCTestCase classes. This way, we don’t have to awkwardly mock the delegate. Instead, we can interact with the tested class in a fairly direct way.

모의객체(Mocking)
우리가 쓰는 모의객체 프레임워크는 OCMock이다. 이 모의객체 주제의 아티클에서 이야기하듯, 모의객체는 메소드 호출에 준비된 결과를 반환하는 오브젝트이다.

우리는 모의객체를 한 오브젝트의 모든 의존성을 위해 사용한다. 이렇게 하면 타깃 클래스를 독립적으로 테스트할 수 있다. 단점이 있다면, 그 클래스에서 뭔가 바뀌게되면 그 클래스에 의존하는 다른 클래스의 유닛 테스트를 자동으로 실패로 만들지 않는다. 그러나 우리는 모든 클래스를 함께 테스트하는 통합 테스트를 하여 이 문제를 해결할 수 있다.

우리는 'over-mock'하지 않도록 주의해야하는데, 이것은 테스트할 하나를 제외한 나머지 모든 오브젝트를 모의객체로 만드는 것이다. 우리가 처음 시작할 때 이런 방식으로 테스트 했었고, 심지어 메소드에 입력하기위해 사용된 간단한 오브젝트까지도 모의객체로 만들었다. 이제는 많은 오브젝트를 모의객체 없이 사용하는 방법으로 테스트 하고 있다.

모든 테스트 클래스를 위한 우리 일반적인 슈퍼클래스의 일부이고, 한 메소드를 추가한다. 
이것은 메소드/테스트 마지막에서 검증하는 모의객체이다. 이것이 모의객체 사용을 더욱 편리하게 만든다. 우리가 만든 모의객체가 그 지점에 옳바르게 있는지 확인할 수 있다:

상태와 상태없음(State and Stateless)
지난 몇년동안 상태없는 코드를 많이 이야기해왔다. 그러나 결국 우리 앱은 상태를 필요로 했다. 상태가 없는 앱은 꽤 요점을 잃어버린다. 반대로 상태를 관리하면 그것이 굉장히 복잡하기 때문에 수많은 버그를 만들어 내기도 한다.

우리는 상태로부터 코드를 떼어내어 작업하기 쉽게 만들었다. 몇몇 클래스는 상태를 가지고 있으나 대부분의 클래스에는 상태가 없다. 또한 코드를 테스트하기도 아주 쉬워졌다.

예를들어 우리가 EventSync라는 클래스가 있는데, 이 클래스의 역할은 로컬의 변화를 서버에 보내는 것이다. 이것은 어떤 오브젝트가 서버에 갱신을 보내야하는지 현재 서버에 보내진 갱신들은 무엇인지 기억하고 있어야한다. 한번에 여러 갱신을 보낼 수 있지만 같은 갱신을 두번 보내서는 안된다.

또한 우리가 주시해야하는 오브젝트들 사이는 상호의존적이다. 만약 A가 B에 연관되있고 B에서 로컬 갱신이 일어나면, A 갱신을 보내기 전에 B 갱신을 먼저 서버에 보낼때까지 기다려 주어야 한다.

우리는 다음 요청을 만드는 -nextRequest 메소드를 가진 UserSyncStrategy를 가지고 있다. 이 요청은 로컬에서의 갱신을 서버로 보낼 것이다. 이 클래스 안에는 상태가 없으나 그 모든 상태는 UpstreamObjectSync 클래스 안에 캡슐화되어 들어 있는데, 이 클래스는 유저가 만든 모든 로컬 갱신에 대한 기록을 서버에 날린다. 이 클래스 바깥에는 상태가 없다.

이 경우 이 클래스가 관리하는 상태가 올바른지 체크한다. UserSyncStrategy의 경우 UserSyncStrategy를 모의객체로 만들어 UserSyncStrategy 내부의 상태에 더이상 신경 쓰지 않아도 된다. 이것이 테스트의 복잡도를 확 낮춰주는데, 수많은 다른 종류의 오브젝트를 동기화하고 있기 때문이다. 그러면 다른 클래스들은 상태가 없으며, UpstreamObjectSync 클래스를 재사용 할 수 있다.

Core Data
우리 코드는 굉장히 Core Data에 의존한다. 우리 테스트가 다른 하나로부터 독립되야하므로 각 테스트 케이스마다 명확한 Core Data 스택을 만들어야하고 그 후에 그것을 다시 원래대로 해야했다. 우리는 이 store를 한 테스트 케이스에만 사용하고 다음 테스트에는 다시 사용하면 안되었다.

우리 모든 코드는 다음 두가지 Managed Object Context 주변에 집중되있다: 하나는 유저 인터페이스가 사용하고 메인 큐에 묶여있는 것이고 다른 하나는 동기화를 위해 사용되며 자신의 개인 큐를 가지고 있다.

우리는 그들이 필요로하는 모든 테스트마다 Managed Object Context 자꾸자꾸 생성하길 원하지 않는다. 그러므로 공유된 TestCase 수퍼클래스의 -setUp 메소드에 두개의 Managed Object Context를 만들어둔다. 이것은 각 개별 테스트에서 가독성을 높혀준다.

Managed Object Context가 필요한 테스트는 간단하게 self.managedObjectContextself.syncManagedObjectContext를 호출하면 된다:

우리는 코드의 일관성을 만들기 위해 NSMainQueueConcurrencyTypeNSPrivateQueueConcurrencyType을 사용하고 있다. 그러나 독립적 문제 때문에 -performBlock: 상단에 우리만의 -performGroupedBlock:을 구현했다. 이것에 대핸 더 많은 자료는 비동기 코드를 테스팅하는 섹션에서 볼 수 있다. 

여러 컨텍스트를 합치기
우리 코드에는 두 컨텍스트를 가지고 있다. 프로덕션에는 -mergeChangesFromContextDidSaveNotification:의 의미로서 한 컨텍스트가 다른 컨텍스트와 합쳐지는 것에 굉장히 의존적이다. 우리는 동시에 각 컨텍스트 별로 독립된 퍼시스턴스 store coordinator를 사용하고 있다. 그러면 두 컨텍스트 모두 최소의 명령으로 한 SQLite store에 접근할 수 있기 때문이다.

그러나 테스트를 위해 약간 바꾸어서 메모리 store를 사용할 것이다.

테스트시 SQLite store를 사용하여 디스크에 두는 것은 디스크 store에서 삭제시 경쟁상태(race condition)를 만들기 때문에 동작하지 않는다. 이것은 테스트간의 독립성을 해칠 것이다. 반면 메모리에 store하면 매우 빠르게 동작하며 테스트하기도 좋다.

우리는 모든 NSManagedObjectContext 객체를 만들기 위해 팩토리 메소드를 사용한다. 기본 테스트 클래스는 이 팩토리 클래스를 약간 고쳐 모든 컨텍스트가 같은 NSPersistentStoreCoordinator를 공유한다. 각 테스트의 마지막에는 다음 테스트가 사용할 새 것이나 새 store가 있는지 확인하기 위해 공유하고 있던 퍼시스턴트 store coordinator를 버린다.

비동기적 코드를 테스트하기
비동기적인 코드는 조금 까다로울 수 있다. 그러나 대부분 테스트 프레임워크는 비동기적 코드를 위한 기본 기능을 지원한다.

NSString에 비동기적인 메시지를 가지고 있다고 해보자:

XCTest에서는 아래와 같이 테스트할 수 있다:
대부분 테스트 프레임워크가 이런식으로 되었다.

그러나 비동기 테스트의 주된 문제는 독립적으로 테스트 하기 힘들다는 것이다. 테스트 습관에 관한 글에서 말했듯, 독립(Isolation)의 첫 글자는 "I" 이다.(원문: Isolation is the “I“ in FIRST, as mentioned by the article about testing practices.)

비동기 코드에서 다음 테스트가 시작하기 전에, 현재 테스트의 모든 스레드와 큐가 완전히 멈추는 것을 확신하기 까다로울 수 있다.

이 문제에 대해 우리가 찾는 최고의 해결책은 dispatch_group_t라는 이름의 그룹을 사용하는 것이다.

혼자 두지 말고 그룹에 넣자
몇 우리 클래스들은 내부적으로 dispatch_queue_t를 사용할 필요가 있다. 몇 우리 클래스들은 NSManagedObjectContext의 private 큐에 블럭들을 넣는다.

모든 비동기 작업이 끝날때까지 -tearDown 메소드에서 기다린다. 이것을 하기위해 우리는 아래 보이는 것처럼 여러 일들을 한다.

테스트 클래스는 이런 프로퍼티를 가진다:

우리는 이것을 일반적인 수퍼클래스에 한번만 선언해 두었다.

다음으로 dispatch_queue나 그 비슷한 것을 사용하는 모든 클래스 안에 이 그룹을 넣었다. 예를들어 dispatch_async()를 호출하는 대신, dispatch_group_async()를 호출하였다.

우리 코드는 CoreData에 의존적이므로 NSManagedObjectContext에 호출하는 메소드도 추가하고
모든 Managed Object Context에 새 dispatchGroup 프로퍼티를 추가했다. 그래서 우리는 독립적으로 -performGroupedBlock:을 사용했다.

이렇게하여 모든 비동기 처리가 끝날때까지 tearDown 메소드에서 기다릴 수 있었다.

메인 루프에서 -tearDown이 호출된다. 메인루프에서 큐에 들어간 어떤 코드가 실행되었는지 확인하기 위해 메인루프를 끈다. 위 코드는 그룹이 비지 않는한 영원히 돌고 있다. 우리의 경우 타임아웃을 넣어 살짝 바꾸었다.

모든 작업이 끝날때까지 기다리기
이렇게 하면 수많은 다른 테스트들도 쉬워진다. 아래와 같이 사용할 WaitForAllGroupsToBeEmpty()를 만들었다:

마지막 라인은 모든 비동기 작업이 완료될때까지 기다리는 코드이다. 즉, 이 테스트는 추가적인 비동기 처리를 큐에 넣은 비동기 블럭들까지도 모두 끝나고 어떠한 것도 거절된 메소드를 호출하지 않는다.

이것을 만든한 메크로로 만들었고:
나중에는 공유된 TestCase 수퍼클래스에 메소드를 정의했다:

커스텀 예외
이 섹션의 초반부에서, 어떻게 이것을 하는지 이야기 했었고

비동기 테스트를 위해나 블럭을 만드는 기본이다.

XCTest는 NSNotification과 key-value observing을 위한 몇가지 약속이 존재하는데, 이 둘다 블럭을 만드는 최상단에서 구현될 것이다.

그러나 종종 여러 곳에서 이 패턴을 사용하고 있다는 것을 발견하였다. 예를들어 Managed Object Context가 비동기적으로 저장될거라 예상할 때, 우리 코드는 이렇게 생길 것이다:

이 코드를 공유된 한 메소드만을 호출하게하여 가볍게 만들었다:

그리고 테스트에서 사용할 때이다:

이렇게하면 가독성이 더 좋아진다. 이 패턴은 사용하면 다른 상황에서도 자신만의 커스텀 메소드를 추가할 수 있다.

The Ol’ Switcheroo — Faking the Transport Layer
앱을 테스트하는데 중요한 줄문 중 하나는, 어떻게 서버와 연동하여 테스트할 것인지 이다. 가장 이상적인 솔루션은 실서버를 로컬에 빨리 복사하고, 가짜 데이터를 제공하여 http를 통해 직접 테스트를 돌려보는 것이다.

사실 우리는 이러한 솔루션을 이미 사용하고 있다. 이 솔루션은 굉장히 실제와 유사한 테스트 환경을 제공한다. 그러나 현실적으로 너무 느리게 환경설정이 된다. 각 테스트마다 서버의 DB를 정리하는 것이 너무 느리다. 우리는 1000여개의 테스트를 가지고 있다. 실서버에 의존하는 30개의 테스트가 있는데, 만약 DB를 정리하고 서버 인스턴스를 깨끗히 만드는데 5초가 걸린다치면 적어도 2분 30초를 테스트를위해 기다려야 한다는 것이다. 그리고 또한 서버 API가 구현되기 전에 서버 API를 테스트할 수 있는 것도 필요했다. 우리는 뭔가 다른 것이 필요했다.

이 대안의 솔루션은 '가짜 서버(fake server)'이다. 우리는 서버와 통신하는 모든 클래스를 TransprotSession이라는 한 클래스와 통신하도록 구조를 짜고, 이 클래스는 NSURLSession과 비슷한 스타일이지만 JSON 변환까지도 처리해준다.

우리는 UI에 제공할 API 테스트들을 가지고, 서버와 통신하는 모든 것들은 TransportSession이라는 가짜 서버로 우회하여 두었다. 이 transport session은 실제 TransportSession과 서버 모두의 행동을 따라한다. 이 가짜 session은 TransportSession의 모든 프로토콜을 구현하여 그것의상태를 설정할 수 있게 해주는 몇 메소드를 추가한다.

OCMock를 사용하여 각 테스트에 커스텀 클래스를 가지는 것은 모의 서버(mocking the server)를 넘어 여러 이점을 가진다. 그중 하나는, 실질적으로 모의 서버를 사용하여 더 복잡한 시나리오를 만들어 테스트해볼 수 있다. 실제 서버에서는 시도해보기 어려운 극한의 상황을 시뮬레이트 해볼 수 있다.

또한 가짜 서버는 그 스스로 테스트를 가지므로 그 결과가 좀 더 정밀하게 정의되어 있다. 만약 요청에 대한 서버의 응답이 항상 바뀌어야 한다면 한 장소에서 오직 그렇게 한다. 이것은 가짜 서버를 사용하는 모든 테스트를 보다 더 튼튼하게 만들며, 우리 코드에서 새 기능이 잘 동작하지 않는 부분을 좀 더 쉽게 찾아낼 수 있다.

FakeTransportSession 구현은 간단하다. HTTPRequest 객체를 요청에 관한 URL, 메소드, 패이로드(payload)에 관련하여 캡슐화 시키면 된다. FakeTransportSession은 내부 메소드에 모든 끝부분을 매핑시키고 응답을 발생시킨다. 이것이 알고있는 오브젝트의 기록을 가지고 있기 위해 메모리에 담은 CoreData 스택까지도 가지고 있는다. 이렇게하여 PUT으로 추가된 이전 오퍼레이션의 리소스를 GET으로 반환할 수 있다.

이 모든것을 하기에 시간이 부족하다고 생각할 수도 있겠지만 사실 가짜 서버는 꽤 간단하다. 실제 서버가 아니며, 많은 부분을 떼어냈다. 가짜 서버는 오직 한 클라이언트에만 기능을 제공하기 때문에 퍼포먼스나 스케일리비티는 전혀 신경쓰지 않는다. 또한 한번에 큰 노력을 들여 모든것을 구현할 필요가 없다. 우리가 개발이나 테스트에 필요한 부분만 만들면 된다.

그러나 우리 상황의 경우, 우리 테스트를 시작할쯤엔 서버 API가 꽤 안정적이고 잘 정의되있었다.

커스텀 Assert 메크로
Xcode 테스트 프레임워크에선, 실제 확인을 위해 XCTAssert 메크로를 사용한다:
애플의 “Writing Test Classes and Methods” 글에 "카테고리로 정의된 Assertion의 모든 목록이 있다.

그러나 우리는 아래와 같은 특정 도메인을 체크하는 Assertion을 자주 사용했다:
이렇게하면 가독성이 너무 떨어지고, 코드의 중복을 피하기 위해 간단한 assert 메크로를 만들었다:

테스트 할때는 아래와같이 간단하게 사용하면 된다:

이 방법으로 테스트의 가독성이 굉장히 좋아졌다.

한단계 더
그러나 우리 모두가 알듯 C의 전처리기 메크로는 굉장히 난잡하다(a beast to dance with).

몇몇은 이것을 피할 수 없으며 그 고통을 줄이고싶은 것에 대한 이야기이다. 어디라인 어디파일에 assertion 실패가 생겼는지 알기 위해 테스트 프레임워크를 정렬하는 경우 메크로가 필요하다.(We need to use macros in this case in order for the test framework to know on which line and in which file the assertion failed.) XCTFail()은 메크로이고 __FILE____LINE__이 설정되는 것에 의존하고 있다.

좀 더 복잡한 assert와 체크를 위해 FailureRecorder라 불리는 간단한 클래스를 만들었다:

우리 코드에는 두 딕셔너리가 서로 일치하는지 확인해야하는 부분이 곳곳에 있는데, XCTAssertEqualObject()가 그것을 체크한다. 이것이 실패했을때 내뱉는 결과가 아주 유용하다.

우리는 이런식으로 하길 원했다:

결과에는

그래서 이렇게 메소드를 만들었다.

FailureRecord가  __FILE__, __LINE__, 테스트 케이스를 잡아내는 방법을 썼다. -recordFailure: 메소드는 그냥 간단하게 문자열을 테스트 케이스로 전달한다:

Xcode, Xcode 서버와 통합
XCTest의 최고 장점은 놀라울 정도로 Xcode IDE와 통합하기 좋다는 것이다. Xcode6과 Xcode6 서버와 함게 작업하면 더욱 빛을 발한다. 이 강력한 결합력은 생산성을 증진시키는데에도 큰 몫을 한다.

초점
테스트 클래스에서 한 테스트나 여러 테스트를 하고 있을 동안, 왼편 라인 넘버 옆에 있는 작은 다이아몬드는 특정 테스트나 테스트 집합을 실행시켜준다.



테스트에 실패하면 빨간색으로 되고:



성공하면 초록색이 된다:




^⌥⌘G 단축키는 마지막 테스트를 다시 돌려볼 수 있게 해주는데, 자주 사용하게 될 것이다. 다이아몬드를 클릭하고, 우리가 테스트를 변경하면, 키보드에 손 델 필요없이 간편하게 다시 테스트를 돌려볼 수 있다. 디버깅 테스트시 아주 유용하다.

네비게이터
(Xcode 왼쪽 창에 있는) 네비게이터는 Test Navigator라는 것인데, 클래스별로 모든 테스트를 묶어 보여준다:



그룹 테스트나 개별 테스트는 이 UI로부터 시작할 수도 있다. 더 유용한 점은 네비게이터 하단의 세번째 아이콘을 활성화시켜 실패한 테스트만 보여주게도 할 수 있다:




이어지는 통합
OSX 서버는 Xcode 서버라 불리기도 한다. 이것은 Xcode를 기반으로 이어지는 통합(continuous integration) 서버이다. 우리는 이렇게 사용해 왔다.

우리의 Xcode 서버는 github의 새 커밋이 들어올때 자동적으로 프로젝트를 체크한다. 우리는 스태틱 어널라이저를 실행하고 iPod touch나 다른 iOS 시뮬레이터에서 모든 테스트를 돌린 뒤 마지막으로 다운받을 수 있는 Xcode 아키브(archive)를 생성하도록 했다.

Xcode6에서는 Xcode 서버의 이 기능들이 복잡한 프로젝트에까지도 꽤 유용하게 쓰인다. 우리는 커스텀 트리거를 가지고 있는데, 이것은 배포 브런치에서 Xcode 서버의 빌드 부분을 실행한다. 이 트리거 스크립트는 생성된 Xcode 아키브를 파일 서버에다 올려둔다. 이렇게 함으로서 버전별 아키브를 관리할 수 있다. UI팀은 파일 서버로부터 미리 컴파일된 특정 버전의  프레임워크를 내려받을 수 있다.


BDD와 XCTest
당신이 만약 BDD(behavior-driven development)에 익숙하다면, 우리의 네이밍 스타일이 이 방식(BDD)에 영감을 받았다는 것을 알 수 있을 것이다. 우리 중 몇명은 Kiwi라는 테스트 라이브러리를 사용해 보았고 자연스럽게 클래스나 메소드의 동작에 집중함을 느꼈을 것이다. 그러면 XCTest가 그 좋은 BDD 라이브러리를 대체할 수 있을까? 대답은 아니오 이다.

XCTest가 간편하다는 것에는 장단점이 분명 존재한다. 당신이 클래스를 생성하고 "test"라는 단어를 접두에 붙인 테스트 메소드를 만들어서 그렇게 해도 된다. 게다가 Xcode와 XCTest는 최고의 통합을 자랑한다. 한 테스트를 실행하기 위해 왼편의 다이아몬드를 누르면 되고, 실패한 테스트들을 ㅅ ㅟㅂ게 걸러볼 수 있으며, 또한 테스트의 리스트 중에 원하는 테스트로 쉽게 이동할 수도 있다.

불행히도 당신에게 이런것들을 새로 배우기에 꽤 부담스러울 수 있는 양이다. 우리는 XCTest와 개발/테스트하면서 어떠한 장애물도 만나지 않았으나 종종 더 편하게 사용해왔다. XCTest 클래스는 일반 클래스처럼 보이지만, BDD 테스트 구조는 nested context가 있다. 이것은 테스트시 nested context를 만드는 것을 잊어버릴 수도 있다. nested context는 개별 테스트를 간단하게 하면 더 많은 특정 시나리오를 만들어내야한다. 물론 XCTest에서도 그렇긴하다. 예를들어 몇 테스트를 위해 커스텀된 초기화 메소드를 호출함우로써 말이다. 이것의 단지 편리함 때문만은 아니다.

BDD 프레임워크의 추가적인 기능이 얼마나 중요한지는 프로젝트의 크기에 따라 알게될 것이다. 우리의 결로은 다음과 같다. XCTest는 작은 사이즈나 중간 사이즈의 프로젝트에 적합하나, 큰 사이즈의 프로젝트에는 KiwiSpecta 같은 BDD 프레임워크를 사용하는 것이 더 낫다.

요약

XCTest가 옳바른 선택일까? 당신의 프로젝트에 따라 판단해야한다. 우리는 KISS의 부분으로 XCTest를 선택했고—다르게 해보고 싶었던 위시리스트를 가진다. XCTest는 우리가 어느정도 절충해야 하지만, 그 역할을 잘 한다. 그러나 다른 프레임워크에서는 다른 것들도 절충해야할 것이다. 



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

,

우리는 건축물을 만들고, 그 후에 건축물이 우리를 만드는 것을 아키텍처의 범주로 잘 알려져있다. 결국 모든 프로그래머가 배움으로서 이것은 그냥 소프트웨어 구축을 보다 더 좋게 하는데 적용된다.

우리가 짠 코드들이 간결한 아이덴티티가 부여되고 명확한 목적을 가지며, 각 코드가 논리적인 측면에서 서로 맞아 떨어지게 설계하는 것이 중요하다. 이것이 바로 소프트웨어 아키텍처가 의미하는 바이다. 좋은 아키텍처는 제품의 성공이 아니라 제품의 유지보수 용이성과 사람들이 유지보수를 할 때 제정신을 차리도록 멘탈을 보호해주는 것이다!

이 글에선 VIPER라 불리는 아키텍처를 iOS에 적용시켜 소개해보려고 한다. VIPER는 많은 큰 프로젝트에 사용되어 왔으나, 이 글의 목적상 간단한 to-do 리스트 앱을 통하여 VIPER를 보여줄 예정이다. 여러분은 여기 Github에 예제 프로젝트와 함께 따라오길 바란다.


VIPER란?
iOS 앱을 만들면서 테스트는 주요 작업이 아닐 때가 많다. 우리는 Mutual Mobile에 있을때 테스팅 절차를 들여라는 요사항이 들어왔는데, iOS 앱을 위해 테스트를 준비하는게 쉽지 않다는 것을 깨달았다. 우리는 소프트웨어를 테스트 할 수 있는 방법을 개선하기로 결정하였고, 앱의 아키텍처를 더 좋은 방법으로 만들 필요가 있었다. 그렇게해서 나온 해답이 바로 VIPER라 불리는 것이다.

VIPER는 iOS 앱들에게 클린 아키텍처의 어플리케이션이다. VIPER라는 단어는 VIew, Interactor, Presenter, Entity, Routing의 약자로 구성된다. 클린 아키텍처는 논리적인 구조를 앱의 책임별 각 층으로 나눠준다. 이것은 의존성을 고립시키고 각 층들 사이 경계에서의 상호작용을 테스트하기 쉽게 해준다.


대부분의 iOS 앱은 MVC(Model-View-Controller) 아키텍처를 사용한다. 앱에서 MVC 아키텍처를 사용하는 것은 당신으로 하여금 모든 클래스들이 model이자 view이자 controller라 생각들게 만들것이다. 그러므로 대부분의 앱 로직은 model이나 view에 들어가있지 않고 controller에 몰려있다. view controller가 비대해지면서 결국 MassiveViewControlle 문제가 되버리고 만다. 이 비대한 view controller의 중량을 줄여 코드의 질을 높히는 과제는 iOS 개발자만이 직면한 문제가 아니지만, 이러한 시도는 좋은 시작 시점에 와있다.

VIPER의 각 층은 명확한 위치에 앱 로직이 들어가고, navigation 관련 코드가 되도록 함으로서 이러한 과제를 해결하는데 도움을 준다. VIPER를 적용함으로서, 우리 to-do 리스트 예제에서의 view controller가 머신을 조종하는 view에 의존한다는 것을 당신은 인지하게 될 것이다. 또한 view controller에 있는 코드와 모든 다른 클래스들이 이해하기 쉽고, 테스트하기 쉬우며, 그리하여 유지보수하기까지 쉽다는 것을 깨달을 것이다.

유스케이스에 기반한 앱 설계
앱은 종종 유스케이스의 한 집합으로 구현된다. 유스케이스는 기준이나 동작을 수용하고 앱이 어떤 일을 의도하는지 설명하는 것으로 알려져 있다. 리스트는 날짜, 타입, 이름으로 정렬이 가능해야 한다고 정의하는 것이 유스케이스이다. 한 유스케이스는 앱에서 비즈니스 로직을 위한 기능인 한 층(layer)이다. 유스케이스들은 그들의 유저 인터페이스 구현으로부터 독립적이여야한다. 또한 그것들은 작아야하고, 잘 정의되있어야한다. 복잡한 앱을 더 작은 유스케이스로 어떻게 쪼갤지 고민하는 것이 과제이고, 이 부분은 숙달이 필요하다. 그러나 당신이 해결할 각 문제나, 당싱이 작성한 각 클래스들의 범위를 제한하는 방법을 사용하면 여러므로 도움이 될 것이다.

VIPER로 앱을 만드는 것은 각 유스케이스를 수행하는 요소의 집합을 구현하는 것을 포함한다. 앱 로직은 유스케이스를 구현하는데 있어서 중요한 부분이지만, 앱 로직만 구현해야하는 것은 아니다. 또한 유스케이스는 유저 인터페이스의 영향을 받을 수 있다. 추가적으로 네트워크나 데이터 퍼시스트와 같은 다른 중요한 요소들과 유스케이스가 어떻게 연결될지 고려하는 것도 중요하다. 요소들은 유스케이스에 플러그인처럼 동작하고, VIPER는 각 요소들의 역할이 무엇인지 그들이 어떻게 서로 상호소통할 수 있는지 표현하는 방법이다.

우리 to-do 앱의 유스케이스나 요구사항 중 하나는 유저 선택을 기반한 여러 방법으로 그룹을 짓는 것이다. 유스케이스에 데이터를 조직화하여, 로직을 쪼갬으로서, 우리는 유저 인터페이스 코드가 깔끔하게 유지되고, 우리가 예상한대로 동작하는지 확인하기 위한 테스트에서 쉽게 유스케이스를 적용할 수 있다.

VIPER의 주요 부분

아래는 VIPER의 주요 부분이다.
  • View : Presenter에 의해 요청 받은 것을 화면에 표시하고, 유조의 입력을 Presenter에게 넘겨준다.
  • Interactor : 유스케이스에 의해 부여된 비지니스 로직을 담고있다.
  • Presenter : (Interactor로부터 받은)content를 화면에 보여주기 위해 준비하거나, (Interactor 로부터 요청한 데이터인) 유저 입력에 반응하여 처리하는 View 로직을 담고 있다.
  • Entity : Interactor에 의해 사용되는 기본 모델의 객체를 담는다.
  • Routing : 명령에의해 어떤 화면으로 갈지 알고있는 navigation 로직을 담고 있다.

이러한 분리는 단일책임원칙(Single Responsibility Principle)에 들어맞게 된다. Interactor는 비지니스 해석의 임무를 가지고 Presenter는 인터렉션 디자이너를 나타낸다. View는 시각적 디자이너 임무를 맡는다. 

아래 다이어그램은 서로 다른 컴포넌트들이 어떤 식으로 연결되는지 보여준다.

VIPER 컴포넌트들은 어떤 순서로도 구현될 수 있지만, 우리는 추천하는 구현 순서대로 소개하고자 한다. 이 순서는 전반적으로 앱을 만드는 과정과 일치한다. 제품에서 이것을 하기 위해 무엇이 필요한지 토론하는 것부터 시작하여, 유저가 어떻게 상호 소통하는지 알려줄 것이다.

Interactor
Interactor는 앱에서 하나의 유스케이스를 의미한다. 이 컴포넌트는 특정 작업을 수행하기 위해 모델 오브젝트(Entity)를 다루는 비즈니스 로직을 가진다.  Interactor에서 완료된 일은 어떤 UI에도 독립적이여야한다. 한 Interactor로 iOS앱이나 OSX앱에서 동작될 수 있어야한다.

왜냐하면 Interactor는 로직을 1순위로 담은 PONSO(Plain Old NSObject)이기 때문에, TDD를 이용하여 개발하기 수훨해진다.

샘플 앱에서 첫째 유스케이스는 다가오는 to-do 아이템들을 보여주는 것이다.(예를들어 다음주까지 끝내야하는 어떤것..) 이 유스케이스의 비즈니스 로직은 오늘부터 다음 주말까지 사이에 있는 to-do 아이템을 찾고, 그 기간을 배정하는 것이다.(오늘, 내일, 이번주내, 다음주..)

아래 VTDListInteractor에 해당하는 메소드이다.


Entity
Entity들은 Interactor에 의해 사용되는 모델 오브젝트들이다. Entity들은 오직 Interactor에 의해서만 관리된다. Interactor는 절때로 Presentation 층에 Entity를 넘겨주지 않는다. Entity들 또한 PONSO이다. 만약 CoreData를 사용하면 managed object를 데이터 층 뒤에 남겨두고 싶어 할 것이다. Interactor는 NSManagedObjects와 함께 작업하지 않아야 한다.

여기 이것은 우리의 to-do 아이템을 위한 Entity이다.

Entity가 단순한 데이터 구조체처럼 생겼다고 너무 놀라지 마라. 모든 앱 의존 로직은 Interactor 안에 구현되 있을 것이다.

Presenter
Presenter도 PONSO인데, 이것은 UI에 넘겨주는 로직으로 구성되있다. Presenter는 유저 인터페이스를 언제 주는지 알고 있다. 이것은 유저 인터렉션으로부터 입력을 받고, UI를 갱신할 수 있으며, Interactor에게 요청을 보낼 수도 있다.

만약 유저가 새 to-do 아이템을 추가하기 위해 다른 UI를 present할지 wireframe에게 물어본다.


Presenter는 또한 Interactor에게 결과물을 받고, View에 표시되기 적합한 결과물로 한번 더 변환한다.

아래는 Interactor로부터 최근 아이템을 받은 메소드이다. 이것은 데이터를 처리하고 유저에게 어떻게 보여줄지 결정한다.


Entity는 절때 Interactor에서 Presenter로 넘어가지 않는다. 대신 간단한 데이터 구조체는 Interactor에서 Presenter로 넘어간다. 이러한 것은 Presenter에서 '실제 작업'을 행하는 일을 방지한다. 따라서 Presenter는 View에 띄우기 위한 데이터를 준비하는 것만이 가능하다.

View
View는 수동적인 녀석이다. 이것은 화면에 뿌릴 컨텐츠를 Presenter로부터 받을때까지 기다린다. 절때 Presenter에게 데이터를 달라고 직접 말하지 않는다. (로그인 화면의 LoginView와 같은) View에 정의된 메소드들은 높은 수준의 추상화를 통해 Presenter와 소통할 수 있게 해주며, 어떻게 그 컨텐츠가 화면에 표시되는지에대한 것은 아니다. Presenter는 UILabel, UIButton등등 이런 것들이 존재하는지 조차 모른다. Presenter는 오직 컨텐츠를 들고 있다는 것과, 언제 화면에 표시하는지만 안다. View는 화면에 어떻게 표시할지만 결정한다.

View는 Objective-C 프로토콜(Protocol)로 정의된 추상 클래스이다. UIViewController나 그것의 자식 클래스가 View프로토콜을 구현할 것이다. 우리 예제의 'add' 화면은 아래 인터페이스를 가진다.


View와 View Controller들은 유저 인터렉션과 유저 입력 또한 다룬다. 이것이 왜 View Controller가 커져버리는지의 이유이고, 그들에겐 입력의 몇 동작을 다루는데 가장 쉬운 위치이다. 유저가 어떤 동작을 취할때, View Controller 의존을 유지하기 위해 그것에게 필요한 부분의 정보를 주도록 해야한다. View Controller는 이 동작에 의해 어떠한 결정도 내려선 안되며, 할 수 있는 어떤것 사이에서 이 이벤트를 넘겨줘야 한다.

우리의 예제에서 AddViewController는 아래 interface를 따르는 이벤트 핸들러 요소를 가진다.


유저가 취소버튼을 누르면 View Controller는 유저가 add 동작을 취소했다고 이벤트 핸들러에게 말한다. 여기서는 이벤트 핸들러가 AddViewController를 dismissing 시키고 리스트 뷰를 갱신해라고 알린다.

ReactiveCocoa는 View와 Presenter를 연결해주는 역할을 한다. 이 예제에서 ViewController는 버튼 액션을 나타내는 신호를 반환하기 위해 메소드를 제공할 수도 있다. 이것은 역할을 나눌 필요 없이 Presenter가 쉽게 이 시그널을 응답할 수 있게 해준다.

Routing
한 화면에서 어디로 갈지의 기능은 인터렉션 설계자에의해 Wireframe에 정의되어있다. VIPER에서 Routing의 기능은 Presenter와 Wireframe 이 두 객체가 공유되게 하는 것이다. Wireframe 객체는 UIWindow, UINavigationController, UIViewController 등이 될 수 있다. 이것의 기능은 View나 ViewController를 생성하고 화면에 설치하는 일이다.

그러므로 Presenter는 유저 입력에 반응하는 로직을 가지고, Presenter는 언제 다른 화면으로 어디로 가는지 알고 있어야 한다. 한편 Wireframe은 어떻게 화면간 이동을 하는지 알고 있어야한다. Presenter는 Wireframe을 사용하여 화면 이동을 실행한다. 둘은 어느 화면에서 다른 화면으로 가게 해준다.

Wireframe은 또한 navigation transition animation을 다루는 명확한 위치에 있다. add wireframe인 이 예제를 보자.


Add View Controller를 띄우기 위해서 커스텀 View Controller Transition을 사용한다. 그러므로 Wireframe은 Transition 실행의 역할을 담당한다. 이것은 Add View Controller를 위한 Transition 델리게이트가 되고 이것은 적절한 Transition 애니메이션을 리턴할 수 있다.

VIPER에 맞춰진 앱 컴포넌트
iOS  앱 아키텍처는 앱을 만드는 주 도구가 UIKit과 CocoaTouch라는 사실을 고려할 필요가 있다. 이 아키텍처는 앱의 모든 요소들이 평화롭게 공존해야한다. 또한  프레임워크의 어떤 부분이 사용되는지, 그들이 어디에 있어야하는지 가이드라인을 제공해야 한다.

iOS 앱의 작업장은 UIViewController이다. 이것은 쉽게말해, MVC를 대체하는 그 대상은 View Controller가 무거워지는 것을 최대한 피해야한다. 그러나 View Controller는 플랫폼의 중심에 있다: 이들은 화면회전을 다루고,  유저 입력에 반응하며, navigation controller와 같은 시스템 요소까지 담고 있으며 iOS7부터는 화면 사이에 커스텀 Transition 까지 가능한데, 이 모든것이 View Controller에 들어가게되면 View Controller가 무거워 질 수 밖에 없다.

VIPER에서는 View Controller는 오직 View를 컨트롤하는 역할만을 한다. 우리 to-do 리스트 앱은 2개의 View Controller를 가진다. 하나는 리스트 화면이고 나머지 하나는 추가(add)화면이다. Add View Controller는 극단적으로 기본적인 것만 구현되있다. 왜냐하면 오직 View를 컨트롤 하는게 전부이기 때문이다.


앱들은 보통 네트워크를 탈 때 다소 부자연스러운 면이 있다. 그러나 어디서 네트어크를 타야하고, 네트워크를 타기위해 누가 초기화를 해줘야할까? 이 일은 보통 Interactor에서 일어나되, Interactor가 직접 네트워크를 타는 코드를 가지고 있지는 않는다. 이것은 Network Manager나 API Client같은 의존(dependency)에 물어볼 것이다. Interactor는 유스케이스 실행에 필요한 다양한 정보를 다양한 소스로부터 제공받아 모으는 일을 할 것이다. 그러면 Interactor로부터 Presenter에게 데이터를 넘겨주는데, 화면에 표시하기 적당한 형식에 맞춘다.

Data Store는 Interactor에게 Entity들을 주는 역할을 한다. Interactor는 그것의 비즈니스 로직을 감당하고 있으므로 Data Store로부터 Entity를 검색하고, Entity를 다루고, 그리고 Data Store에 Entity들을 집어넣어 저장하는 기능들이 필요할 것이다. Data Store는 Entity들의 퍼시스트를 관리한다. Entity들은 Data Store에 대해 전혀 모르므로 Entity들은 어떻게 그들 스스로 퍼시스트 되는지도 모른다.

Interactor도 마찬가지로 어떻게 Entity들을 퍼시스트 하는지 몰라야한다. 때때로 Interactor는 Data Store와 함께 그것의 인터렉션을 위해 Data Manager에서 불려진 오브젝트 타입을 사용하고 싶어한다. Data Manager는 패치(fetch) 요청 생성, 쿼리 만들기 등과 같은 Store에 특화된 오퍼레이션 타입을 다룬다. 이러한 것은 Interactor가 앱로직에 더 집중할 수 있게 해주며, 어떻게 Entity들을 모으고 퍼시스트 하는지 몰라도 된다. Data Manager를 사용하는 시점은 CoreData를 사용하는 시점과 같다. 아래 그 설명이 있다.

이것은 앱 Data Manager의 인터페이스 부분이다:


Interactor를 개발하기 위해 TDD를 사용할때, Production Data Store를 테스트 double/mock(옮긴이: 테스트를 위해 만든 가상의 더미코드, 껍데기코드)으로 전환할 수 있다. 원격의 서버를 사용하지 않고, 디스크 접근을 하지 않으면서 테스트 하는 것은 더 빠르고 더 많은 반복 테스트를 해볼 수 있다.

명확한 경계로 별개 층을 만들어 Data Store를 사용하는 이유는 당신으로 하여금 특정 퍼시스트 기술을 고르는데 지연할 수 있게 해준다(옮긴이: ?). 만약 당신의 Data Store가 하나의 클래스로 이루어져 있다면, 기본 퍼시스트 전략부터 앱을 만들어야하고, 그 다음 SQLite나 CoreData를 업그레이드하여 감각대로 만들면 당신의 앱 코드베이스에서 모든것이 바뀌어야 하게 될지도 모른다.

iOS 프로젝트에서 CoreData를 사용하는 것은 종종 아키텍처를 짜는 것 보다 시간이 더 걸릴 수도 있다. 그러나 VIPER와 함께 CoreData를 사용하면 여태껏 겪어보지 못한 최고의 CoreData를 경험 해볼 수 있을 것이다. CoreData는 유지보수의 빠른 접근과 낮은 메모리 접유율로 데이터를 퍼시스트 하는 좋은 툴이다. 그러나 이것은 NSManagedObjectContext가 앱 구현파일의 전체에 걸쳐 휘감아버리는 습관이 있다. VIPER는 Data Store 층에서 이것을 해결해준다.

여기 to-do 예제에서는 CoreData가 사용된다는 것을 아는 부분은 딱 두 부분이다. Core Data 스택을 설정하는 Data Store 자체와 Data Manager 이 둘이다. Data Manager는 패치 요청을 실행하는데, 표준 PONSO 모델 오브젝트로 Data Store에의해 반환된 NSManagedObject들을 변환하고, 이것을 비즈니스 로직 층에 넘겨준다. 이런 방법은 앱의 코어가 Core Data에 절때 의존하지 않게 된다. 추가적으로 불완전한 스레드의 NSManagedObject 동작을 걱정할 필요가  없다. 이것은 CoreData Store 요청을 만들어 낼 때의 Data Manager 내부 모습이다.


대부분의 CoreData 작업은 UI 스토리보드에서 일어난다. 스토리보드는 수많은 유용한 특징들이 있고, 전적으로 실수를 줄여주는 장점이 있다. 그러나 스토리보드의 기능만을 사용하여 작업하기엔 VIPER의 모든 목적을 달성하기 어렵다.

우리가 만들고자 하는 절충안은 segue( https://developer.apple.com/library/ios/recipes/xcode_help-IB_storyboard/Chapters/StoryboardSegue.html )(옮긴이 : 스토리보드에서 드레그 드롭으로 화면간의 이동을 연결해주는 방식)들을 사용하지 않는 방법이다. 우리는 종종 segue들을 사용하여 화면을 만드는 경우가 있을 수 있지만, segue를 사용하면 화면들 사이에–UI와 앱 로직 사이도 마찬가지로–분리를 유지하기가 쉽지 않을 것이다. prepareForSegue 메소드를 필연적으로 구현해야 할 때 최고의 방법은 segue를 쓰지 않으려 노력해야한다.

다른 경우에 스토리보드들은 유저 인터페이스를 위한 레이아웃 구현이 잘 되있다(특히 Auto Layout 같은). 우리는 스토리보드를 이용한 to-do 리스트 화면 구현과 우리 고유 navigation 동작과 같은 것을 코드를 이용한 구현으로 둘 다 채택하였다.


모듈을 만들기 위해 VIPER를 사용하기
VIPER를 사용하는 종종, 한 화면이나 화면들의 집합이 하나의 모듈로 나뉘는 경향을 보게 될 것이다. 한 모듈은 여러 다른 방법으로 구현될 수 있으나, 일반적으로 이러한 방법(화면 단위로 나누는)이 적당할 때가 많다. 팟케스팅 앱에서, 한 모듈은 오디오 플레이어 혹은 구독 브라우저가 될 수 있다. 우리 to-do 리스트 앱에서, list와 add 화면은 각 다른 모듈로 만들어 질 수 있는 것이다.

모듈들을 만듦으로써 앱 설계를 하는것은 몇가지 이점이 있다. 그 중 한가지는 모듈들이 굉장히 깔끔하고 잘 정의된 인터페이스를 가질 수 있으며, 게다가 다른 모듈에게 독립적이다. 이러한 이유로 기능을 넣고 빼기가 쉬우며 여러분의 인터페이스를 유저에게 보여주는 방법도 쉽게 바꿀 수 있다.

우리는 to-do 리스트 예제에서 모듈 사이에 분리를 굉장히 명황하게 하고자 원했고, 우리는 add 모듈을 위해 두개의 protocol을 정의했다. 첫번째는 모듈 인터페이스로서, 모듈이 무엇을 할 수 있는지 정의했다. 두번째는 모듈 델리게이트로서, 모듈이 무엇을 했는지 보여준다. 아래는 예제이다:


그러므로 한 모듈은 유저에게 어떤 의미를 지니도록 보여질(present) 수 있고, 모듈의 Presenter는 보통 모듈 인터페이스를 구현한다. 다른 모듈에서 이것을 보여주고 싶을 때, 그것의 Presenter는 모듈 델리게이트 프로토콜을 구현해야하고, 모듈이 보여질 때 모듈이 무엇을 했는지 알고 있을 것이다.

한 모듈은 여러 화면에서 사용될 수 있는 Entity, Interactor, Managerdls 인 일반적인 앱 로직 층을 가지고 있어야 한다. 물론 이것은 화면들 사이에 인터렉션에 의존하며 비슷하게 생겼다. 쉽게 to-do 리스트를 예로들어 한 모듈은 오직 한 화면으로 표시될 수 있다. 이 경우에는 앱 로직 층이 그것의 각 모듈의 동작에 굉장히 특화될 수 있다.

모듈들은 코드를 조직화하기에 꽤 좋은 방법이다. 만약 당신이 뭔가 바꾸고 싶을때 XCode에서 모든 코드를 그것의 그룹이나 폴더에 집어 넣어 둔다면 다시 찾기 쉬워질 것이다. 그리고 클래스가 어디 있는지 당신이 예상하는 곳에 있게될 것이다.

VIPER에서 모듈화를 시킬때 또 한가지 장점은 여러 형태로 쉽게 확장할 수 있다는 것이다. Interactor층에 고립된 모든 유스케이스의 앱 로직을 가지는 것은 앱 층을 재사용함으로서 테블릿, 폰, 맥의 새 유저 인터페이스를 만드는데 집중할 수 있게 해준다.

한걸음 더 나아가서 아이패드를 위한 유저 인터페이스는 아이폰의 View, View Controller, Presenter를 재사용 할 수 있을 것이다. 이 경우 아이패드 화면은 아이폰에서 쓰인 Presenter와 Wireframe을 'super' 함으로써 표현될 수 있다. 다중 플랫폼을 지원하면서 개발하고 유지보수하는 것은 꽤 도전일 수 있으나, 모델과 앱 층을 재사용하는 좋은 아키텍처라면 이것을 쉽게 도와줄 것이다.

VIPER에서 테스트
이 VIPER는 일을 쪼개도록 도와주는데, 이것이 TDD를 쉽게 적용시키게 만들어준다. Interactor는 어떤 UI에도 독립적인 순수히 로직만을 담고 있는데, 이것은 테스트하기 쉽게 해주는 역할을 한다. Presenter는 화면에 보여주기 위한 데이터를 준비하는 로직을 가지며, 이것은 어떤 UI 위젯에도 독립적이다. 이 로직을 개발하는 것 또한 테스트하는데 쉽게 도와준다.

우리의 필요한 메소드는 Interactor에서  시작될 것이다. UI에서 모든 것들은 유스케이스의 필요한 것을 그들에게 나를 것이다. Interactor API를 테스트하기 위해 TDD를 사용함으로서, 당신은 UI와 유스케이스간의 관계를 더 잘 이해할 수 있게 될 것이다.

이 예제에서, 다가오는 to-do 아이템 리스트를 위한 Interactor의 역할에 대해 살펴볼 것이다. 다가오는 아이템을 찾는 방법은, 우선 다음 주말까지 모든 아이템을 검색하고, 오늘, 내일, 이후 이번주, 다음주 별로 시간에 따라 묶는 것이다. 

우리가 작성한 첫번째 테스트는 Interactor가 다음 주말까지의 모든 to-do 아이템을 찾아내는지 확인하는 것이다.


Interactor가 적절한 to-do 아이템을 위해 요청한다는것을 알고, 그것이 정확한 날짜와 연관하여 올바르게 to-do 아이템들을 배치하는지 확인하기 위한 여러 테스트를 만들면 된다.


이제 Interactor의 API가 어떻게 생겼는지 알고 있으므로 Presenter를 만들어 볼 수 있다. Presenter가 Interactor로부터 다가오는 to-do 아이템들을 받게되면 이것이 적절한 양식의 데이터인지 UI에 띄울 수 있는지 테스트하고 싶을 것이다.


또한 유저가 새 to-do 아이템을 추가하기 원할때 앱이 적절한 동작을 취하는지도 테스트하고 싶을 것이다.


이제 View를 개발할 수 있다. 만약 다가오는 to-do 아이템이 하나도 없다면, 특정 메시지를 띄울 필요가 있을 것이다.


화면에 띄울 다가오는 to-do 아이템이 있다면, 테이블이 보여지길 원할 것이다.


먼저 Interactor를 만들면 TDD에 맞추기 편해진다. Presenter에 따라 Interactor부터 먼저 개발하면 이 층 주변에 테스트들을 만들어 낼 수 있고, 이 유스케이스를 구현하기 위한 기반을 설치할 수 있다. 그것들을 테스트하기 위해 UI와 연결될 필요가 없으므로 이 클래스들을 빠르게 재사용할 수 있을 것이다. 그러고 View를 개발할 때 테스트된 로직과 디스플레이될 층을 서로 연결만 시켜주면 되겠다. 모든 테스트가 잘 동작했다면, View 개발을 다 끝냈을 땐 한번에 앱의 모든 기능이 잘 동작함을 확인할 수 있을 것이다.

결론
VIPER 소개에 즐거웠기를 바란다. 많은 사람들이 이제 다음으로 무얼 해야할지 고민하고 있을 것이다. 만약 당신의 다음 앱을 VIPER를 써서 만들고 싶다면, 어디서부터 시작할 수 있을까?

이 글과 VIPER를 사용한 우리 예제는 우리가 만들 수 있는한 최대한으로 명확하고 잘 정의되게 했다. 우리의 to-do 리스트 앱은 꽤 같단하지만서도 어떻게 VIPER를 이용하여 앱을 만드는지 정확하게 설명하고 있다. 실제 프로젝트에서 당신의 과제와 제약사항들이 이 예제와 얼마나 비슷할지는 모르겠다. 우리의 경험에서는 각 프로젝트에서 VIPER를 조금씩 사용하도록 조금씩 바꾸었고, 바꾼 프로젝트 모두 그러한 접근법을 가이드하기위해 그것을 사용하는 것으로부터 좋은 이점을 얻었다.

다양한 이유로 VIPER에 의해 놓인 길로부터 탈선하고 싶은 생각이 들 수 있다. 아마 '토끼(bunny)' 객체를 양토장에 넣거나, 당신의 앱이 스토리보드에서 segue를 사용하고 싶을 수도 있을 것이다. 괜찮다. 이런 경우에는 당신이 어떤 결정을 내리든 VIPER가 어떤 표현을 할지 그 정신만을 기억하면 된다. 그것의 핵심은 VIPER가 단일 책임 원칙(Single Responsibility Principle)을 기반한 아키텍처라는 것이다. 당신에게 뭔가 문제가 생기면 어떤것을 먼저 옮길지 결정할 때 이 원칙을 생각해보아라.

그리고 또한 당신은 이미 존재하는 앱에 어떻게 VIPER를 적용시킬지 고민할 수도 있다. 이 경우, 새 기능이 추가될때 VIPER를 고려하여 추가해보아라. 우리는 이미 존재하는 프로젝트들도 이러한 루틴을 타고 있다. 이렇게 하면 VIPER를 이용한 모듈을 만들 수 있고, 또한 단일 책임 원칙을 기반으로 한 아키텍처는 현재 적용시키기 어려운 이슈들을 해결하는데 도움을 줄 것이다.

소프트웨어를 개발하는데 있어 재미있는점 중 하나는 모든 앱이 각기 다르다는 것이다. 따라서 각기 다른 앱마다 서로 다른 아키텍처를 구현할 수 있다. 우리는 모든 앱마다 새로운 배움의 기회를 얻을 수 있고 새로운 것을 시도한다. 만일 여러분이 VIPER를 사용하기로 결정하면, 몇가지 새로운 어떤 것을 배우게 될 것이라 생각된다. 읽어주어서 감사하다.

Swift 부록
저번주에 애플은 WWDC에서 Cocoa, Cocoa Touch 개발로서 Swift 프로그래밍 언어를 소개했었다. Swift 언어에 대해 복합적인 견해를 만들기에 좀 이르지만, 언어는 소프트웨어를 어떻게 설계하고 만드는지에 가장 큰 영향을 줄 것이라 생각된다. 우리는 VIPER가 무엇인지 배우는데에 돕기위해 Swift를 이용한 VIPER to-do 앱을 새로 만들었다. 우리는 Swift의 몇 특징을 찾았는데, VIPER를 이용해 앱을 만들기에 더 향상된 경험을 할 수 있었다.

Structs
VIPER에서 우리는 각 층 (Presenter에서 View로) 사이에 데이터를 주고 받기 위해 가벼운 모델 클래스를 사용한다. 이 PONSO들은 정말 간단하게 작은 양의 데이터를 다루는 경향이 있고 자식 객체가 되지 않으려는 경향이 있다. Swift 구조체들은 이 상황과 아주 맞아 떨어진다. 여기 VIPER Swift 예제에서 사용된 구조체의 예시가 있다. 이 구조체는 동일한지 체크하는 것이 필요했고, 이 타입의 두 객체를 비교하기 위해 == 연산자를 오버로드 하였다.


Type Safety
Objective-C와 Swift의 가장 큰 차이는 어쩌면 타입을 어떻게 다루는지이다. Objective-C는 다이나믹 타입이고 Swift는 컴파일 시간에 이것이 어떻게 구현되는지 타입체크를 함으로서 굉장히 의도적인(intentionally) 제약을 가진다. VIPER와 같은 아키텍처에는 앱이 여러 층에 걸쳐 구성될 때, type safety가 프로그래머의 효율면이나 아키텍처 구조 측면에서 크게 도움을 줄 수 있다. 컴파일러는 컨테이너를 확신할 수 있게 도와주고 오브젝트는 그들이 각 층 경계를 지나갈 때 옳바른 타입인지 확신할 수 있게 도와준다. 위에서 보여주듯 구조체를 사용하기에 좋은 장소이다. 만약 한 구조체가 두 층 사이에 경계에서 존재해야 한다면, 두 층 사이에서 어딘가로 사라져버리지 않는다는 이점을 얻을 것이다. type safety에 감사하라.


iOS 아키텍처 관련 번역글



번역에 도움을 주신 분 :



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

,


iOS 개발자들은 iOS앱이 Model-View-Controller(MVC) 디자인 패턴으로 만들어진다고 말할 것 이다. 또한 그들은 이렇게 말한다. 그 패턴은 Massive View Controller라고.. 그 이유는 코드가 View와 Model에 명확하게 분배되지 않고 Controller에 코드를 쑤셔넣게 됨으로서, Controller는 비대해지고 한 클래스가 어마어마하게 커져버려 유지보수하기 힘들게 만들어버리기 때문이다.

Brigade의 iOS앱을 만들면서 우리는 몇가지 명확한 키포인트를 잡아 아키텍트를 짜길 원했다.
  • 재사용에 용이함
  • 동업에 적합함
  • 일을 잘 분배할 수 있음
  • 테스트하기 쉬움

그리고 우리는 VIPER라는 아키텍처를 만나게 되었고, 위 요구사항과 잘 들어맞아 보였다. VIPER는 Mutual Mobile에서 개발되었고, 그들은 이 블로그 글을 포함한 이상으로 VIPER를 소개한 멋진 블로그글을 써놓았다. 우리는 애플의 표준에서 벗어나 VIPER를 한 번 사용해보기로 결심했다.

VIPER는 무엇이고 어떻게 이것을 사용하는가?
만든 사람에 의하면, VIPER는 앱 아키텍처를 만들기 위한 가이드를 제공하며, 개별의 앱들을 맞춰 넣을 수 있다. 뒤에서 VIPER가 무엇인지. 그리고 어떻게 우리의 원하는 것들을 맞춰 넣을 수 있는지 이야기 해볼 것이다.

VIPER를 가장 빠르게 이해할 수 있는 방법은 MVC에 대한 모든것을 잊어버리는 것이다. VIPER는 완전히 새로운 종족이라 생각하고, 만일 아직 MVC를 마음에 담아두고 있다면 VIPER를 이해하는데 어려움을 겪을 것이다. 당신이 iOS 앱 구조에 대해 하나도 모른다고 생각하라. 이제부턴 MVC는 없다.

VIPER는 몇몇 역할 중 하나에 기능을 분리해 넣는 것이 목적이다.
  • View/User Interface
  • Interactor
  • Presenter/Event Handler
  • Entity
  • Router/Wireframe
Note : 다른 블로그에 가면 위 용어 대신 다른 용어를 사용할 수도 있음에 유의하라.

여기 예제에서는 몇개의 역할을 더 넣을 것 이다. 바로 Data Manager와 Service이다.

아래 그림은 우리 앱의 일반적인 VIPER "stack"(나중에 설명하겠다)의 다이어그램이다. 각 박스는 클래스 하나하나를 의미하고 그것을 잇는 선은 각 클래스의 객체를 참조한다는 뜻이다.




각 클래스들을 일꾼이라 생각하고 선으로 연결되었다고 생각해보자. 각 클래스들은 제한된 동작만 할 수 있도록 되있고, 다른 클래스에 의존하여 도움을 받아 작업을 완료한다.

이제 각 클래스의 첫번째 요소에대해 알아보고, 예제를 통해 유저의 인터렉션(interaction)에서부터 유저에게 처리된 데이터를 보여주기까지의 과정을 짚어갈 것이다.

View/User Interface



View의 책임
  • 유저게에 정보를 표시
  • 유저의 인터렉션을 감지
View는 Presenter에의해 어떤걸 보여줄지 표시하고, event가 일어나게되면 Presenter에게 알려준다.

유저에게 정보를 표시
View가 유저에게 에러를 표시하려 한다면 Presenter는 아래와 같이 메소드를 호출할 것이다.

그러면 요구한대로 에러를 화면에 띄우기 위해 경고뷰나 레이블등을 View에 표시할 것이다. 여기서는 어떻게 화면에 에러를 표시하는지에대해서 Presenter가 관여하지 않는다는 것이 중요하다. Presenter는 오직 표시가 되있는지 그 자체에만 관심이 있다.


유저 인터렉션을 감지
로그인 버튼 누르기와 같은 이벤트가 발생하면 View는 Presenter에게 아래와 같은 메소드를 호출 할 것이다.

이전에도 보았드시 메소드를 만드는 오브젝트는 그냥 한번 호출하여 다음 일꾼이 그 작업을 처리하도록 넘겨준다. View가 관여하는 한 유저의 반응을 감지하여 그 작업을 완료하고 Presenter에게 이벤트 형태로 일을 준다.


Presenter/Event Handler


Presenter의 책임

  • 무엇을 표시할지 View에게 말함
  • 이벤트를 관리(다룬다)
Presenter는 View에게 무엇을 표시할지 말해줘야하고 이벤트들을 적절하게 다룬다.

무엇을 표시할지 View에게 말함
우리는 방금, Presenter의 책임중 하나가 무엇을 표시할지 View에게 말하는 것이라 했다. 이 작업을 완료하기 위해서는 마치 도배업자(decorator)나 진행자(presenter)처럼 행동해야한다. View를 위해 알맞은 데이터로 맞추고 나면 의미있는 방법으로 표시될 수 있다.

이전에 에러 예제로 돌아가보자. Presenter는 에러를 받는게 에러 오브젝트를 받는 것처럼 하였다. View에 바로 에러를 뿌리는게 아니라, 적절한 메시지로 에러를 풀어서 View에게 던져주었다.

이벤트를 관리
Presenter는 보통 View나 Interactor, 이 두 경우로부터 메소드가 호출된다.

View 이벤트의 경우
Presenter가 View에의해 이벤트를 받으면 이 이벤트를 처리해야한다. 이것은 보통 Interactor에게 정보를 검색해달라고 하거나 몇 작업을 수행해달라고 요청하는 일이다.

우리의 로그인 예제에서 Presenter가 View로부터 특정 유저 이름과 패스워드를 담아 로그인을 시도하는 이벤트가 발생했다고 알림을 받는다. 그러면 Presenter는 적당한 메소드를 호출함으로써 Interactor에게 일을 넘길 수 있다.

아마 당신은 Presenter에서 아무런 처리도 하지 않음을 짐작할 수 있을것이다. Presenter의 주 목적은 이벤트를 관리하는 것이며 Interactor에게 작업을 넘겨주는 일을 한다.


Interactor 이벤트의 경우
Presenter는 또한 Interactor로부터 이벤트를 받을 수도 있다. Interactor에서 작업이 끝나고 유저가 작업의 결과를 알아야 할 때 이러한 상황이 발생할 것이다.

예를들어 로그인 시도에 실패할 경우, Interactor는 아래와같이 Presenter에게 알릴것이다.

Presenter는 이 에러를 받고, 사용자에게 보기 좋은 문자열로 바꾼뒤, View에게 화면에 표시하라고 말 할 것이다.

Interactor

Interactor의 책임
Interactor는 데이터를 주변에서 가져와서 앱의 비즈니스 로직을 실행시킨다.

비즈니스 로직을 실행시키기
Interactor는 View가 Presenter에게 넘긴 이벤트를 어떻기 처리해야 할지에 대해서만을 알고있다.

예를 들어보자. 당신의 앱이 동기적으로 통신하는 2개의 네트워크 요청을 백그라운드에서 API 호출 하였다고 해보자. B라는 요청은 A요청의 응답 결과를 이용해야하므로 A요청이 끝날때까지 B요청을 해서는 안된다. 이것은 Presenter에서 Interactor로 호출하는 메소드로부터 작업이 시작된다.


여기서 반드시 기억해야할 사실은 Interactor가 직접 네트워크를 타지는 않는다는 것이다. 사실 Presenter는 실제로 네트워크 요청이 있는지 없는지 조차도 모른다. 아는것이라곤 Data Manager(나중에 이 개념에 대해 이야기 할 것이다)가 Entity 형태로 데이터를 넘겨주는 정도이다. 이것을 기억하고 위에서 호출된 Interactor 메소드는 아래와 같이 생겼을 수 있다.


Interactor는 Data Manager에 메소드를 호출하고 콜백을 받아 결과를 Presenter에게 돌려준다. 여기서 Interactor는 'performMyTask'가 'fetchFooWithCallback:'과 'fetchBarWithFoo:callback:'이 호출된다는것을 알고있고, 'fetchFooWithCallback:'의 콜백 블럭(block)에서 호출된다는 것까지 알고있다. 이 경우 Interactor는 앱의 "비즈니스 로직"을 다룬다고 할 수 있겠다.

Data Manager


Data Manager의 책임
  • 데이터를 검색
  • 데티러를 저장(부가적인)
Data Manager는 어디에서 데이터를 검색할 수 있는지, 저장할 수 있는지 없는지 알고있다.

데이터를 검색
Interactor 예제에서 보았듯 Data Manager에게 쿼리를 날리고 필요한 Entity들을 받아낼 수 있다. 우리는 그 데이터가 어디서부터 오는지 몰라도 된다. Data Manager가 알아서 관리해주기 때문이다.

Data Manager는 어디서부터 특정 데이터를 검색할지나 어디서 특정 리퀘스트를 수행하는지 정확하게 안다. 예를들어 Interactor는 Data Manager에게 사용자 객체를 요청할 수 있다:


만일 이 앱이 백엔드 서버 동작을 사용하고 있다면, 사용자 데이터를 검색할 수 있는 네트워크 요청을 누가 가지고 있는지 Data Manager는 알고 있을 것이다. 이 앱이 서버를 사용하지 않는다면 Data Manager는 네트워크 대신 로컬 persistent store로부터 데이터를 검색할 것이다. 네트워크 요청이 있는 경우, Data Manager는 Service 객체와 함께 쿼리를 날린다.


Service는 좀 있다가 설명하고, 여기서의 키포인트는 특정 정보를 검색하기 위해 어떤 Service를 호출해아하는지 알고 있다는 것이다. 이제 정보를 받으면 그 정보를 Interactor에게 넘긴다.

데이터를 저장
Persisting 데이터는 Data Manager의 관심사이기도하다. 서버로부터 받은 데이터를 패치(갱신)하기 전에 잠시 들고 있는 목적으로 Data Manager는 언제 데이터를 저장하는지 알고 있어야한다. 주 요점은 어떤 다른 클래스도 어떤 데이터가 persistent한지 모른다. 왜냐하면 Data Manager가 추상화를 했기 때문이다. 이 예제는 위 코드를 수정한 코드이다.


이제 하나하나 파헤쳐보자.
  • Data Manager는 먼저 'User ID'를 넘겨줌으로써 일치하는 해당 유저가 있는지 확인한다.
  • 만일 유저를 찾으면 Interactor에게 알리고 메소드는 return된다.
  • 만일 유저를 못 찾으면 Data Manager는 백엔드에서 유저를 찾아보고 서버를 이용하여 처리한다.
  • 서버로부터 유저 정보를 받으면 유저는 첫 persistent가 되고 interactor에게 알린다.
persisting 데이터는 그것이 소유하는 넓은 범위를 말하며, 다양한 형태로 구현될 수 있다. 여기서의 예제는 단순히 가르쳐주기 위한 스키마이지만 Data Manager에서의 모든 추상화 아이디어는 VIPER의 키포인트를 담는다.

Service

Service의 책임
  • 특정 Entity들을 위해 서버로 네트워크 요청을 날린다.

Service 오브젝트는 VIPER의 필수요소는 아니다. 그러나 이것은 굉장히 유용하다.

특정 Entity들을 위해 서버로 네트워크 요청 날리기
우리는 네트워크 요청의 DRY(Don't Repeat Yourself)에 Service가 좋은 역할을 할 것이라는 것을 알아냈다. 이 생각은 한개의 Service는 한개의 Entity 타입을 다루고, 이 Service는 Entity 타입의 다양한 수행들을 위해 어떤 네트워크 요청이 필요한지 알고있다.

예를들어, 사용자 Entity를 위한 Service가 있을 수 있다. 이 헤더파일은 이 클래스를 위한 것이다.


위 Service는 어떻게 유저를 생성하고, 로그인하고, 유저 정보를 갱신하는지 알고있다. ID를 주어서 유저 오브젝트를 갱신하는 것은 앱에 다양한 곳에서 사용할 수 있고, 이것은 Service 오브젝트의 코드가 DRY를 지켜가며 다룰 수 있다는 뜻이기도 하다.

Entity


Entity의 책임
  • 데이터를 표현한다.
Entity는 꽤 직관적이고 여러분이 예상하는 바와 비슷하다. 이것들은 데이터의 타입을 정의하고 다른 클래스들 사이에서 넘겨지면서 "payload" 역할을 한다. 예를들어 Data Manager는 한 Entity를 Interactor에게 반환하고, 다음 이것이 Presenter에게 반환되어, 마지막으로 View가 그것을 받아 화면에 표시한다.

Router/Wireframe

Wireframe의 임무
  • 모든 다른 클래스 객체들을 초기화
  • 앱에서 다른 View로의 경로를 다루기
Router/Wireframe은 앱에서 다른 VIPER 컴포넌트 모두를 하나로 합쳐주고, 한 View에서 다른 View로 navigating하는것을 도와준다.

모든 다른 클래스 객체를 초기화
여러분은 아마 VIPER 클래스들이 어떻게 초기화되는지, 어떻게 서로 소통하는지 궁금할 것이다. 그 일은 Router/Wireframe이 할 것이다.

VIPER에서는 한 "stack"이 View, Presenter, Interactor, Data Manager, Service, Entity로 구성된다. 이 "stack"은 모듈로서 표현될 수 있는 VIPER이다. 한 VIPER 모듈은 하나의 유스케이스와 일치한다. 한 유스케이스는 당신의 앱이 유저를 위해 실행할 수 있는 어떤 기능이다.

예를들어 많은 앱들을 위한 일반적인 한 유스케이스로는 계정을 이용한 로그인 허가이다. 여기서 앱의 "로그인"화면을 위해 특정 VIPER 모듈을 만들 수 있다. 이 모듈의 기능이다.
  • View는 로그인 화면을 표시한다.
  • Presenter는 특정 유저이름, 패스워드를 받아 로그인 요청을 날리는 그런 이벤트들을 다룬다.
  • Interactor는 유저 로그인 시도와 같은 이벤트를 위해 Data Manager에게 어떤 ㅁ[소드가 필요한지 알고있다.
  • Data Manager는 어떤 Service가 검색에 사용되는지, 어떤 Service가 서버로 정보를 보낼 수 있는지 알고있다.
  • Service는 실제 요청을 보내기 위해 HTTP URL을 알고있다.
  • Entity는 서버로부터 받은 응답으로써 앱에 적용가능한 정보로 변환된 형태이다.
여러분도 인지했겠지만 View, Presenter, Interactor, Data Manager들은 모듈에 안에서 굉장히 한정적으로 사용되는 것이다. 이것들이 오직 로그인과 관련하여만 어떻게 다루는지 안다. 반면 Service와 Entity는 앱 전반에 걸쳐 일반적으로 쓰이는 것이다. 이 경우 Service는 유저와 관련하여 당신 서버에 어떻게 요청하고 끝나는지 알고 있을 것이다. Service는 로그인, 회원가입 혹은 단순히 유저 정보를 검색하는 기능들을 포함한 것이다. 그리고 이 경우 Entity는 앱의 다양한 곳에서 사용될 수 있을 것이다.

이제 우리는 VIPER "stack" 혹은 모듈이 무엇인지 설명했고, Wireframe이 어떤 역할을 하는지 설명하겠다. Wireframe은 각 VIPER 컴포넌트들을 인스턴스화(객체화) 시켜주고 서로 소통할 수 있게 해준다. Wireframe은 View와 Presenter를 서로 참고하게 해주고 Presenter와 Interactor를 서로 참조하게 해주는등의 역할을 한다. Wireframe을 객체로 만든다는 것은 VIPER 모듈 전체를 객체화 한다는 것과 같은 의미이다.

앱에서 다른 View로 라우팅하기
Wireframe의 또다른 별명은 Router이다. 이것은 VIPER의 R을 의미하기도 한다. Wireframe은 요청이 들어올때 어디로 navigate할지 어떤 다른 모듈로 present 할지 알고 있다. 이 말은 즉 어떤 Wireframe은 다른 Wireframe을 참조한다는 뜻이다.

예를들어 당신앱의 첫 화면이 유저 계정을 요구하는 화면이라 생각해보자. 이 화면은 "회원가입"버튼과 "로그인"버튼이 있을 것이다. 그리고 또한 이 화면은 한 모듈이다; 이것을 회원가입 prompt라 부르다. 이 경우 실제 로그인을 하기 전에 유저들은 몇몇 옵션들을 볼 수 있다. 유저가 "로그인"버튼을 누른 경우 나타나는 일반적인 흐름이다.
  • View는 유저가 로그인을 요청했다고 Presenter에게 알린다.
  • Presenter는 이 요청을 알아차리고 모듈의 각 기능을 수행후 Wireframe에게 알린다.
이제 회원가입 Wireframe이 로그인 Wireframe에 의해 객체가 만들어지고, 역시 모든 VIPER 컴포넌트들이 새로 객체화된다. 그 다음 로그인 View에서 회원가입 View로 넘어갈 것이다.

VIPER의 이점
VIPER를 사용하고부터 많은 방법들로부터 이점들을 찾았다. 우리 앱에 VIPER를 적용시키면서 좋았던 점들을 다시 확인해보자
  • 재사용에 용이함
  • 동업에 적합
  • 일을 잘 나눌 수 있음
  • 테스트하기 쉬움

재사용에 용이함
앱에서 새 기능을 넣는다는 것은 굉장히 유용한 점인다, 이것은 이전 코드에 연관되어 새 코드가 어디에 들어갈지 알고 있다는 것이다. VIPER는 각 컴포넌트가 명확한 책임을 가진다는 점에서 굉장히 이상적이다. 이 점은 어떤것 안에서 엉키지 않고 새 코드를 어디에 넣을지 쉽게 결정되게 해준다.

새 기능을 추가할 때 그것들이 매번 쓰는 루틴이라 느껴진다. 왜냐하면 각 부분들의 코드가 어디에 있는지 알고 있고, 그냥 끼워넣으면 끝나는 문제이기 때문이다.

코드가 어디에 있어야할지 불분명하거나 임의로 판단(Judgement Call)해야할 경우 우리는 VIPER 상황에서 동작시킨다. 예를들어 뭔가 수행되기 전에 유저가 여러 아이템을 골라야 한다고 해보자. 이때 현재 View, Presenter 등등 상태를 유지한채 작업해야할까? 당신의 감각으로 어떤것을 만들어야하는지 한번만 이해하면 나중에는 쉽게 이 문제를 해결할 수 있을 것이다. 왜냐하면 이미 해결해보았던 경험이 있기 때문이다. 

동업에 적합
VIPER는 팀에서 작업하기 쉽게 만들어준다. 유스케이스가 서로 다른 모듈별로 나뉘어져있으므로 다른 사람에게 코드를 넘겨받아 작업할 필요가 없다. 그러므로 항상 한 기능을 위한 모든 코드는 그것의 모듈에 구분되있다.

예제에서 각 VIPER 컴포넌트들은 인터페이스를 통해 다른 컴포넌트와 연결된다. Objective-C에서는 이것은 단지 규약일 뿐이다. 좋은 점은 당신이 두 클래스간에 인터페이스를 정의하면 사람들이 이 클래스를 개별적으로 작업할 수 있다는 점이다. 우리는 처음에 View-Presenter의 인터페이스를 정의했고 한사람이 UI만 작업하고 다른 한 사람이 나머지 VIPER stack의 부분인 뒷단(backend)를 작업했다ㅏ.

일을 잘 나눌 수 있음
명확하게 정의된 "하나의 책임 원칙"인 각 VIPER 모듈이기에, VIPER는 클래스 사이에서 자연스럽게 분리된다. 이전 2가지 이점(재사용에 용이, 동업에 적합)과 관련이 깊다고 할 수 있겠다.

테스트하기 쉬움
앞에서 말한 "단일 책임 원칙"으로 쪼게어진 컴포넌트들을 가짐에 따라, 테스트(spec)하기 쉽게 되었다. 이것은 다른 의존성을 잘라내어 특정 기능을 테스트 가능하게 해준다. 예를들어 Interactor로직을 테스트 하고 싶을때는 주변에 붙어있는 Presenter와 Data Manager를 잘라내어 의존성을 제거하고 테스트를 위한 코드를 작성하여 컴파일 할 수 있다. 그러면 당신의 테스트들은 Interactor를 테스트하기위해서만 동작할것이며, Presenter와 Data Manager로부터 분리되어 테스트 가능하게 된다.

결론
전반적으로, VIPER로 전향한 것은 굉장히 유용했다는 것을 느꼈고, 위에서 언급한 이유들처럼 우리에게 많은 이드벤테이지가 있었다. 물론 우리가 원하는 VIPER를 만들면서 많은 난관을 마주쳤지만, 사실 이것은 어떠한 아키텍처를 골랐더라해도 겪는 문제일거라 생각된다.

나는 VIPER를 시도해볼 것을 추천하는 바이고 또한 다른 블로그 포스트도 보길 추천한다. 이 글이 도움이 되었다면 당신이 한 번 VIPER를 시도해보기에 관심이 생겼을 것이라 생각된다.


추가적인 읽을거리




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

,
번역자 : 위 글은 2년전(2014년)에 쓰여진 글임에도 번역한 이유는, 이 개념을 알면 수월하고 모르면 해맬 수 있기 때문입니다. 그렇게 대단한 이야기는 아닐지 모르지만 부모와 자식 뷰 간의 터치 이벤트를 주고 받는 일이 있다면 꼭 한번 읽어보길 추천합니다. 


Hit-Test은 한 점(터치 포인트와 같은)이 화면에 그려진 그래픽적인 오브젝트(UIView와 같은)를 관통하는지 결정하는 프로세스이다. 가장 앞쪽에 올라와 있는 view가 무엇인지 알아내기 위해 iOS에서는 Hit-Test라는 것을 사용한다.  Hit-Test는 역방향 우선순위 깊이우선 탐색(Reverse Pre-Order Depth-First Travesal) 알고리즘을 이용하여 view 계층을 조사하도록 구현되어 있다.

Hit-Test가 어떻게 동작하는지 설명하기 전에,  Hit-Test이 언제 호출되는지부터 알아보자. 아래 다이어그램은 손가락이 화면에 터치되는 순간부터 다시 들어올리는데까지 터치 과정의 큼직큼직한 플로우(high-level flow)를 보여준다.


위 다이어그램에서 보여주듯, 화면을 터치하는 매 시간마다  Hit-Test가 호출된다.  Hit-Test 이전 시점에는 어떤 view나 gesture recognizer가 UIEvent 객체를 받는데, 터치가 어느 지점에서 됬는지 그 event에 표현된다.

Note : 알 수 없는 이유로 Hit-Test가 한번에 여러번 호출된다. 이미 결정된 Hit-Test view도 비슷한 현상이 일어난다.

Hit-Test가 끝나고 나면 터치 포인트 아래 최상단 view가 결정된다. 그  Hit-Test view는 터치 이벤트 순서(began, moved, ended, cancelled)의 모든 UITouch 객체와 연관되어있다. 추가적으로  Hit-Test view에서 view나 조상 view들에 붙은 어떤 gesture recognizer들도 위의 UITouch와 연관되어있다. 그러면  Hit-Test view는 터치 이벤트를 순서대로 받기 시작한다. 한가지 염두하고 있어야 할 것은 손가락을 움직여  Hit-Test view의 범위(bounds)를 넘어 다른 view로 움직여도  Hit-Test는 터치 이벤트 순서가 끝날 때까지 모든 touch들을 받고 있을 것이다.

" 그 Touch 객체는 후에 터치가 view 바깥으로 움직여 나갔더래도 그 라이프 타임 동안에는  Hit-Test view와 연관되어있다."

앞에서 말했듯  Hit-Test은 역방향 우선순위(Reverse Pre-Order)의 깊이우선 탐색(Depth-First Travesal)을 사용한다. (먼저 root 노드를 탐색하고 그 다음 높은 index에서 낮은 index 순서로 자식 노드를 탐색한다.) 이런 종류의 탐색은 반복적인 탐색을 줄여주고, 터치포인트가 포함된 view를 내림차순으로 깊이 우선으로 검색하다가, 결과물을 찾으면 찾는 과정을 멈춘다. 이것이 가능한 이유는 subview가 항상 superview보다 위에서 그려지고(render), sibling(형제) view는 subviews 배열 안에서 index가 낮은 sibling view 보다 위에 그려지기 때문이다. 따라서 특정 포인트에 여러 view가 겹쳐져 있으면, 가장 깊은 view 중에 가장 오른쪽 view가 최상단 view가 될 것이다.

"시각적으로 subview의 요소는 subview의 parent view의 모든것을 가리거나 일부분을 가린다. 각 superview는 순서를 가지는 배열로 그것의 subview들을 가지는데, 그 순서는 각 subview의 상태에 따라 배열에 영향을 준다. 만약 두 sibling subview가 서로 겹쳐있다면 마지막에 추가된 subview가 다른 subview 위쪽에 나타나 있을 것이다."

아래 다이어그램은 view 계층 tree에 관한 예제이고, 화면에 그린 UI에 매칭시켜 놓은 것이다. 왼쪽에서 오른쪽으로 정렬된 tree branch들은 subviews 배열의 순서를 의미한다.


위에서 볼 수 있듯이, View A의 자식인 View A.2와 View B의 자식인 View B.1은 서로 겹친다. 그러나 "View B"의 subview index가 "View A"보다 크므로 View B와 그 subview들은 View A와 그것의 subview들보다 위에 그려진다. 그러므로  사용자의 손가락이 View B.1과 View A.2가 겹치는 부분을 터치하여 Hit-Test를 하게되면 View B.1이 반환된다. 

Depth-First Travesal in Reverse Pre-Order 방식을 적용함으로써 가장 깊은 내림차순으로 터치 포인트가 포함된 view를 찾으면 탐색을 멈춘다.


이 탐색 알고리즘은 UIWindow에 hitTest:withEvent: 메시지를 보냄으로서 시작되는데, UIWindow는 view 계층에서  root view이다. 이 메소드로부터 반환되는 값(view)은 터치 포인트를 포함하는 최상단 view이다.

아래 플로우 차트가  Hit-Test 로직을 설명한다.


그리고 아래 코드는 원래 hitTest:withEvent: 메소드를 실제 구현해본 것이다.


hitTest:withEvent: 메소드는 먼저 터치를 받을 수 있는지부터 확인한다.

view가 터치를 받을 수 있다면:
  • view가 hidden 이 아니여야한다.
    self.hidden = NO;
  • view의 userInteraction이 enable이여야한다.
    self.userInteractionEnable == YES;
  • view의 alpha가 0.01보다 커야한다.
    self.alpha > 0.01
  • view가 포인트를 포함해야한다.
    pointInside:withEvent == YES
그러고 view가 터치 받는 것을 허락하면, 이 메소드는 어떤 어떤 하나가 nil을 반환하기 전까지 높은 곳에서 낮은 곳으로 그것의 각 subview에 hitTest:withEvent: 메시지를 보냄으로서 receiver의 subtree를 탐색한다. 그 subview들에의해 반환된 첫번째 nil이 아닌 값은 터치 포인트를 포함하는 최상단 뷰이고 receiver에의해 반환된다. 만약 모든 receiver의 subview들이 nil을 반환하거나 그 receiver가 suview가 없으면 자기 자신을 반환한다.

다르게말하면, view가 touch를 받지 못하도록 되있을 때는 이 메소드가 더이상 receiver의 subtree를 탐색하지 않고 nil을 반환한다. 그러므로 이  Hit-Test 작업에서는 모든 view들의 계층을 다 탐색하지 않아도 된다는 것이다.

hitTest:withEvent:를 override 하여 사용한 일반적인 유스케이스
한 view가 터치 이벤트 매 순간마다 다른 view에 리다이렉트될 수 있도록 터치 이벤트를 다룰 경우 hitTest:withEvent: 메소드를 override 하면 된다.

" Hit-Test가 호출되기 전에 터치 이벤트 순서 중 첫번째 터치 이벤트가 그것의 receiver에게 보내기 때문에, 이벤트들을 리다이렉트하기위해 hitTest:withEvent:를 override 하는 것은 그 이벤트 순서의 모든 터치 이벤트를 리다이렉트 하게 될 것이다."

View의 터치 면적 넓히기
hitTest:withEvent: 메소드를 override 할 수 있는 또하나의 경우는 view의 터치 범위가 그것의 실범위(bounds)보다 커야할 때 이다. 예를들어, 아래 그림은 20X20 크기의 UIView를 나타낸다. 이 크기는 실제 손가락으로 touch 받기엔 너무 작다. 따라서 그것의 터치 면적을 hitTest:withEvent: 메소드를 override하여 각 방향마다 10px씩 증가시킬 수 있다.



Note : 이 view에대해 정확하게  Hit-Test 하기위해, parent view의 영역(bounds)은 child view의 원하는 터치영역을 포함하고 있거나, 원하는 터치 영역을 포함하기 위해 그것의 hitTest:withEvent: 메소드를 overrid 해야한다.

아래에 터치 이벤트들을 view들에 통과시키기
종종 해당 view의 터치 이벤트를 무시하고 그 view의 아래에 통과시켜야 할 때가 있다. 예를들어, 앱 위에 투명하게 전체적으로 view가 덮혀올라간 경우를 생각해보자. 이 오버레이 는 평범하게 터치를 받을 수 있는 control들과 button들의 subview들을 가질 것 이다. 그러나 어디에서나 오버레이를 터치하면 그 오버레이 아래의 view들에게 터치 이벤트를 넘겨줄 수 있다. 이 동작을 완료하기 위해서는 오버레이에서 터치포인트를 포함하는 그것의 subview들 중 하나를 반환하거나 nil을 반환하기 위해 hitTest:withEvent:를 override할 수 있다. 오버레이 위에 터치가 된 경우에도 마찬가지이다.


subview에 터치이벤트 보내기
또 다른 경우는 parent view의 모든 터치 이벤트를 child view로 보낼때이다. child view가 parent view의 일부분만을 차지하지만 그 parent에서 발생하는 모든 터치들에 반응해야할때 이 동작이 반드시 필요하다. 예를들어, 이미지들이 회전목마처럼 구현된 UI를 생각해보자. parent view로는 UIView, 그 위에 child view로는 UIScrollView를 가지고 이 view는 pagingEnable를 YES로 clipsToBounds를 No로 설정한다. 그 위에 이미지들을 올려 구성한다.


UIScrollView의 내부만 터치를 받는것이 아니라 외부에도 터치를 받고 싶은데, 그 scroll view의 parent view 범위 내로 제한하고 싶을때, 아래와 같이 그 parent의 hitTest:withEvent: 메소드를 override 함으로서 가능하다.





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

,

이번 시간에는 VIPER와 관련된 우리팀의 이야기를 들려주고 경험을 공유하고 싶다. 특히 우리가 특정 상황에서 어떻게 VIPER적으로 다루는지, 우리들의 추천은 어떤지 이야기 할 것이다. 또한 당신의 경험을 코멘트 해주길 바란다(원문 링크에 들어가서 코멘트 해주시면 됩니다).

이 글의 목표는 VIPER의 규칙에 대해 이야기하거나 VIPER의 모든 컴포넌트들을 설명하려는게 아니다. 이러한 이야기는 이미 더 좋은 글들이 많다.  

이 글에서는 우리 팀이 프로젝트를 하면서 무얼 배웠고, 이 아키텍처를 사용하는 동안 어떤 것들이 큰 도전이었는지 알려 주고 싶다. 우리는 시작할 때 많은 궁금증들이 있었다. 작년에 큰 프로젝트를 시작하였고 이야기는 여기서부터 시작되었다. (컨퍼런스 관련 앱인데 의제나 참석자의 목록, 발표자의 목록, 뉴스의 목록 등으로 구성된 앱이다)

VIPER를 타고 달려나갈 준비가 되었는가? 

VIPER CarVIPER Car


왜 우리는 VIPER를 골랐을까?


Note : 만약 새 프로젝트에 적용시키길 적절한 아키텍처를 찾고있다면 이 사이트를 한 번 들어가보아라. 우리의 경우 최종적으로 VIPER를 선택했다.

  • VIPER 아키텍처는 프로젝트 초기에 요구사항이 잘 정의되어 있다면 적합하다고 할 수 있다. 운좋게 우리가 그러했다. 만약 당신의 화면정의나 비즈니스 로직이 프로덕트 오너에 의해 바뀌기가 쉽다면, VIPER는 별로 좋지 않은 솔루션일 수 있다. 하나의 작은 변화에도 당신의 모든 모듈(View, Presenter, Interactor...)을 손봐야 하는 수가 있다. 이러한 대규모의 재설계는 엄청난 시간낭비이고 차라리 새로운 VIPER 모듈을 만드는게 나을지도 모른다.
  • 우리 프로젝트는 상당히 규모가 컸다. 하나의 모듈을 세팅함으로써, 파일들을 생성하고 수많은 반복적인 코드를 만들어낸다. 모든 VIPER 컴포넌트로부터 데이터를 주고 받는일이 잦은데, 한 View에서 API 관리자로 데이터를 넘겨주고, 다시 데이터를 View에 돌려준다. 데이터를 자꾸 옮겨 다녀야하기 때문에 이게 왜 작은 프로젝트에는 적합하지 못한지 보여주는 예이다.
  • (몇몇 예외를 제외한) VIPER는 각 요소마다 기능 정의가 아주 명확하다. 덕분에 파일의 코드 양을 줄여주고, 하나의 기능 컴포넌트에 따라 옳바르게 모듈이 나눠져있을 것이다. 추가적으로 VIPER 프로젝트는 모든 개발자에게 비슷한 관습을 만들어주기 때문에 구조가 잘 잡힌다. 새로운 개발자가 팀에 합류한다해도 빠르게 그 VIPER에 적응하게 될것이고, 새로운 개발자가 원래 프로젝트의 구조를 바꾸기는 쉽지 않을 것이다.
  • 3명의 개발자가 있는 팀에서 일한다면, 모두가 하나의 모듈을 개발할 수 있다. 쉽게 쪼게어 개발할 수 있다는 뜻이다.
  • 우리 프로젝트는 시작단계부터 화면정의와 기능정의가 잘 되있었기 때문에 VIPER 모듈로 만드는 것이 어렵지 않았다.
  • VIPER 컴포넌트의 기본은 한 모듈에 있는 모든것들이 굉장히 잘 나눠져있다. 따라서 유닛 테스트 하기 좋은 조건이다. 이 글을 보면 VIPER에서 TDD 이야기를 들을 수 있다.
  • 마지막으로 우리팀은 새로운 아키텍처를 시도해보고 싶었다!!!


MassiveMassive

시작은 MVC로 했지만, 결국 Massive(덩어리의)VC로 끝나버렸다.

프로젝트 구조, 폴더, VIPER 모듈들

혹시 모든 VIPER 모듈의 컴포넌트들을 외우고 있는가? 우리는 이 웹사이트를 기반으로 컴포넌트를 정의했고, 여기서는 Services라 부르는 컴포넌트를 사용했다. 당신은 Services라는 섹션에서 더 많은 정보를 얻을 수 있을 것이다. 


VIPER 다이어그램VIPER 다이어그램

프로젝트 파일에 어떻게 이것들을 적용시킬까? 모든 컴포넌트는 각 폴더와 클래스로 적용될 수 있다.

다음 질문으로 VIPER 모델로서 어떤것이 제격일까? 기본적으로 가장 쉬운 접근법은 한 화면 단위로 VIPER 모델을 만드는 것이다. 아래 예를 보자.
  • Login Screen (로그인 화면) -> Login Module
  • Participant List (참여자 목록) ->  Participant List Module
흠 모든 것을 수작업으로 하나하나 만들어야 할까? 다행히도 자동생성기를 사용하면 된다.

VIPER 모듈 자동생성기(Generator)
당신의 앱을 정말 VIPER 아키텍처 기반으로 만들고 싶다면 손으로 하나한하 처넣을 생각은 안해도 된다. 그것은 재앙이다! 새 모듈을 자동으로 생성해주는 프로세스가 필요하다. 먼저 VIPER 자동생성기 프로그램을 아래 링크에서 다운받을 수 있다.

우리 프로젝트에서는 첫번째 VIPER gen을 선택하여 우리 목적에 맞게 커스텀하여 사용했다. 예를들어, Interactor와 Presenter를 위한 테스트 파일을 추가했다. 커스텀된 vipergen툴은 아래 링크에서 사용할 수 있다.

이 커스텀 과정에서 루비로 약간이 수정이 필요했고, 단지 자동으로 모든 유닛테스트 파일에 Swift 모듈 이름을 추가해주는 기능이다.

아마 당신은 당신만의 템플릿이 필요할 것이다. 그러려먼 이미 있는 기본 템플릿에서 폴더를 복사하고 수정해가면서 간단하게 만들 수 있을 것이다. 이 저장소에 들어가면 새 템플릿을 어떻게 추가하는지 배울 수 있다. github를 쓸 수 있는 사람이라면 쉽게 커스텀이 가능할 것이다. 

우리는 사실 다른 솔루션을 많이 사용해보진 않았으나, Generamba는 CLI를 셋업하기 좋게 제공되는 툴처럼 보인다. 여러분은 각자 상황에 맞게 가능한 많은 솔루션을 체크해보고 사용하는걸 추천한다.

VIPER 모듈들끼리 서로 정보 보내기
시작할때부터 우리는 VIPER 모듈 사이에 데이터를 어떻게 다루는지 생각해보았는데, 명확한 답이 보이지 않았다. 아래 토픽들은 프로젝트 시작할 때 읽으면 굉장히 도움이 될 것이다.

최종적으로 우리는 "한 모듈"에서 "다른 한 모듈에있는 Presenter"에 정보를 보내기로 결정했다. (이것이 최대한 VIPER 컴포넌트를 망가뜨리지 않고 사용할 수 있는 가장 좋은 방법이라 생각했다.)

Passing DataPassing Data


코드에선 어떤걸 의미할까?

SpeakerDetails 모듈은 SessionList wireframe에서 불려진 클래스 메소드를 기반으로 초기화된다. 그러므로 SpeakerDetails의 Presenter 메소드인 위 메소드는 유저가 어떤 세션을 선택했는지 알고 있어야 한다.


VIPER, Entity와 Core Data
우선 우리는 Core Data Stack을 만들기로 결정했다. 왜 이 작업은 외부 라이브러리를 사용하지 않았냐고 물어본다면, 우리는 Persistence Store의 컨트롤을 자유자재로 하고 싶었기 때문이다.

우리의 Core Data Stack은 Object Context가 두가지가 있다. 하나는 메인 쓰레드용이고 하나는 백그라운드 쓰레드용이다. 둘 다 같은 Persistent Store Coordinator에 연결되어, 각 context는 서로 독립적으로 동작한다. 저장이 완료되었다는 신호(did-save notification)와 함께 변경사항들이 맞바꿔진다. 

CoreData SchemaCoreData Schema

Note : 이 아이디어는 Advanced Core Data라는 책으로부터 얻었는데, Core Data Stack을 많은 선택지와 함께 어떻게 세팅할 수 있는지 알려준다. 우리는 강력히 이 책을 추천한다.


그러나 Entity는 어떨까?

Entity들은 CoreData의 NSManagedObject 인스턴스가 아닌 VIPER 컴포넌트에 의해 주고 받아진다. Managed Object는 local manager들 에서만 접근이 가능하다. 이것들은 Entity로 바꾸거나 Interactor로 보낸다. 

CoreData Convert To EntityCoreData Convert To Entity

왜 NSManagedObject들을 보내지 않을까? 그 이유는 우리 앱은 데이터 모델과 데이터 레이어가 분리되있기 때문이다. 이 방법에서는 우리는 CoreData를 data store layer로 따로 빼어 놓았다.


어떤 데이터 타입으로 Entity를 표현할까? 우리는 모두 구조체(Struct)로 만들었다. 


위 구조체는 알기쉽고, 변경불가능하며 쓰레드-세이브(Thread-Safe)하여 완벽하다 😃


의존성 주입(Dependency Injection)

VIPER 아키텍처를 사용하는 것은 의존성 주입(dependency injection)을 적용해볼 수 있는 기회를 제공한다. 예를들어 이 local manager 예제를 살펴보자. 



VIPER 아키텍처를 사용할 때 모든 요소마다 DI를 사용하는 것은 좋은 습관이다. 우리는 유닛테스트라는 섹션에서 몇 예제와 함께 테스트할때 어떻게 이런 접근이 우리에게 도움이 되는지 보여줄 것이다.
실제 Wireframe이 무엇인가?
우리의 경우 wireframe은 두가지 기능을 가진다.

  • 각 VIPER 컴포넌트들의 인스턴스들을 초기화하고 그들을 연결해준다.
  • 두번째 기능은 다른 모듈에 navigate와 present 해준다.

이게 다다.

유닛 테스트
우리는 이전에 유닛테스트에 경험이 많이 없었다. 이 프로젝트에서 유닛테스트의 첫발을 내딛었는데, Interactor와 Presenter를 테스트하는 것 부터 시작했다. 그 이유는 Interactor에서 메인 비즈니스 로직을 가지고 있고 Presenter는 데이터를 보여주기 직전에 준비하는 작업을 하기 때문이다. 이 컴포넌트들은 다른 것들에 비해 좀 더 중요해 보였으나 오로지 우리 주관적인 의견임을 기억해주기 바란다.

유닛 테스트에서 사용된 라이브러리들이다.

VIPER에서 한 모듈의 모든 요소들은 각 기능별 유닛 테스트하기 적합하게 만들어져 있으며 이것들은 엄격히 분리되어 있다. 

Unit Test MockUnit Test Mock

위에서 볼 수 있듯이 컴포넌트들을 분리함으로써 우리는 Interactor의 테스트에만 집중할 수 있다. Interactor와 연결된 다른 요소는 단지 테스트를 위해 임의로 만든 목(mock)이다.



Services
Services가 무엇일까? 이것은 엄격한 VIPER 컴포넌트로부터 약간 분리되어있으며, 독립적인 컴포넌트라 할 수 있다. Service는 여기에서 언급되었으며, 이것이 반드시 필요한 것은 아니나 굉장히 유용한데 특히 높은 결합력을 만들어준다. 한 Service는 여러 모듈에서 사용될 수 있다. 
여기 그 기능의 예시이다.
  • 캘린더를 다루는 것 (Calendar Service)
  • 연락처를 다루는 것 (AddressBook Service)
  • 키체인 관리 (Keychain Service)
  • 사람들 서비스 (Person Service) - 사진과 같이 유저의 데이터 다운로딩을 관리한다. 이 경우 Service는 네트워크 요청을 위한 apiClient 인스턴스가 필요하다.

Service 동작



백엔드로부터 변화(갱신)들을 듣는(listening)것은 어떻게 다뤄지는가? 

우리 앱에서는 자동으로 갱신되는 두개의 목록이 있다. 우리의 Synchronized Service 는 백그라운드에서 JSON responses를 다운받고 CoreData에 넣는 무거운 작업을 한다. 이 작업은 매 1분마다 하게된다.


notification을 보내기 위해 파라미터를 받는 save 메소드를 가진다.

갱신 플로우에서 다음 컴포넌트는 interactor 이다.


dataUpdated(notification:NSNotification)는 interactor로부터 override 되었다. 당신이 예상한대로 표준 VIPER 플로우가 되었다. interactor는 notification을 받은 후에 데이터를 위해 local data manager에게 그것을 물어본다. 그러면 새로운 데이터르 미리 프로세스하기 위해 presenter에 보낸다. 그리고 presenter는 view에 보내어 화면에 표시한다. 됐다!


VIPER 모듈 컴포넌트들 사이에 API들은 각 프로토콜 파일에 정의되므로 항상 양방향 소통이 가능하다. 끝나는 시점에 작업을 하기 위해 클로저(closure)를 사용하는 것은 interactor에서 presenter로 데이터를 넣는 중에 블락될 수 있다.

오히려 옛날의 delegate 패턴이 우리 업데이트 메커니즘에 더 잘 동작한다. presenter가 interactor에 데이터를 얻기 위해 물어볼 뿐만 아니라, 모든 업데이트 프로세스를 초기화 할 수 있다.

보기에 깔끔하고 딴딴해보이지 않는가?


그래서 언제 VIPER를 사용하고, 언제 사용하지 말아야할까?
항상 그렇듯 대답은 "상황에 따라 다르다”.

아래 다이어그램이 이 중요한 질문에 답변이 될 수 있길 바란다.

ViperOrNotViperOrNot


마지막 요약
VIPER 사용을 시작하는 것은 크나큰 도전일 수 있다. 특히 이 아키텍처를 처음 적용시키는 경우는 더더욱 그렇다. 우리는 여러분이 이  글을 읽고 많은 의문들이 사라졌기를 바란다.

우리의 경우 git flow로 pull request와 함께 연습해보았다. 모든 개발자들이 저장소에 동료가 어떤 것을 push 했는지 조심히 관찰할 수 있다면, 이것은 굉장히 유용했다. 만약 대부분의 사람들이 서로 이야기나 관찰 없이 그들만의 VIPER 버전을 만들어버리면 이것은 재앙이다. 그 즉시 바로 모여 브레인스토밍하고 모두가 사용할 수 있는 새로운 솔루션을 다같이 찾아봐야한다.

VIPER는 앱을 어떻게 만들지 개괄적인 결정을 한다. 우리는 당신에게 오픈 마인드를 가지고, 각 컴포넌트를 커스터마이징하고 최적화하는 것을 멈추지 마라고 조언하고싶다.

VIPER는 지속적인 개선이 필요하며, 우리는 새 프로젝트가 이전 처음 프로젝트보다 더 나은 경험을 할 수 있을거라 기대한다.

특히 프로젝트 첫 시작부분인 당신이 프로젝트 구조를 세팅하는 시점에서 아키텍처를 바꾸지 않는다면, 한 걸음 걸음이 악몽일 것이다. 아키텍처에서 하나의 실수가 더 많은 실수를 유발하게 할 수 있다. 이것이 수 많은 힘겨운 일거리가 만들어지는 이유이다.

마지막으로 VIPER는 유닛테스트에 경험이 없어도 이것을 구현하기 쉽도록 해준다. VIPER에게 감사하다.

흥미로운 자료들

iOS 아키텍처 관련 번역글
 



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

,

원문https://medium.com/@kazmiekr/what-every-ios-developer-should-be-doing-with-instruments-d1661eeaf64f

Introduction

iOS 프로젝트를 마무리할때, 디바이스에 올려 테스트하여 크래쉬가 없이 잘 돌아가면 만족하고 끝낸적이 있을것이다. 그게 과연 올바른 마무리일까? Instruments를 사용하여 프로파일링(profiling)을 하지 않고 끝냈다면 잘 마무리 했다고 할 수 없다고 생각한다. 그 이유는 개발자 디바이스에서 잘 동작한다해서 실유저에서 크래쉬가 안난다는 보장은 없기 때문이다.

Xcode에는 Instruments라는 퍼포먼스 튜닝 어플이 포함 되 있다. 이 프로그램은 다양한 기준으로 개발자의 앱을 프로파일 할 수 있게 해준다. Instruments는 CPU 사용량, 메모리 사용량, 메모리 누수, 파일/네트워크 활성, 베터리 사용량 등을 측정할 수 있는 툴을 포함한다. 이것들은 Xcode에서 바로 시작할 수 있어서 쉽게 켤 수 있다. 그러나 지금 보고있는 프로파일 자료가 뭘 프로파일링 한건지 잘 모를수도 있으며 이런 상황은 개발자들이 이 깊은 잠재력을 가진 툴 사용을 저해할 수 있다.

이 많은 프로파일 툴 중에 어떤걸 선택해야할까? 먼저, 느려진 네트워크 요청이나 버벅거리는 스크롤과 같은 눈어띄는 퍼포먼스 이슈 해결을 위한 선택지들이 있을 수 있다. 그러나 나는 당신이 생각하기에 모든것이 정상적으로 돌아가는것으로 보이는 부분에서 CPU나 메모리 사용량을 체크해보는걸 추천한다.

프로파일을

이 이야기를 하기 전에 한가지 우리가 염두할 것은, 모든 개발자들이 항상 그들의 앱에 프로파일을 하지는 못한다는 것이다. 대부분은 데드라인과 예상 결과물이 존재한다. 어떨땐 앱을 프로파일링이고뭐고 마감을 빠듯하게 지키는것도 힘들 때가 있지만, 우리는 이런 상황까지 고려하여 '어느 시점에 프로파일링을 하면 좋을까'에 대해 얘기해보고자한다.

최소한 만든 앱을 앱스토어에 제출하기 전에 한번은 프로파일링을 해줘야한다. 당신은 앱스토어의 심사를 통과 한 후 유저들이 앱을 사용하면서 안 좋은 일들이 일어나길 바라지는 않을것이다. 프로파일링을 하지 않았다가 문제가 생긴다면, 좋지 않은 리뷰들이 많아질 것이고 다운로드 수는 감소할 것이다.

나는 당신이 주요 기능들을 끝낸 후에, 재빨리 프로파일을하여 모든것이 정상적으로 동작하는지 확인해보는 것을 제안한다. 만일 재빨리 하지 못하면, 잠재적인 이슈들이 점점 드러날테고, 그것이 한참 쌓이면 완전히 새로운 기획으로 바뀔 수도 있으며 이것은 런칭을 심각하게 미루게 될것이다. 팀으로써 당신은 프로파일하는 시간을 개발 플랜의 일부로 살짝 넣도록 해야한다.

익숙하지 않은 프레임워크를 쓰게되면 Instruments 켜는걸 제안한다. 또한 대부분의 iOS 프레임워크와 라이브러리는 끊임없이 바뀌며, 이런 변화는 당신이 익축하지 않은 프레임워크와 작업하는것과 비슷하다. 대부분 개발자들은 바뀌는게 언제인지 느낌이 올때가 있고, 만약 당신이 그런 느낌을 받았다면 얼른 당신이 만든 작업물들에서도 안정적으로 동작하는지 확인하기위해 프로파일링을 돌려야한다. 당신이 추가한 라이브러리에서 당신의 통제를 범위를 벗어나 발생될 있는 메모리 이슈가 있는지 확인하기 위한 프로파일에는 3rd party 라이브러리를 가져다 쓰는것 또한 좋은 방법이다.


Xcdoe에서 프로파일링

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 입장에서 무거운 메소드들 리스트를 모니터링 하면서 앱의 정확한 지점을 최적화 시켜볼 있다. 몇몇은 건드리기 힘들 수도 있지만 최적화 가능한 것들도 많이 있을 것이다. 필자는 최적화를 어떻게 하는지에대해서는 깊게 보지는 않을 것이지만 여기 몇몇 생각해볼만한 것들이 있다:

  • 무거운 작업은 UI 프로세스가 아닌 것은 다른 쓰레드로 옮긴다.
  • 항상 다시불러올 필요가 없는 이미지, 데이터 등등의 것들은 캐싱한다.
  • 당신은 필요하지 않는 UI 업데이트를 지도 모른다. UI 업데이트를 줄인다.

한가지 팁은 ‘Call Tree’ 섹션의 오른편에 존재하는 ‘Extended Detail’ 섹션이다. 이것은 당신이 선택한 스택 흔적(trace) 있고 라인을 더블클릭하면 정확히 당신 코드를 찾아 보여줄 것이다. 작은 Xcode아이콘을 클릭하면 동작중인 메소드를 Xcode에서 보여줄 것이다.

퍼포먼스 이슈를 해결하여 업데이트를 시킨 후에는 같은 방식으로 profiler 돌리면서 이전 퍼포먼스보다 나아졌는지 체크한다. 과정을 당신이 만족할 까지 반복하면 된다.

메모리 프로파일링

다음으로 프로파일링 것은 메모리 프로파일링이다. 이것은 직접적인 이슈가 아닐때가 많아서 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’ 검색해도 찾을 있을것이다.

여기 필자가 만든 예제에는 4개의 스냅샷이 있고 ViewController라는 이름으로 객체들을 필터링 했다. 그리고 스냅샷 사이에 ServiceViewContoller에서 누수가 일어났다는걸 있다.

여기 글의 목적을 위해 ViewController에다 뭔가 문제가 있는 retain cycle 집어넣으나, 원래는 코드를 뒤져보면서 어떤것이 객체의 누수를 만드는지 일일히 살펴봐야한다. retain cycle 객체가 서로 강한 참조를 하고있어서 서로 할당 해제되는걸 막을때 일어난다. 필자는 retain cycle에대해 깊게 말하진 않을것이나, 이런 경우에 delegate blocks/closures 먼저 확인하는 것을 추천한다. 당신의 delegates (weak)하고 blocks/closures에서 약한 참조를 사용한다는걸 확인하면 좋을것이다.

당신의 객체에서 다른 객체를 참조하고있어서 그 다른 객체가 메모리 해제되지 못하는지 일일히 확인하기 좀 힘들다면, Instruments 사용하여 많은 객체들의 자료를 얻을 있을 것이다. Instruments에서 객체 옆에 작은 화살표를 누르면, 객체의 모든 할당된 객체들을 보여줄 것이며 누가 생성하였는지까지도 나온다. 그다음 작은 화살표를 누르면 retain release count 정보까지 얻을 있다. 만일 count 0 되지 않는다면 메모리 해제가 되지 않았다는 뜻이다. 여기서 팁을 주자면 ‘Responsible Library’ 당신 코드이니 유심히 보고 ‘libsystem_blocks’ 유심히 봐라. 반대로 ‘UIKit’ 스킵해도 된다. 필터링하기위해 아이템들을 검색 박스에 쳐볼 있다. 그러고나면 ‘ Extended Detail’ 보이고 이것들이 어떻게 돌아가고있는지 스택 trace 보이게 될것이다.

결론

글은 Instruments 아주 일부만 보여준 것이다. 그래도 Instruments 시작하는 사람들에게 도움이 되길 바라며 처음부터 바로 잘하지 못해도 괜찮다. 툴을 사용하는데 점점 익숙해지다보면, 당신의 코드가 나은 코드가 될것이다. 당신은 문제를 만든 이슈를 찾는것에 주도적이게 것이며, 프로파일링이 당신의 익숙한 개발 플랜 하나의 플랜으로 자리잡게 될 것이다.

Instruments에대해 배우고 싶다면 필자는 2014 WWDC 비디오(Improving your app with Instruments) 보는것을 적극 추천한다. 여기서는 추가적인 팁엔 트릭과 함께 살펴볼 있다. Instruments에대해 읽고싶으면 애플이 만든 유저 가이드 확인해보길 바란다.



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

,

MVC, MVP, MVVM, VIPER에대해 확실하게 잡기

원문https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.wtcp3gqzw

UPD: NSLondon에대해 내가 발표한 슬라이드 자료가 이 링크 있다.

iOS에서 MVC 사용한다는게 다소 이상하게 느껴질 있다. 당신은 MV모데VM으로 바꾸려고 생각해본 적이 있는가? VIPER 적용시켜볼 생각을 적은 있으나, 그게 의미있는 것인지 확신이 들지 않는가?

글을 읽어 내려가면 것들에 대한 답을 찾을 있을 것이다. 또한 자유롭게 댓글로 의견을 제기할 있다.

당신은 iOS 환경에서 아키텍처 패턴에 대한 지식을 정리하고 싶을 것이다. 우리는 유명한 것들을 골라 한번 보고, 이론과 비교한 , 작은 예제들과 함께 연습해 것이다. 아래 링크는 당신이 특별히 관심있는 것을 연습할 있다.

디자인 패턴을 마스터하는것은 중독될 있으므로 조심해야한다: 전보다 많은 질문들이 생겨날 것이기 때문에.

- 누가 네트워크 리퀘스트를 소유하여야하나: 모델이냐 컨트롤러냐?
- 새 뷰의
어떻게 모델을 넘겨주나?
- 누가 새로 생긴 VIPER 모듈을 생성해야하나: Router Presenter?

아키텍처를 고르는데 신중해야하는가?

당신이 만약 개발을 하다가 디버깅을 해야하는데 엄청난 양의 클래스와 엄청난 양의 다른 것을 비교해야 하며, 이게 아키텍처가 없는 상황이라면, 당신 클래스의 어떠한 버그를 찾지도 고치지도 못하는 상황을 맞이하게 것이다. 우리는 클래스의 모든 속성을 머릿속에 담아두고 있을 없다. 만약 그짓을 하다보면 중요한 세부적인 요소를 놓힐 수가 있다. 만약 개발하면서 이런 경험을 이미 해보았다면 아래와 같은 것을 겪어봤을 것이다.

  • 클래스가 UIViewController 자식클래스이다.
  • 당신의 데이터들이 UIViewController에서 바로 저장된다.
  • UIView들이 거의 아무 일도 하지않는다.
  • Model 데이터 구조이다.
  • 유닛 테스트로 아무것도 하지 않는다.

그래도 애플의 가이드라인이나 애플의 MVC(링크) 따랐다해도 이러한 상황은 생길 있으니 너무 낙담하지는 마라. 애플의 MVC 뭔가 잘못되었고, 우리는 그걸 바로잡을 것이다.

좋은 아키텍처의 특징 정의해보자:

  • 엄격한 룰에 따라 개체들간의 책임 분리(Distribution) 균형있게 해야한다.
  • 첫번째 말한 특징으로부터 나올 있는 테스트들이 가능(Testability)해야한다. (그리고 걱정마라: 적절한 아키텍처를 고른다면 어렵지 않을것이다.)
  • 사용하기 편해야(Ease of use)하고 유지보수하기 쉬워야한다.

분리해야하나?
분배는 우리가 이게 어떻게 동작하는지 알아낼려고 노력하는 동안 우리의 뇌에서 균등하게 생각하도록 해준다. 만약 당신이 천재라 생각되면 그냥 하던대로 해라. 그러나 능력은 선형적으로 커지니 않을 뿐더러 광장히 빨리 한계에 도달해버린다. 그러므로 가장 빨리 복잡한 것을 극복하는 방법은 단일 책임 원칙으로 수많은 개체들의 책임을 쪼개는 것이다.

테스트 가능해야하나?
이미 유닛테스트에대한 중요성을 알고 있는 사람에게 던지는 질문이 아니라, 기능을 추가한 일때나, 클래스의 몇몇 복잡성을 리팩토링을 하기 위해서 테스트에 실패하는 사람들이 하는 의문이기도하다. 이것은 테스트가 런타임 내에서의 이슈를 찾는데 도와주며, 반대로 실유저에게 이슈가 발생한다면 그걸 고친 앱을 다시 실유저가 다시 사용하기까지 일주일씩이나 걸린다.

사용하기 쉬워야하나?
가장 좋은 코드가 뭔지는 한번 언급해 가치가 있다: 하나도 작성하지 않은 코드이다. 따라서 적은 양의 코드는 버그가 적다. 게으른 개발자 말을 빌려 적은 코드를 작성 하기를 갈망하며 이것은 코드를 설명해야하면 안된다. 또한 당신이 눈을 감고 허우적대며 유지보수하는 솔루션을 원치도 않을 것이다.

필수 MV(X)

요즘은 아키텍처 설계를 할때 수많은 선택지가 있다:

위에서 세개(MVC, MVP, MVVM) 아래 3 카테고리중 하나는 들어가있다:

  • Models데이터나 데이터 접근 레이어(Person 클래스나 PersonDataProvider 클래스와 같이 데이터를 다루고있는) 소유를 책임지는 부분
  • Views레이어에 표현되있는 것을 책임지는 부분(GUI), iOS 환경에서는 'UI' 접두로 붙는다(역자주: UILabel, UIView 등등..).
  • Controller/Presenter/ViewModelModel View 붙여준다. 보통 유저가 View에서 어떤 액션을 취할때 Model 변경하거나 Model 변경되었을 , View 갱신하는 책임을 가지는 부분

개체들을 나눌때 이점:

  • 이전보다 이해할 있다(이미 알고 있다 하더라도).
  • 재사용 가능하다(대부분 View Model 적용 가능하다).
  • 독립적으로 테스트 가능하다.

어서 MC(X) 패턴을 시작하고 나중에는 VIPER까지 해보도록 하자.

MVC

이전에는 어떻게 사용해왔을까?

애플의 MVC 살펴보기 전에 전통적인 MVC 어떻게 사용되었는지 보자.

Traditional MVCTraditional MVC

경우는 View 범위가 정확하지 않다. Model 바뀌고나서 Controller에의해 한번 랜더링(rendering) 된다. 웹페이지에서 다른 페이지로  수 있는 링크를 누른 , 다시 로딩되는 것을 생각해봐라. iOS 앱에서 전통적인 MVC 구현하는것은 가능할지라도 구조적인 문제때문에 효과적으로 처리할 없으며 당신 앱이 그러기도 원치 않는다.— 모든 개체가 둘씩 묶여있고, 개체는 다른 두개에 대해 알고있다. 이것은 각기 그들이 재사용성을 심각하게 줄여버린다. 이러한 이유로 우리는 흔히 쓰는 MVC 작성하는 또한 스킵 하겠다.

전통적인 MVC 최신 iOS 개발에 적합해 보이지 않는다


Apple’s MVC

기대한것..

Cocoa MVCCocoa MVC

원래 Controller Model View 연결시켜주는 역할을 하므로 서로에 대해 알필요가 없다. 그중에 가장 재사용 불가능한 것이 Controller이며, 우리도 그걸 알고있다. 따라서 우리는 모든 특이한 로직을 Model 아닌 Controller 넣어야한다.

이론적으로는 굉장이 전략적으로 보이지만 뭔가 문제가 있다. 당신은 MVC 컨트롤러 덩어리(Massive View Controller) 불리는걸 들은적이 있을지도 모른다. 나아가 View Controller offloading iOS 개발자들에게 중요한 토픽이다. 애플은 전통적인 MVC 조금 개선하여 사용하여서 이런 일이 일어나버린건가?

Apple’s MVC

실체는..

Realistic Cocoa MVCRealistic Cocoa MVC

Cocoa MVC View Controller 덩어리 작성하도록 만들어버린다. 이유는 View들의 라이프 사이클 안에서 뒤엉키는데 그것들을 분리해내기가 어렵기 때문이라고 말한다. 너가 Model*비지니스 로직이나 데이터 변환같은 것을 없애는 능력을 가졌을 지라도 대부분의 View에서 반응하면 액션을 Controller로 보내게 될것이다. 뷰 컨트롤러는 결국 모든 것의 델리게이트(delegate)나 데이터소스(data source)가 될테고, 종종 네트워크 요청과같은 처리도 하고 있을지 모른다. 

이런 종류의 코드를 얼마나 많이 보았는가:

Model 함께 직접적으로 구현된 View cell MVC 가이드라인을 위반한다. 그러나 항상 그렇게 사용하며 사람들은 이게 문제가 아니라고 느낄때가 많다. 좀더 MVC 따르고자 한다면 cell Controller에서 구성하고 View 안에 Model 거치지 않아햔다. 그러나 그렇게해버리면 Controller 커져버리게 될것이다.

Cocoa MVC View Controller 덩어리의 이유이기도하다.

문제는 유닛 테스트(여러분 프로젝트에 있기를 바란다)에까지 나타날 거라는걸 확신할수 없다. 당신의 View Controller View 붙어있고, 이렇게하면 그들의 View 라이프 사이클이나 테스트를 위한 View 만들기가 어려워지기 때문에 테스트가 힘들어진다. 반면 View Controller 코드를 작성하고 있으면 당신 비지니스 로직은 가능한 View 레이아웃 코드로부터 분리될것이다.

간단한 예제를 보자:

MVC 분리하면 현재 View Controller안에서 동작되게 있다.

테스트하기 좋아보이지는 않다. 우리는 greeting 생성을 GreetingModel 클래스에 옮겨 넣을 있다. 그러나 GreetingViewController안에서 UIView 연관되어있는 메소드(viewDidLoad, didTapButton) 호출하지 않은체 상연 로직(예제에는 로직이 많이 없지만) 테스트를 수가 없다.

사실, 로딩테스트는 디바이스를 바꿔가며(iPhone4S, iPad 등등으로) 확인해보는 것에대한 이점이 없다. 그래서 Unit Test target configuration에서 “Host Application” 지우고 시뮬레이터 없이 테스트 해보는것을 추천한다.

View Controller 사이의 상호작용은 Unit Test로써 테스트하기에 좋지 않다.

위에서 말한건, Cocoa MVC 사용하는것은 별로 좋지 않은 선택인것 같아 보인다는 것이다. 그러나 글의 서두에 언급했단 특징들의 용어를 정의했었다.

  • Distribution사실 뷰와 모델은 분리되 있지만, View Controller 붙어있다.
  • Testability거지같은(?)분리 때문에 아마 Model 테스트 가능할 것이다.
  • Ease of use다른 패턴에 비해 코드가 적게 든다. 추가로 많은 사람들이 친숙하게 사용하기도하며 경험해보지 못했던 개발자도 쉽게 접근할 있다.

Cocoa MVC 아키텍처 쪽에 시간을 투자할 시간이 별로 없을때 선택하는 패턴이며, 작은 프로젝트에는 지나친 유지보수 비용이 들어간다는 것을 느낄 있을 것이다.

Cocoa MVC 개발 속도면에서는 최고의 아키텍처 패턴이다.


MVP

전달될거라 약속한 Cocoa MVC(Cocoa MVC’s promises delivered)

Passive View variant of MVPPassive View variant of MVP

사진이 애플의 MVC 굉장히 비슷하지 않는가? 이것의 이름은 MVP(Passive View Variant)이다. 그럼 애플의 MVC MVP 같다는 걸까? 그렇지 않다. MVC에서는 View Controller 서로 붙어있지만 MVP에서 중간다리 역할을 하는 Presenter View Controller의 라이프 사이클에 아무런 영향을 끼치지도 않으며, View 쉽게 테스트가능한 복사본(moked) 만들 있다. 그러므로 Presenter에는 레이아웃 관련 코드가 없고 오직 View 데이터와 상태를 갱신하는 역할만 가진다.

만약 UIViewController View라고 말했으면 어떨까.

사실 MVP 입장에서는, UIViewController 자식클래스에 Presenter 아닌 View들이 있다. 이러한 구분은 좋은 테스트 용이함을 제공하지만, 수작업의 데이터나 이벤트 **바인딩 따로 만들어야하기때문에 개발 속도에대한 비용도 따라 온다. 아래 예제에서 확인할 있다:

Important note regarding assembly(중요 요약 모음)

MVP 세개의 다른 레이어를 가짐으로써 이런 문제 집합이 처음으로 나타난 패턴이다. 그러므로 뷰가 Model에대해 알기를 원치 않기 때문에, 현재 View Controller(View 것이다) 모아서 동작시키는건 옳지 않으므로 다른곳에서 동작시켜야한다. 예를들어, 우리는 앱에서 범용적인 모아서 수행하거나 View-to-View 보여주기위한 Router 돌릴 있다. 이슈는 MVP 뿐만아니라 아래 모든 패턴들에게도 나타나는 문제이기도하다.

이제 MVP 특징 보자.

  • DistributionPresenter Model 책임을 거의 분리했고 View 빈껍데기가 셈이다( 예제에서는 Model 빈껍데기 같았지만..)
  • Testability최고로 좋다. View 재사용가능 덕분에 대부분의 비지니스 로직을 테스트 있다.
  • Easy of use위에서 비현실적인 예제에서는 MVC에비해 코드의 양이 2배정도 많이 들지만 MVP 아이디어는 굉장히 명료하다.

iOS에서 MVP 테스트하기엔 좋지만 코드가 길어진다.


MVP

With Bindings and Hooters

MVP 다른 버전(MVP Supervising Controller) 있다. 이러한 다양한 MVP들은 Presenter(Supervising Controller) View로부터 액션을 처리하고 View 적합하게 변경하는 동안 View Model 직접 바인딩을 포함한다(?).

Supervising Presenter variant of the MVPSupervising Presenter variant of the MVP

그러나 우리가 이미 이전에 배웠듯, 막연하게 책임을 나누는건 좋지않은데다, View Model 합쳐버린다. 이것은 Cocoa 데스크탑 개발에서 어떻게 동작하는지와 비슷하다.

전통적인 MVC와같이, 결함이 있는 아키텍쳐의 예제를 찾기 힘들었다.

MVVM

마지막이자 MV(X) 종류의 최고 종류

MVVM은 최근에 나온 MV(X) 종류이다. 그러므로 이전의 MV(X) 문제들을 해결하여 나오기를 기대해보자.

이론적으로는 Model-View-ViewModel이 굉장히 좋아보인다. ViewModel은 이미 우리에게 친숙할테고, View Model 이라불리는 중계자 또한 마찬가지일 것이다.

MVVMMVVM

MVP 비슷하다:

- MVVM View Controller View라고 일컫는다.
- View Model 서로 연결 되어있지 않다.

추가로 MVP Supervising버전에서 처럼 binding 있다; 그러나 여기서는 View Model 관계가 아닌 View View Model 사이의 관계이다.

그래서 실제 iOS에서 View Model 뭘 의미할까? 그것은 기본적으로 UIKit인데 그로부터 View 독립된 표현이거나 상태이다. View ModelModel에서 변경을 호출하고 Model 자체를 갱신한다. 따라서 View나 View Model 사이에서 바인딩을 하며, 적절히 처음것이 갱신된다.

Bindings(바인딩)

MVP 파트에서 간당하게 언급한적이 있다. 그러나 여기서 좀 더 이야기 해보자. 바인딩은 OS X 개발을 위한 박스(역자주: 프레임워크나 툴을 말하는듯 합니다)에서 나왔으나 iOS 툴박스에서는 보지못한다. 물론 KVO나 notification을 가지고 있긴 하지만 그것이 바인딩만큼 편리하지는 않다.

그러므로 

- 바인딩 기반 라이브러리인 KVO에는 RZDataBinding 혹은 SwiftBond 이런게 있다.
- The full scale functional reactive programming beasts like ReactiveCocoa, RxSwift or PromiseKit. (번역하지 못했습니다ㅠ)

사실 요즘엔 MVVM을 들으면 바로 ReactiveCocoa를 말하기도하며, 반대도 그렇다(역자주: 뭐라고??????). 비록 간단한 바인딩으로 MVVM을 만드는게 가능하기는 하나 ReactiveCocoa (혹은 siblings)으로는 최고의 MVVM을 만들수 있게 해준다.

Reactive 프레임워크에는 쓰디쓴 진실이 하나 있다: 큰 책임엔 큰 에너지가 필요하다. Reactive를 사용하게되면 굉장히 혼잡해지기 쉬워진다. 다른말로 설명하자면, 문제가 하나 생기면 앱을 디버깅하는데 시간이 굉장히 많이 걸리며, 아래와 같은 콜 스택을 보게 될것이다.

Reactive DebuggingReactive Debugging

우리의 예제에서는 FRF 프레임워크나 KVO까지도 배보다 배꼽이 식이다. 대신에 showGreeting 메소드를 이용하여 갱신하기 위한 View Model 명백하게 물어 것이고 greetingDidChange 콜백 함수를 위해 작은 프로퍼티를 사용할것이다.

이제 돌아와서 특징들을 나열해보겠다:

  • Distribution우리의 작은 예제에서는 명료하게 나타나지 않았지만, 사실 MVVM View MVP View보다 책임이 많다. 왜냐면 두번째 것이 Presenter 포워드(forward)하고 자신를 갱신 하지는 않은 그 때, 바인딩을 세팅함으로써 View Model에서 처음 것의 상태를 갱신한다.
  • TestabilityView Model View에대해 전혀 모르며, 이것이 테스트하기 쉽게 해준다. View 또한 테스트 가능하지만 UIKit 의존이면 그러고 싶지 않게 원하게 될것이다.
  • Easy of use우리 예제에서는 MVP 비슷한 양의 코드나 나왔으나 View에서 Presenter으로 모든 이벤트를 포워드하고 View 갱신하는 실제 앱에선 바인딩을 사용했다면 MVVM 코드 양이 적을 이다.


MVVM 앞에서 말한 장점들을 합쳐놓은것 같아서 굉장히 매력적이다. 그리고 View입장에서 바인딩을 하기 때문에 View 갱신하는데 추가적인 코드를 필요로 하지도 않는다. 그럼에도불구하고 테스트에도 굉장히 좋은 수준이다. (역자주: 완전 극찬이군요)


VIPER

iOS 설계에 레고 조립 경험을 적용하다

VIPER 마지막 지원자다. 이것이 특별히 흥미로운 이유는 MV(X) 카테고리로 부터 나온 녀석이 아니기 때문이다.

이제부터 당신은 책임의 단위가 매우 좋다고 인정하게 될것이다. VIPER 분리된 책임이라는 아이디어에서 생겨난 다른 iteration 만드며, 이번 시간에는 다섯 레이어를 것이다.

VIPERVIPER

  • Interactor데이터 개체나 네트워킹과 연관되어있는 비지니스 로직을 가지고, 서버로부터 그들을 받아오거나 개체 인스턴스를 만드는것을 좋아한다. 이러한 목적으로을 위해서 당신은 VIPER 모듈의 일부로써 몇몇 Services Managers 사용해야 것이나, 다소 외부 의존도가 있을것이다.
  • Presenter—Interactor에서 발생되고 비지니스 로직과 관련있는 (그러나 UIKit과는 관련없는) UI 가진다.
  • Entities일반적인 데이터 객체이다. (데이터 접근 레이어(data access layer) Interactor 책임이기 때문에 Entities 아니다.)
  • Router—VIPER 모듈 사이의 연결고리(seques) 책임을 가진다.

기본적으로 VIPER 모듈은 스크린(screen)이나 당신 어플리케이션의 모든 ***사용자 스토리(user story) 있다인증을 생각해보면 스크린이나 여러개가 하나에 연관되어 있을 있다. 얼마나 작은 “LEGO” 블럭어여야 할까?—전적으로 당신에게 달려있다.

MV(X) 종류와 비교하면, 우리는 책임의 분리가 다르다는걸 확인할 있다:

  • Model(data interation) 로직은 데이터 구조로써 Entities 함께 Interactor 이동된다.
  • 오직 Controller/Presenter/ViewModel Presenter 이동하는 UI 표시 책임을 갖지만, 데이터를 변경할 능력은 없다.
  • VIPER 명시적으로 Router에의해 결정된 네비게이션 책임을 해결한 패턴이다

iOS 어플리케이션 입장에서는 각기 방법으로 라우팅 하는게 도전이라고 수있다. MV(X) 패턴들은 이러한 이슈가 발생하지 않는다.

토픽이 MV(X) 패턴을 반영하지 못했으므로, 예제 또한 라우팅이나 모듈간의 interaction 반영하지 않았다

이제 다시 돌아와 특징들을 살펴보자:

  • Distribution틀림없이 VIPER 책임 분배의 최고봉이다.
  • Testability분리가 잘되있는만큼 테스트에도 좋다.
  • Easy of use마지막으로 여러분이 이미 추측한것처럼 두배 정도의 유지보수 비용이 들것이다. 매우 작은 책임을 위해 수많은 클래스 인터페이스를 작성해야하는 점이다.

그래서 레고는 뭐였나?

VIPER 사용하는 동안 레고로 엠파이어 스테이트 빌딩(위키:엠파이어 스테이트 빌딩은 1931년부터 1972년까지 세계 최고층 건물이었다.) 쌓는 기분이 들것이자, 이것이 유일한 문제이기도하다. 아마 당신 앱에 VIPER 적용시키기에 이를수도 있고 좀더 간편한것으로 고려해도 좋다. 몇몇 사람들은 이걸 아예 무시하고 대포에다가 화살을 쏘아대는 경우도 있다. 지금은 비록 엄청나게 높은 유지보수 비용이 들지만, 그들이 미래에는 그들의 앱에 VIPER 필요할지도 모른다는걸 알고있을거라 생각한다. 만일 당신도 생각이 같다면 Generamba(VIPER 골격을 제공해주는 ) 한번 사용해보길 바란다. 개인적으로는 이건 새총 대신에 자동 대포 조준 시스템을 사용하는 느낌이긴하다.


결론

우리는 몇몇 다른 아키텍처 패턴을 살펴보았고, 무엇이 당신을 괴롭히는지 찾아냈기를 바란다. 그러나 여기에 완벽한 해답은 없고 아키텍처를 선택하는게 당신의 특별한 상황에서 문제의 비중을 등가교환하게 된다는걸 알게되었음을 의심하지 않는다

그러므로 앱에 다른 아키텍처를 섞어 사용하는것은 자연스러운 일이다. 예를들어 MVC 시작했지만 어떤 화면에서만 MVC 관리하기 어려워지는 상황이 생기면 부분만 MVVM으로 바꿀 있다. 이런 아키텍처들은 서로 공존할 있기때문에, 다른 화면이 MVC 골격으로 동작하면 바꿀 필요가 없다



Make everything as simple as possible, but not simpler 
이론은 가능한 간단해야하지만지나치게 간단해서는 안된다
— Albert Einstein




*비지니스 로직 (business logic)

**바인딩 (binding)

***사용자 스토리 (user story)



iOS 아키텍처 관련 번역글



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

,

원문 : medium.com/@mandrigin/ios-app-performance-instruments-beyond-48fe7b7cdf2?source=userActivityShare-d07a45aa48c6-1455001286


iOS 퍼포먼스: Instrument 이상

유저들은 기다리는걸 굉장히 싫어한다. 그들은 앱이 뭔가 초기화 한다는것을 전혀 모른 상태에서 자신들의 업무 처리를 최대한 빠르게 하고싶어한다. 그러므로 앱이 모두 즉각적으로 시작할 있다면 인터페이스가 흐르듯 부드럽게 넘어갈 있다. 퍼포먼스가 나오는 앱은 소프트웨어 마켓에서 경쟁력있는 특징중 하나이다. 개발자로서 앱이 퍼포먼스있게 동작하게 하는것을 자랑스러워하는 것을 원하는것도 이유이다.

그러나 많은 이들이 겪듯 퍼포먼스 최적화는 다루기 어려운 문제이다. 대부분 문제를 직관적으로 접근하기 어렵다. 각기 정리된 측정법 없이는 앱이 느려지는 이유를 알아내는건 매우 어렵다.

당신의 퍼포먼스를 최적화하기 위해서는 데이터에 기반한 결론을 내려야한다. 장에서는 당신의 앱에 서로 다른 부분에서 퍼포먼스 측정 데이터를 어떻게 얻어낼 있는지 보여줄것이다.

파트에서는 아래의 것들을 다룰 것이다.

  • 앱에서의 CPU, GPU, 메모리&베터리 사용량
  • 반응성(리스폰시브니스)
  • 앱시작동안의 시간
  • 유저로부터 퍼포먼스 데이터를 얻는

바로 시작해보자!

CPU, GPU, 메모리&베터리 사용량

첫번째 할일은 CPU, GPU, 메모리를 과하게 사용하는 비효율적인 코드를 찾아내는 일이다. 애플은 일을 하기위한 좋은 (Instruments) 제공한다.

우리가 주로 측정해야할 부분은 아래 4 정도이다.

  • CPU ("Time Profiler" 툴 이용)
  • GPU (“Core Animation” 툴 이용)
  • 메모리 사용량 (“Allocations” 툴 이용)
  • 베터리 소모량 (“Energy diagnostics” 툴 이용)

WWDC 비디오는 당신 앱을 분석하기위한 최고의 정보를 제공한다.
아래는 시작하면서 몇 개 골라보았다:


반응성(Responsiveness)

퍼포먼스 측정(이해 "측정"으로 줄여서 말하겠습니다)에 있어서 다음으로 중요한 것은 UI 반응성이다. 터치 헨들링은 메인 쓰레드에서 발생한다. 메인쓰레드에서 시간이 걸리는 작업을 하면, 앱은 버벅거리게 될것이다.

몇몇 동작은 CPU 사용하지 않는 주제에 시간을 잡아먹기도 한다. 만약 메인쓰레드에서 동기화 콜을 불렀다면, 콜이 얼마나 시간이 걸리는지 알아내는것이 문제를 해결하는 방법일것이다.

시간을 측정하기 위해 로그를 찍어볼 수도 있다.

한가지 다른 방법은 Viber 개발자들이 만든 솔루션으로 나타내는 것이다. 솔루션은 메인 쓰레드 하나를 400ms보다 많이 멈추지 않게 지켜보고, 체크하는 특별한 스레드를 가지고있다.

Testing Responsiveness (from Viber’s presentation at NSSpain)


Testing Responsiveness (from Viber’s presentation at NSSpain)


많은 정보는 발표자료(PDF, 7MB)에서 확인할 있다.

데이터를 이용하여 너무 많은 시간이 걸린 (메인싸레드가 멈추는데에는 400ms정도가 적당한 최대치이며, 보면 많은 정보를 얻을 있다.) 찾아내고, 이것을 최적화 시킬지 메인쓰레드 밖으로 보내던지 해야한다.

시작 시간

다음으로 중요한 측정은 앱의 시작하는데 걸리는 시간이다. 전형적인 유저는 당신의 앱을 오직 몇분만 사용한다. 시작시간은 앱의 이미지에 좋지않은 영향을 준다.
여기 시작의 2가지 경우가 있다.

  • Cold 시작 : 당신의 프로세스가 동작하지 않고, OS 의해 실행된다.
  • Warm 시작 : 당신의 앱은 최소화되나 죽지않는다. 이것은 백그라운드로부터 다시 불러온다.


색션에서는 리소스를 많이 잡아먹는 Cold 시작에 초점을 맞출것이다
아래에 iOS 앱의 시작 순서가 나와있다.


The Application Startup Phases (from the documentation)

1. 시작하는데 걸리는 시간을 측정한다.

우리는 main()에서부터 applicationDidBecomeActive:까지 시간이 얼마나 걸리는지 측정해야한다.

앱의 기능을 보여주면서 시간을 잡아먹게 하지 말아야한다. cold 시작 시간을 1 미만으로 떨어뜨리려고 노력하는 것이 좋다.

2. 시작하는 순서에서 부분별로 측정한다.

보통 시작시간의 전체만 아는 것은 충분하지않다. 어떤 부분에서 시작시간을 느리게 만들었는지 아는것 또한 중요하다.
밑에 보이는 것들이 가장 중요한 부분들이다

  • -[AppDelegate application:didFinishLaunchingWithOptions:] - 콜백은 런칭이미지(혹은 스토리보드) 보여질때 호출된다. 곧바로 메소드로부터 return되면 실제 UI 로딩을 시작한다.
  • -[UIViewController loadView] - 앱에 커스텀 뷰를 불러와야한다면, 여기서 뷰를 초기화하게된다.
  • -[UIViewController viewDidLoad] - 뷰가 불러와졌고, 마지막 초기화의 시간이다.
  • -[AppDelegate applicationDidBecomeActivate:] - UI 이미 초기화되어있지만, 콜백이 끝날때까지 블럭되있다. 메소드는 백그라운드로부터 restore될때 호출된다.

몇몇 매소드가 너무 시간이 많이 걸린다면, 최적화시켜야한다.

3. “under pressure” 시작 시간을 측정한다.

전형적으로 현실과 테스트 환경은 다르다는걸 알아야한다.

당신의 앱은 안타깝게도 "현실세계"에서 작동된다. 유저는 종종 다른 앱에서 당신의 앱을 열기도 한다. “다른 굉장히 무거운 앱일 수도 있다. 당신의 앱이 시작될때 다른 무거운 앱은 백그라운드로 가면서 데이터를 저장하려고 있으며, 그런 상황에서의 시간 측정은 굉장히 중요하다.

그런 테스팅(무거운 다른 앱에서 내 앱을 열어보는)을 통해 예측하지 못한 결과를 만들 수도 있기 때문에, 이전에는 코드가 완벽하게 안정적이다가도, 저런 상황에서는 느려질 수도 있다는걸 명심해야한다.

4. 앱이 이미 시작되었지만 여전히 소용없다.(무슨 의미인지 모르겠습니다)

만약 당신 앱이 곧바로 UI 불러오는것이 무의미한 일이라면, 런칭화면이 끝나지 않을 것이다. 비록 UI 불러와지고 반응이 왔었어도, 불러오는데 준비를 위한 데이터가 필요하며, 또한 역시 시작 시간을 재어봐라.

당신의 유저로부터 측정값을 수집해야한다.

모든 측정은 테스트 환경에서 가능할 것이다. 그것은 반드시 필요하지만 너무 완벽하게까지 할필욘 없다. 만약 당신 앱이 인기있는 앱이라면, 만약 유저들이 해외시장을 기반으로 한다면, 몇몇 유저들은 당신이 예상하지 못한 아주다른 환경을 사용할 수도 있다.

추측하기에 다를 있는 환경들이다.

  • 네트워크 상태
  • 하드웨어
  • 소프트웨어(OS 버전, Jailbreak(탈옥...)
  • 기기의 남은 용량
  • 기타 등등

분명 당신 개발실에서 측정한 것은 모두 안정적인 상태일지라도, 별점하나로 컴플레인("앱이 너무 느림ㅡㅡ") 리뷰를 받을 수도 있다. 어떻게 대처하면 될까?
퍼포먼스 측정의 집합을 정의하고(혹은 KPI) 실유저로부터 얻어와야한다. 이것은 대부분 통계 패키지와 함께 사용할 있다.

아래는 당신의 유저로부터 얻을 있는 KPI 예시들이다.

  1. cold 시작시간
  2. warm 시작시간
  3. 마디별 시작시간
  4. 반드시 서버로부터 다운받아야할 것들의 소요시간
  5. 메인쓰레드가 400ms보다 오래 블럭되는
  6. 메모리 워닝이 일어나는
  7. FOOMS(링크)
  8. UI 블럭되거나 쓰잘때기없는 동작의 길이 

결론

쉽게 설명하자면 퍼포먼스 측정은 Instruments.app 프로그램을 열면서 시작하며, 앞에서 본 것 말고도 다양한게 있다. 몇몇 소개된 방법은 구현하기 쉽지만 어떤건 시간과 노력이 필요할 것이다. 어쨋든 솔루션들은 당신의 퍼포먼스 이슈를 찾고 해결을 위한 모니터링하는데 도움을 줌과 동시에 즐겁게 만들것이다.

별점5 리뷰를 받기를 바란다!


+) 이해하면서 의역을 부분이 많이 있습니다. 잘못 이해한 부분이 있다면 지적해주시면 감사합니다.


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

,

invalid Toolchain. New apps and app updates must be built with the public (GM) versions of Xcode 6 or later, and iOS 8 SDK or later. Don't submit apps built with beta software.

앱 개발을 완료하고 앱 검수를 위해 앱을 업로드한다. 그다음 "심사를 위해 제출"을 누를 때 위와 같은 오류가 뜬다면 나의 경우 아래와 같이 해결했다.

원문 : http://stackoverflow.com/questions/32174954/submitting-app-from-building-in-xcode-6-4/32233429#32233429

Apps that you submit should be developed using the latest version of Xcode from the Mac App Store and should be built for publicly available versions of iOS, OS X, and watchOS — except when GM seeds are available. Now Mac App Store's Xcode is 6.4 and OS X Yosemite is Build 14F27. If you user xcode 6.4 on OS X El Capitan, you should follow the steps:

  1. Using Xcode, then archive your project
  2. Open organizer, find your .xcarchive file find .xcarchive file
  3. Right click the xcarchive file, choose [Show package Contents]
  4. Find Products/Applications/XXX.app/Info.plist
  5. then change [BuildMachineOSBuild] value to 14F27, just like this: example
  6. Now, you can go to Xcode->organizer, then 【Submit to App Store】




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

,

안녕하세요. 아이폰 앱 개발자 카나피오입니다. 저는 지난 3년간 아이폰 앱 개발을 하면서 모았던 유용한 링크를 공유합니다. 대부분 실제로 사용하고 있는 링크를 선별했고, 필요 없는 링크는 제외했습니다. 또한 최대한 자료를 무료로 제공받을 수 있는 링크만 뽑아보았습니다. 혹시 좋은 링크가 있다면 댓글에 달아주세요! 서로 공유하면 좋으니까요.


디자인 참고
Dailygood Design
디자인 관련 링크 모음 사이트. 유명한 디자인 사이트를 모아놓은게 아니라 필수적인 디자인 사이트를 모아놓았다. 디자인 사이트를 모아놓은 사이트조차 예쁘다. 나는 즐겨찾기 해놓고 종종 들어간다.

Dribbble
앱이나 웹의 UI를 참고할 수 있는 디자인 웹 커뮤니티 사이트. 내가 가장 애용하는 디자인 참고 사이트이기도 하다. 보통 앱 개발을 시작할 때 이 사이트를 켬으로써 화면 기획을 시작하기도 한다. UX보다는 UI 디자인이 많다. 예쁘다.

Pinterest
영감을 얻기 위한 사이트. 앱, 웹, 제품 등등 온갖 디자인들이 있다. 더 나아가 영감을 얻을 수 있는 다양한 작품들도 있다. 카테고리도 다양하게 있어서 자신의 취향에 맞게 팔로우하여 볼 수 있고 자신이 저작권에 상관없이 자료들을 모을 수도 있다.

Behance
어도비사가 만든 디자인 커뮤니티 사이트. 어도비의 지원을 빵빵하게 받는 만큼 자료가 많다. (근데 웹사이트가 좀 느리다ㅠ)

Vimeo
영상 디자인 참고 사이트. 작품성 있는 영상 자료들이 많이 올라가있다. 유튜브와 비슷하지만 용도는 조금 다르다. 훨씬 질 좋은 영상 자료들이 올라가있고 고품질을 지향한다. 나는 앱개발 영상을 제작할 때 주로 사용하며, 앱개발시 모션 그래픽을 참고하기도 한다.


디자인 리소스
Noun Project
아이콘 모음 사이트. 온갖 아이콘이 많이 있다. 하악하악


개발
Google Play Console
구글 개발자 페이지.

iTunes Connect
애플 개발자 페이지.


통계
App Annie
개발자 계정을 연결하여 자신의 앱(또는 다른 앱)의 실시간(1시간 정도의 시간차가 있습니다.) 순위권이나 다운로드 수, 수익, 사용자 리뷰, 앱스토어 피쳐드 등등의 퍼블리싱 정보를 얻을 수 있다.

구글 어널리틱스
구글에서 제공하는 서비스 분석기. 자신의 앱에 코드를 심어 놓으면 실시간 사용자나 누적 사용자, 활성 사용자 등 다양한 사용자 정보의 통계를 확인 할 수 있다. 화면 전환이나 이벤트 등을 등록해놓고 사용자가 얼만큼의 빈도로 이벤트를 발생시키는지 알 수 있다.


참고
아이콘 메이커
1024*1024 크기의 아이콘을 넣으면 개발에 쓰이는 아이콘 크기별로 뽑아준다. 이게 겁나 편하다. 안드로이드, 아이폰, 아이패드, 애플워치 등등 해당 디바이스의 아이콘을 다 뽑아준다.

iOS 폰트 리스트

애플 앱스토어 리뷰 시간
http://appreviewtimes.com/
애플 앱스토어의 앱 리뷰 대기 시간을 평균 내어주는 사이트.


추가
유용한 사이트 모음
https://github.com/etdot/bookmark
좀 더 다양한 사이트 모음. 



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

,

원문 : http://www.raywenderlich.com/14865/introduction-to-pixel-art-for-games

 Glauber Kotaki 

픽셀아트는 요즘 게임쪽에서 아주 인기있는중인데, 그 이유는 아래 세가지 정도라고 생각한다.

  • 외관(Looks). 픽셀아트는 보기 훌륭하다! 한 스프라이트에 각 픽셀에대해 만드는 것에대핸 이야기를 말한다.
  • 향수를 일으킨다(Nostalgia). 픽셀아트는 플레이어들이 자라면서 즐겼던 닌텐도, Super 닌텐도, Genesis(나의 경우!)등의 엄청난 향수를 불러일으킨다.
  • 배우기가 편하다.(Ease of learning). 픽셀아트는 배우기에 가장 쉬운 타입의 디지털 게임중 하나로써, 특별하게 당신이 아티스트이기보다는 프로그래머 타입에 가깝다면 더욱이 쉽다.

이제 픽셀아트를 만져볼 생각이 있는가? 그럼 나와 같이 따라오면 내가 가장 간단하게 만들면서 게임에 바로 사용할 수 있는 쿨내 풍기는 캐릭을 어떻게 만드는지 보여주겠다!


그리고 추가로, 캐릭 만드는 법을 보여주고 난 다음에는, iPhone game에 적용시키는 법을 Ray가 알려줄 것이다!


이 강좌를 따라오려면 Adobe Photoshop이 필요하다. 만약 포토샵이 없으면, 여기 어도비에서 제공하는 무료 다운로드 링크(http://www.adobe.com/cfusion/tdrc/index.cfm?product=photoshop)를 이용하면 된다.


픽셀속으로 빠져들어 읽어내려가보자!

픽셀 아트가 뭔가?

시작하게전에 “픽셀아트”이 뭔지 부터 깔끔하게 잡고 가자. “픽셀아트”은 당신이 생각하는것 만큼 명확한 뜻이 있진 않다. 그래서 의견이나 스타일의 문제이나 이번 강좌에서 정의를 나름 지어질 수 있길 바란다 :]

가장 쉽게 “픽셀아트”을 정의하는 방법은 무엇이 “픽셀아트"이 아닌가를 생각하면 된다. 무엇이든 generates 픽셀이면 픽셀아트가 아니라 할 수 있다. 아래 예제가 있다.

GradientGradient

Gradients : 두 컬러를 고르고 공간의 간격 사이에 두 색을 계산하여 넣는다. 보기에 굉장히 좋아보이지만 pixel art라고 할 수 없다!

BlurBlur

Blur tools : 픽셀들을 확인하여 이전의 이미지의 새로운 이미지를 만들기위해 되풀이/수정한다. 이것 또한 pixel art라 할 수 없다.

Anti-aliasAnti-alias

Anti-alias tool (기본적으로 서로 다른 색갈의 픽셀들을 보기 ‘부드럽게’하려고 새로운 픽셀을 생성해내는 것). 
당신은 이제 저것들을 피해야한다. 

몇몇은 자동적으로 색을 뽑아내주는 것 또한 pixel art가 아니라고 말하기도하며, 레이어 blending effects(알고리즘을 이용하여 두 레이어의 픽셀을 합치는 것) 또한 아니라는 것을 의미한다. 그러나 요즘의 하드웨어부터는 100만 컬러를 사용할 수 있으므로, 이것은 무시할 수 있다 - 아직, pixel art에서는 작은 컬러 사용이 좋은 연습이 될 수 있다.

line tool이나 paint bucket tool과 같은 다른 툴들 또한 자동으로 픽셀을 계산하여 넣어준다. 하지만 당신은 픽셀 경계에 안티 에일리싱을 하지 않게 설정함으로써, 모든 픽셀을 채울 수 있게 해주며, 이것은 pixel art스럽다고 할 수 있다.

끝으로, pixel art는 스프라이트에 각 픽셀의 좋은 배치에대한 모든것을 말하며, 대부분 한땀한땀 제한된 색의 팔레트로 작업한다. 이제 한번 도전해보자! 


시작하며

첫 pixel art를 만들어보기 전에, 당신은 pixel art는 쉽게 크기변환을 할 수 없다는 사실을 알아야한다. 만약 스케일을 줄인다면 그것은 어떤 덩어리로 보일 것이다. 아니면 스케일을 크게한다면 두배정도 올리기 전까지 보기 괜찮을 것이다.

이 문제를 피하기 위해서는 시작하기 전에 게임 캐릭터/적/게임 요소들의 크기를 얼만큼 할건지 정확하게 생각하여 잡아놓아야한다. 당신이 타겟으로 잡은 디바이스의 화면 사이즈를 기반으로 눈으로 보기에 얼마나 큰 “pixels”을 사용할지 정할 수 있다.

예를 들어 해상도가 480x320 픽셀의 iPhone 3GS에서 두배 사이즈의 게임이 되길 원한다고 해보자(“난 내 게임의 픽셀이 굉장히 북고풍으로 보이길 원한다!”). 그러면 당신의 작업 해상도는 그 절반의 사이즈(240x160 픽셀)로 해야한다.

포토샵에서 새 캔버스를 열고(File > New ...) 저 사이즈로 설정한 다음 한번 확인해봐라. 그리고 당신의 캐릭터 사이즈를 정하면 된다. 


32x32 pixel이다!

나는 32x32 픽셀을 그냥 막 고른 것이 아니라 이 해상도에 적합해 보여서 골랐다. 그러나 32x32픽셀은 2의 제곱수이고 이것은 게임엔진에서 다루기 편할 수 있다.(타일 사이즈들이 2의 제곱수이고, 텍스처가 2의 제곱수로 체워지기 때문의 이유 등등)

Tip : 비록 사용하는 게임엔진이 어떤 사이즈라도 적용이 가능할지라도, 짝수의 이미지 크기는 굉장히 좋은 습관이다. 이 방법으로, 이미지의 스케일이 커져야한다면, 이 짝수의 크기가 나눠졌을때 깔끔하게 보기 좋게 나타날 것이다.


너의 첫 캐릭을 만들어보자

pixel art는 최고의 모양새나 명료함 그리고 그래픽적으로 판별하기 쉬운 것으로써 잘 알려져있다. 당신은 캐릭터의 얼굴, 눈, 머리카락, 몸통 등의 부분을 고작 몇 픽셀로 표현할 수 있다. 그러나 개발자 사이즈는 더 복잡하다. 더 작은 캐릭은 모든것에 맞추기 어렵다.

더 많은 예제로 만들어서 캐릭에 어떻게 더 작게, 보기 쉽게 할지 정한다. 나는 항상 눈을 선택한다. 그 이유는 (마법적이게도) 캐릭에게 생명을 불어넣는 가장 좋은 방법중 하나기 때문이다.

포토샵에서, Pencil tool을 고른다. 만약 찾지 못했다면 Brush Tool을 꾹 눌러서 스크롤을 내리면 Pencil Tool이 나온다(아마 두번째에 있을 것이다). 그리고 brush size를 1로 설정해야한다(tool options bar에서 클릭하여 크기를 변경하거나 ‘[‘단축키를 누르면 된다)

당신은 또한 Erase tool 또한 필요하다. 클릭하여(혹은 단축키 ‘E’) “Mode:Pencil”로 바꿔 설정한다. (이게 브러쉬의 안티에일리싱을 막아준다)

그리고 픽셀링을 시작해보자! 아래와 같이 두 눈썹과 눈을 이미지에 그린다.


키읔! 난 픽셀링한다!! (역자주:미국 개그가 물씬 느껴지는 멘트내요)


당신은 벌써 lineart(당신이 자연스럽게 그리듯이 라인을 그리고 캐릭의 모양을 잡으면된다.)를 시작할 수 있다. 그러나 더 연습을 위한 방법으로 실루엣을 먼저 만들어보자. 좋은 점은 이 단계에서 완벽하게 할 필요 없이 표현할 것의 크기와 자세를 잡아보는 것이다.

회색으로 아래와 같이 해보자:

이 단계에서는 완벽하게 할 필요 없다는 것을 명심하자

빈 공간을 남겨두는 것 또한 주의하자. 당신은 빈 캔버스를 채울 필요가 없고, 나중에 다른 프레인에 더 많은 공간이 필요하게 될 것이다. 그리고 모두 같은 캔버스 크기를 유지하는게 굉장히 유용할 것이다(역자:무슨 말인지 이해하지 못했습니다..)

실루엣 작업이 끝나고, lineart작업을 할 차례이다. 이제 좀 더 조심스럽게 픽셀을 건드려야한다. 옷이나 방어구같은 디테일한건 아직 건드리지말고 작업하자. 만약 그것이 필요하다면 새 레이어를 만들어서 디테일한것을 작업하며, 실루엣이 작업된 레이어가 수정되지 않게 한다.

pencil tool로 작업하는데 엄청 느리게 느껴진다면 Line tool을 사용하는것도 괜찮은 방법이다 - 대신 Pencil 만큼 정확하진 않다는걸 기억하라. 그리고 아래 보이는 것과 같이 설정할 필요가 있다.

사각형을 누르고 있음으로써 스크롤을 내려 Line tool을 선택하자.

tool settings bar로 가서 세번째 아이콘 (“Fill pixels”)을 고르고, 굵기(Weight)를 1로 한뒤 “Anti-alias” 체크를 해제한다. 아래와 같이 보이면 된다.

Tip : 내가 아래쪽 발부분에 테두리선을 만들지 않았음을 주의하라. 발은 다리와 구별하는 필수적인 요소가 아니므로써 반드시 필요한게 아니고, 그럼으로써 당신은 한 줄의 픽셀을 절약할 수도 있다.


색과 그림자를 적용시키기

이제 색을 입힐 준비가 되었다. 일부로 아주 적당한 색을 고르려 하지는 않아도 된다. 색은 나중에 굉장히 쉽게 바꿀 수 있으니 말이다. 대신 모든 것이 고유의 색을 가지게 만들어라. 이제 Swatch 탭(Window > Watches)에서 기본 색들을 사용할 수 있다.

돌아와서 당신의 창조물을 아래와 같이 색을 입힌다. (그냥 자유롭고 크리에이티브하게 당신의 색을 입혀라!)

대조적인 좋은 색감은 당신의 작업물의 가독성을 더 좋게해준다!

나는 아직 옷이나 머리카락의 어떠한 아웃라인도 만들지 않았음을 알고 있어라. 당신이 할 수 있는한 많은 픽셀을 절약하는 것을 항상 기억해라!

아참, 각 칼라 픽셀을 조심스럽게 배치하는데 시간을 낭비하지 말라. 작업 속도를 높히기 위해서는 색에 대한 라인은 잡고 Paint bucket tool을 이용해서 공간을 채우는 방법이다. 당신은 이 툴 또한 설정해줘야한다. 툴바에서 이 툴을 선택하고(단축키 ‘G’를 눌러도 된다) Tolerance을 0으로 바꾸고 anti-alias 체크를 해제한다.

Tip : 만약 Magic Wand tool을 사용해본 적이 없다면, Paint bucket과 같은 설정을 적용해주어라 - tolerance나 anti-alias를 없핸다.


다음 작업에서는 기초적인 빛과 그림자에 대한 상식이 필요하다. 만약 자세히 모른다면 여기링크에 빠른 가이드가 있고, 여기 하나더 완성된 링크가 있다. 만약 지금 당장 배우고 싶지 않다면 그냥 이 단계를 스킵하고 “Spicing up your palette”(모든것이 끝난뒤 넣는 명암은 스타일을 정하고, 당신의 게임의 느낌과 장래를 정한다)로 가도 된다.

혹은 심플하게 내 예제와 비슷하게 명암을 넣어도 된다!


전체에서 같은 밝기를 사용한다.

당신이 할 수 있는/원하는 만큼 모양을 주려고 해보라. 그러면 보통 결과물이 보기에 더 풍성하게 보일 것이다. 예를들어 이제 당신이 코, 눈쌀, 머리볼륨, 깊이감, 접힌 바지들을 볼 수 있다. 또한 약간의 발광점을 추가하여 더 괜찮게 보이게 하면 된다.


명암에 같은 밝기를 유지하라


당신의 팔레트에 양념을 치자

많은 사람들은 기본 팔레트 색을 사용한다. 그러나 그것 때문에 많은 게임들이 서로 비슷해 보이기도 한다.

포토샵은 엄청 다양한 색을 가진 표준 팔레트를 가지고있다. 그러나 그것에 너무 의존해서는 안된다. 하단의 툴바에서 메인 팔레트를 클릭하여 당신의 고유 색을 만드는게 최고이다.

그런다음, Color Picker Window에서 오른쪽 바에 색을 고르면서 찾아보고 메인 영역에서 밝기(흰색<->검정색)를 선택하고 채도(선명한<->흐릿한)를 선택한다.

당신이 색을 고르고 나면 OK를 누르고 Paint Bucket tool을 다시 설정해라. 아, 혼란스러워하지말고 그냥  “Contiguous”박스의 체크를 해제하면된다. 이제 새로운 색으로 색칠할때, 모든 픽셀들이 같은색으로 칠해지게 될 것이다.

이것은 왜 색 갯수를 줄이는게 중요한지에대한 이유이고, 같은 요소(셔츠, 머리카락, 헬맷, 방어구 등등)를 작업할때 항상 같은 색을 사용해야한다. 그러나 각 다른 영역에는 다른 색을 사용해야함을 잊지말자, 그렇지 않으면 다시 색칠해야할 수도 있다!


같은 색의 픽셀들을 모두 칠하기 위해 “Contiguous”의 체크를 해제한다.

당신이 만족할 때 까지 색을 바꿔보자. 그리고 당신 캐릭터에 섹시한 색이 될 때까지 해보면 된다! 당신은 아웃라인 또한 다시 채색할 수 있다. 그냥 배경과 함께 적절히 알맞게 만들면 된다.

마지막으로 배경색을 테스트해보자. 캐릭터 레이어 밑에 새 레이어를 만들어서 다양한 색을 넣어보면 된다. 이 작업은 캐릭터가 보여질 밝거나 어둡거나 따뜻하거나 시원한 배경들에게 알맞은지 확인하는 것은 굉장히 중요하다.


포토샵에서 픽셀을 수정하는 팁

당신도 보았다시피 나는 내가 사용하는 모든 툴의 anti-alias을 껐다. Elliptical marquee 나 Lasso tool과 같은 다른 툴들은 이 기능을 끄지 않았음을 명심하자.

그것은 일부분의 사이즈를 바꾸거나 회전을 시킬때, 더 나아가 애니메이션을 돌릴때도 편하게 작업할 수 있다. 이렇게 하여, marquee tool (단축키 ‘M’)을 이용하여 영역을 지정하고, 오른쪽 마우스 눌러서 ‘Free Transform’을 선택한다(혹은 단축키 Ctrl+T). 그러면 리사이징이나 회전을 쉽게 할 수 있을 것이다.

그러나 포토샵은 Transform 기능을 사용하여 수정한 모든것에 자동적으로 anit-alias을 해버린다. 수정물을 확인하기 전에 Edit > Preferences > General (Ctrl+K)에 들어가서 “Image Interpolation”을 “Nearest Neighbour”으로 바꾸어라. 한마디로 이것은 새 포지션과 사이즈를 어림잡아 계산해주며 새 컬러나 투명도를 적용시키지 않고 당신이 선택한 색들이 알아서 변경되지 않게 막아준다.


iPhone Game에 Pixel Art 넣기

레이먼이 왔습니다. 나는 이 강좌에 쏙 들어와서 어떻게 Cocod2D 아이폰 게임 프레임워크로 Pixel art를 적용시킬 수 있는지 보여줄 것입니다.

만약 Cocos2D가 처음이거나 아이폰 개발이 처음이라면 다른 수많은 강좌보다 Cocos2D and iPhone tutorials 이 강좌를 먼저 보고 오길 추천합니다. 그리고 당신이 Xcode와 Cocos2D가 깔려있고 기본 지식이 있다면, 읽어주세요! :]

iOS\cocos2d v2.x\cocos2d iOS template, 이름은 PixelArt로하고 iPhone for device family를 선택하여 새 프로젝트를 만듭니다.

그리고 당신의 프로젝트에다가 당신이 최종적으로 만든 pixel art 캐릭터를 드래그해넣습니다.

다음 HelloWorldLayer.m을 열고 아래 init메소드로 대체합니다.

1
2
3
4
5
6
7
8
9
-(id) init { 
    if( (self=[super init])) { 
        CCSprite * hero = [CCSprite spriteWithFile:@"sprite_final.png"]; 
        hero.position = ccp(96, 96); 
        hero.flipX = YES; 
        [self addChild:hero]; 
    } 
    return self; 
}

우리는 스프라이트를 화면의 왼쪽 사이드에 놓고 그가 오른쪽을 보는것처럼 만들것입니다.

컴파일하고 돌려보면 당신이 예상한대로 스프라이트는 화면에 나타날것입니다. 

그러나 이 강좌에서 처음 말했던것을 기억하세요. 우리는 일부로 크기를 키워야합니다. 그리고 각 픽셀들이 추가 블록을 주어 픽셀아트 느낌이 나도록 보이게 하고 싶다.

따라서 init 메소드에 아래 라인을 추가하자:

1
hero.scale = 2.0;

쉽지요? 컴파일과 실행... 잉? 스프라이트가 블러처리가 되버렸다! 

이건 스케일이 바뀔 때 Cocos2D의 기본 세팅인 anti-aliases art 때문이다. 픽셀아트에서는 이걸 사용하지 말아야한다. - 우리는 딱 떨어지는 엣지가 필요하다.

운좋게도, 아래 라인 하나만 더 추가하면 굉장히 쉽게 해결할 수 있다.

1
[hero.texture setAliasTexParameters];

이 설정은 image의 스케일을 변화시켜도 anit-aliasing이 일어나지 않게 Cocos2D를 세팅하고, 이것은 “pixel-like”하게 보이게 만든다. 컴파일과 실행하여... ㅇ어로올1!! 된다!!

pixel art를 사용할 때 이점을 말하자면 - 우리는 실제 화면에 보이는 그림 사이즈보다 작은 이미지 파일을 사용하면 된다, 이건 텍스쳐 용량을 엄청나게 줄여준다. 그리고 우리는 레티나 디스플레이를 위한 이미지를 따로 준비할 필요가 없다!


이제 뭘 해야하나?

나는 당신이 이 튜토리얼을 즐기고 pixel art에대해 조금이나마 배워갔기를 바란다.

헤어지기전에 여기 마지막 팁들을 주겠다.


  • 항상 anti-alias, gradients나 너무 많은 컬러를 사용하는것을 피해야 한다는 것을 기억하자. 이것은 당신이 알지 못하고 하는것보다 알고 하는것이 당신 자신에게 굉장히 좋을 것이다.
  • 만약 당신이 북고풍을 구현하기를 REALLY 원한다면, 8-bit나 16-bit와 같은 완전 극한의 오래된 콘솔 작품을 한번 구경해 보아라.
  • 거기엔 ‘복고풍’을 떠나서 엄청나게 많은 스타일이 있다. 하드웨어가 좋아지면서, 리소스들은 더 좋고 더 다양한(섹시한) 색을 사용할 수 있게 되었다. 이것은 오래된 하드웨어에서 어떻게 픽셀아트가 만들어지는지 몰라도 되게 되었으며,  더 다양하고 새로운 아티스트들을 이끌었다. Newer consoles like Game Boy Advance, Nintendo DS, Playstation 1 and some mobile phones have games with these styles, so look into those!
  • 몇몇 스타일에서는 어두운 테두리가 없다; 어떤건 빛이나 그림자가 없다. 이것은 스타일에 따라 다르다! 이 강좌를 통해 알게된 작업물에 어떻게 명암을 주는지 아는것은 훌륭한 일이지만, 당신이 마음 가는대로 스타일링하는것을 마음속에 새겨두자.
  • 만약 좀 더 자신이 있다면, 다음 용어들로써 더 세세한 강좌를 찾아볼 수 있을 것이다: isometric, dithering, anti-alias, celout and subpixel animation.

Pixel art는 시작할 때 쉬워서 쉽게 느껴질 수 있지만 사실 엄청난 시간이 걸려야 좋은 pixel art 가 만들어진다.

스킬을 올리는 가장 좋은 방법은 연습, 연습 또 연습이다. - 그리고 pixel artists에게 조언을 받는것까지!

나는 당신이 pixel art 포럼에 당신의 작업물을 올려 다른 작가로부터 조언을 받을것을 강력히 추천한다. 작게 시작하고, 많이 연습하여 피드백을 받으면 당신은 엄청난 pixel art 게임을 탄생시킬 준비가 된 것이다!

만약 pixel art에 대해 다른 질문이 있다면, 아래 토의 포럼에 가입하고 당신들이 미래에 굉장한 pixel art를 만들 수 있는 모습을 보게되면 좋겠다! :]


translated by canapio


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

,
이번 포스팅에서는 iOS에서 Objective-C로 푸시를 구현하는 방법에 대해 설명하겠다. 그렇지만 단순히 푸시를 받는것이 다가 아닌 푸시를 받았을 때 다음 이벤트 처리도 앱에서 어떻게 처리할지에대한 고민도 함께할 것이다. 이 부분은 애플에서 제공하는 방법이 생각보다 복잡하게 되있는것 같으므로 포스팅에 남기기로 하였다.

먼저 푸시를 받는 방법에 대해 설명하..  검색해보니 바로 원하는 링크가 안나와서 푸시 받는법부터 설명해보겠다.



1. 푸시 아이디를 얻어내는법

푸시 아이디를 얻고자 하는 시점에서 다음을 호출하면 된다. 
// 현재 푸시가 On인지 Off인지 알아내는 함수
BOOL pushEnable = NO;
if ([[UIApplication sharedApplication] respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    pushEnable = [[UIApplication sharedApplication] isRegisteredForRemoteNotifications];
} else {
    UIRemoteNotificationType types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
    pushEnable = types & UIRemoteNotificationTypeAlert;
}

// 푸시 아이디를 달라고 폰에다가 요청하는 함수
UIApplication *application = [UIApplication sharedApplication];
if ([application respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    NSLog(@"upper ios8");
    // iOS 8 Notifications
    [application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
    [application registerForRemoteNotifications];
} else {
    NSLog(@"down ios8");
    // iOS < 8 Notifications
    [application registerForRemoteNotificationTypes:
     (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound)];
}


// AppDelegate.m 파일에서 아래 함수를 추가한다. 아래 함수는 푸시 아이디를 받아내는 함수이고, 푸시아이디는 아래 함수를 통해서만 받을 수 있다.
// AppDelegate.m
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
     NSString* newToken = [[[NSString stringWithFormat:@"%@",deviceToken]
                           stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]] stringByReplacingOccurrencesOfString:@" " withString:@""];
     NSLog(@"DeviceToken : %@", newToken );
}


위 코드를 보면 이것저것 복잡해보인다. 여기서 중요한 라인만 설명을 하자면

UIApplication *application = [UIApplication sharedApplication];
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
[application registerForRemoteNotifications];
위 세 라인이다.
1. 앱 싱글톤 델리게이트를 받아와서 2. 푸시받을 타입을 정한다. 그리고 3. 실제로 푸시 아이디를 달라고 요청한다.
registerForRemoteNotifications가 호출되는 순간 appdelegate.m에 
첫째, 앱을 설치하고 처음으로 registerForRemoteNotifications를 호출하는 상황이면 사용자에게 푸시를 허용할지 말지에 대한 경고창이 뜬다. 여기서 허용을 하면 AppDelegate.m에 구현된application:didRegisterForRemoteNotificationsWithDeviceToken: 함수가 호출되면서 deviceToken파라미터에 푸시아이디가 담기게 된다. 반대로 사용자가 푸시 허용을 하지 않는다면 application:didRegisterForRemoteNotificationsWithDeviceToken: 함수는 호출되지 않는다. 그게 다다. 사용자가 푸시알림 허용 안했다고해서 뭐 어떻게 더 바로 할 수 있는게 없다. 푸시알림 무한 유도를 막기위함의 애플의 생각이 아닐까 싶은데, 이 부분때문에 고민을 좀 많이했다. 그래서 준비한게 BOOL pushEnable;이고 푸시가 현재 허용되있는지 아닌지 대강 판단해준다. 푸시가 꼭 필요한 어플일 경우는 pushEnable를 받아내서 확인할 수 있다. (하지만 이것도 사용자가 한번도 푸시 설정을 안했는지, 실제 푸시 거부를 한건지 알 방법은 없다. 단지 현재 푸시 설정 On/Off 여부만 알 수 있다.)
둘째, 앱을 설치하고 푸시 허용을 해놓은 상태이며(최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 AppDelegate.m에 application:didRegisterForRemoteNotificationsWithDeviceToken:함수가 실행되면서 푸시아이디를 받아올 수 있다. 푸시 아이디를 서버에 등록할 때 사실상 푸시 아이디는 바뀔 수가 있다. 그렇기에 나같은 경우는 푸시아이디와 디바이스아이디를 함께 서버에 보내서 디바이스 아이디 기준으로 푸시 아이디를 저장한다. 아무튼 두번째의 경우는 무조건 푸시아이디를 받아올 수 있는 상황이다.

세번째, 앱을 설치하고 푸시 허용을 해제해놓은 상태이며(마찬가지로 최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 아무일도 일어나지 않는다. 그렇기때문에 이 경우는 미리 pushEnable를 받아내서 푸시가 가능한지 아닌지를 판단해놓을 필요가 있다. pushEnableNO이면 푸시 허용 Off인 상태인거다. 그러면 푸시 설정을 해라고 경고를 띄우면 된다. 


2. 서버에서 푸시를 쏘았을때 폰에서 받은 위 호출되는 함수
서버에서 푸시를 쏘자마자 알아내는 방법은 하나밖에 없다. -> 앱이 실행되는 중일때이다.
앱이 만약 꺼저있거나, 백그라운드에서 돌고있으면 푸시가 왔는지 안왔는지 앱에서는 모른다. 앱이 다시 포그라운드로 왔을때 푸시가 왔으면 ‘푸시받는함수'가 호출된다. 하지만 웃긴것이, 앱이 백그라운드에도 없고 메모리에 올라가있지 않을때 푸시가 왔을때, 바탕화면에서 푸시를 받아서 푸시를 눌러 앱에 들어가면 ‘푸시받는함수’가 호출되지 않는다. 이 예외적인 부분을 처리하는 방법에 대해 설명하겠다.
 
// AppDelegate.m
// 앱이 런칭되서 메모리에 올라갈때 실행되는 함수이다.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    UILocalNotification *notificationUserInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (notificationUserInfo) {
//        NSLog(@"app recieved notification from remote%@",notification);
        NSMutableDictionary *mNotificationUserInfo = [NSMutableDictionary dictionaryWithDictionary:(id)notificationUserInfo];
        mNotificationUserInfo[@"appLaunch"] = @"yes";
        [self application:application didReceiveRemoteNotification:mNotificationUserInfo];
    }else{
//        NSLog(@"app did not recieve notification");
    }
    return YES;
}

여기서
// AppDelegate.m
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

    [PushController shared].userInfo = userInfo;

    if (userInfo[@"appLaunch"]) {
        [PushController shared].waitingViewDidLoad = YES;
    } else {
        if (application.applicationState==UIApplicationStateActive) {
            [[PushController shared] pushInActiveStatus];
        } else {
            // if (application.applicationState==UIApplicationStateInactive) {
            [[PushController shared] presentPushPostTableViewController];
        }
    }
}
위의 PushController는 일단 신경쓰지 말고 푸시를 받았을 때 라이프 사이클만 신경쓰자. 
저기서 특이한 점은 
if (userInfo[@"appLaunch”])
를 이용하여 첫째 상황인지 먼저 구분하고
if (application.applicationState==UIApplicationStateActive) ;
를 이용하여 둘째상황과 셋째상황을 구분했다는 점이다.

첫째 상황에서는 아직 ViewController가 생성이 되기 전의 라이프사이클이다. 그렇기때문에 ViewControllerviewDidLoad를 호출하기 전까지 푸시정보를 잠시 가지고 있어야한다. 
둘째 상황에서는 application.applicationState==UIApplicationStateInactive에 해당하는 상태인데, 현재 앱이 백그라운드에서 포그라운드로 가고 있다는 뜻으로 사용자가 앱을 켜놓고 바탕화면에서 푸시를 눌렀을 때를 말한다. 이때는 바로 푸시 행위를 진행하면 된다.
셋째상황은 application.applicationState==UIApplicationStateActive의 상황으로써 앱이 이미 포그라운드에서 돌고 있었다는 뜻이고 이때는 자동으로 푸시 기능을 실행시켜버리는게 아니라 상단에 팝업창이 떠서 푸시가 온것처럼 만들어주면 사용자가 인지하기 쉬울 것이다. (안드로이드는 기본으로 제공되고, 아이폰은 카톡에서 볼 수 있는 기능이다) 상단에서 팝업이 잠시 내려왔을때 팝업을 누르면 푸시 기능을 수행하게 만들었는데, 이 기능은 PushController에 있다. PushController는 UI작업을 좀 했기 때문에 라인이 좀 되서 첨부해두겠다.

아무튼 위와같이 처리하면 푸시를 받는 일을 놓히지 않고 처리할 수 있다. 
 



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

,
1. 구글 드라이브 web host 폴더 만들기
1) 구글 드라이브 가입하기
2) 구글 드라이브 컴퓨터에 설치하기
3) 배포용 폴더 만들기 (본인의 경우 Google 드라이브>2015 학교>수업>안드로이드실험>프로젝트>ios_APP의 경로로 설정했다)
4) 배포용 폴더를 웹호스팅 공개로 설정


빨간색 네모박스에 있는 키값을 꼭 복사해놓는다. (내 폴더 키는 소중하니까 모자이크처리)
맨 마지막에 URL 만들때 필요하다!

다음으로 고급을 누른다.

변경을 누른다.

'웹에 공개’를 누르고 저장을 누르면 웹 호스팅 폴더가 완성됬다!

5) Finder에서 확인해보면 폴더 모양이 공유된거처럼 생겼다.



2. Adhoc용으로 ipa파일 만들기
1) AdHoc용 Provisioning Profiles을 만들고 더운받아서 실행한다.
-> 아마 Xcode가 실행될 것이다.

2) Product>Archive하기 전에 세팅을 한다.
-> Adhoc으로 Archive(필드와 비슷한 것)해야하기 때문에 세팅을 한다.
- Project>General에서 Team설정 : None에서 자신의 계정으로 설정
- Project>Build Setting에서 Code Signing>Code Signing identity를 iPhone Distribution:XXXXX로 설정
- Project>Build Setting에서 Code Signing>Provisioning Profile을 1번에서 만든 AdHoc Provisioning Profile로 설정

3) Product>Archive를 실행


주의사항 : 시뮬레이터가 선택되있으면 Archive가 비활성되있다. 위 스샷처럼 iOS Device를 선택해주자.

4) 빌드가 끝나면 새로운 창이 뜨면서 어떤식으로 배포할 것인지 정하는 프로세스가 나올것이다. export를 하여 Adhoc을 고르고 구글 드라이브와 연동되있는 폴더에 ipa파일을 생성하면 된다.

3. BetaBuilder를 이용하여 URL만들기

1) choose IPA…버튼을 눌러서 .ipa파일을 가져온다.

2) 하단에 Full Web Deloyment Path를 입력한다. 
-> 구글 드라이브의 경우 https://googledrive.com/host/XXXXXXXXXXXXX
XXXXXXXXXXX부분에는 1.4번 구글드라이브 폴더에서 가져온 ipa파일이 들어있는 해당 디렉토리의 키값이다.


3) Generate Depolyment Files… 버튼을 눌러서 .ipa파일이 들어있는 폴더에 Generate 시킨다.


4. 링크 배포하기
UDID를 등록한 핸드폰(혹은 패드)에서 위 링크를 타고 들어가면 html이 열릴 것이다.
'Tap Here to install’을 누르면 다운이 시작될 것이다.

아래 스샷은 맥에서 연 사진이지만, 단말기의 브라우저에서 열어야한다.



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

,

원본 : http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1

위 링크의 강좌를 2포스트에 걸쳐서 번역을 진행할 것이다. 본 포스트는 위 원문 포스팅의 절반을 번역한 것이다.


Update note : Swift를 사용한 iOS8 기반의 Grand Central Dispatch tutorial 최신 튜토리얼이 있다!

비록 Grand Central Dispatch(혹은 줄여서 GCD)가 얼마동안 주변에 있어 왔다고 해서, 모두가 그것을 잘 아는건 아니다. 이것은 이렇게 이해될 수 있다; 동시에 하는것은 편법이고, C기반 API의 GCD는 Objective-C의 세계로 자연스럽게 뽀죡한 끝으로 푹 찌르는 것처럼 보인다. 깊이있게 두 파트의 시리즈를 가진 Grand Central Dispatch 강좌를 배워보자.

이 두 파트에서, 첫번째 강좌에서는 GCD가 뭘하는지와 GCD 기능 기반의 몇몇 예시를 보여줄것이다. 두번째 파트는 GCD가 가진 더 많은 기능적인 이점을 배울 것이다. 



GCD란?
GCD는 libdispatch를 위한 마케팅적인 이름이다, 애플 라이브러리는 iOS와 OSX의 멀티코어 하드웨어에서 코드 동시 실행을 위한 기능을 제공한다. 이것은 아래 이점들을 따른다:
  • GCD는 무거운 처리를 살짝 뒤로 미루고 백그라운드에서 처리할 수 있게 도와주므로써  당신 앱의 대응력을 높혀준다.
  • GCD는 쓰레드를 락 거는것 보다 더 쉬운 동시 실행 모델을 제공하고 동시 실행시 생기는 버그들을 피할 수 있게 도와준다.
  • GCD는 싱글톤으로써 일반적인 패턴의 높은 퍼포먼스와 함께 당신의 코드를 아름답게 만들어 줄 것이다.
이 강좌는 블록코딩과 GCD가 어떻게 동작하는지 잘 알고있다는 가정하에 진행한다. 만약 GCD를 처음 접한다면 Multithreading and Grand Central Dispatch on iOS for Beginners를 먼저 필수로 보고 오면 좋겠다.


GCD 용어
GCD를 이해하기위해, 여러분은 쓰레딩과 동시실행어대한 몇몇 개념을 명확하게 할 필요가 있다. 이것들은 둘다 모호하고 헷갈리는 것이지만 잠시 다시 GCD의 관점에서 그것들을 가볍게 생기시켜 봐야한다.
(역자 주 : 원문에서는 GCD 용어라고 해놓았지만, 구글에 "쓰레드 용어"로 검색하면 더 많은 정보를 얻을 수 있다)

Serial vs. Concurrent
이 용어는 작업들이 서로관에 연관이되어 실행되는 경우를 설명하는 용어이다. 연속적으로 실행되는 작업들은 항상 한번에 실행된다. 작업들은 한번에 동시에 실행되어야한다.

비록 이 용어가 넓은 응용프로그램이지만, 이 강의의 목적상 당신은 Objective-C block 작업 수행에만 초점을 맞출 수 있다. block이 뭔지 모른다고? 여기 이 강좌(How to Use Blocks in iOS 5 Tutorial)를 참고해라. 사실 당신은 또한 함수포인터와 함께 GCD를 사용할 수 있지만, 대부분의 케이스에서는 이것이 사용에 실질적이고 편법적이다. Block은 매우 편하다!



Synchronous vs. Asynchronous
GCD에서, 이 용어는 함수가 연관된 다른 작업을 끝났을때 함수는 실행하기위해 GCD에게 물어보는 것을 뜻한다. 동기화(synchronous) 함수는 명령된 일이 모두 끝난 후에 리턴한다.

반면 비동기화(asynchronous) 함수는 즉시 리턴하고, 일이 다 끝났다고 알려주지만 실제 일이 끝날때까지 기다리지는 않는다. 그러므로 비동기화 함수는 다음 함수에서 처리되는 실행의 현재 쓰레드 block이 아니다.

조심하자 - 현재 쓰레드의 동기화 함수 “blocks”을 읽을때나 함수가 “blocking”함수 이거나 blocking operation인경우 혼동하지 말자! blocks라는 동사는 어떻게 한 함수가 현재 쓰레드에 영향을 미치는지이고 명사 block과는 아무 연관이 없다. 명사 block는 Objective-C에서 이름없는 함수의 용어이고 GCD에 보내는 일들을 정의한다.

Critical Section
이것은 한번에 두 쓰레드가 도는 상황에서 반드시 동시에 실행되지 않는 코드 조각이다. 동시 프로세스에 의해 접근된다면, 이것은 코드가 (변수와 같은) 공유된 자원을 동시에 건드릴 수 있기 때문에 잘못된 값이 들어가거나 할 수 있다.

Race Condition
---

Deadlock
만약 그 두가지(혹은 그 이상)의 일이 서로의 처리가 끝나기를 기다리고 있을때 이것을 deadlock 되었다고 부른다(대부분의 쓰레드에서 나타날 수 있다). 첫번째 처리는 두번째 처리가 끝나기를 기다리기 때문에 끝날 수 없다. 그러나 두번째 처리 또한 첫번째 처리가 끝나기를 기다리기 때문에 두번째 처리도 끝날 수가 없을 것이다.

Thread Safe
쓰레드 세이프 코드는 여러 문제(데이터 오염, 크래쉬 등등)를 피하면서 멀티쓰레딩이나 동시처리로부터 안전하게 콜이 가능하다. 쓰레드 세이프가 되지 못한 코드는 한번에 한 콘텍스 안에서 동작한다. 쓰레드 세이프 코드 중 하나는 NSDictionary이다. 당신은 멀티 쓰레드 이슈 없이 저것을 이용할 수 있다. 반면 NSMutableDictionary는 쓰레드 세이프가 아니다. 이것은 오직 한번에 한 쓰레드안에서 접근할 수 있다.

Context Switch
context switch는 당신이 한 싱글 프로세스에서 다른 쓰레드를 실행하여 전환할 때의 저장 및 복구 실행 상태의 프로세스이다. 이 프로세스는 멀티쓰레딩하는 앱을 만들때 아주 일반적인 방법이지만, 이것은 추가적인 비용이 따른다.

Concurrency vs Parallelism

참고 한글 자료 : http://skyul.tistory.com/263
Concurrency와 parallelism은 종종 함께 거론된다. 그래서 짧게 그 두개를 비교하여 설명할 것이다.

concurrent 코드의 나눠진 부분은 “동시에” 실행될 수 있다. 그러나 그것이 어떻게 처리되는지는 시스템이 정하기 나름이다.

멀티코어 디바이스는 병렬적으로 같은 시간에 멀티 쓰레딩을 실행시키지만, 싱글코어 디바이스는 이것을 수행하기 위해 쓰레드를 실행시키고, 컨텍스 스위칭을 동작하며, 다른 쓰레드나 프로세스를 작동시킨다. 이것은 아래 그림처럼 마치 병렬적으로 수행되게 보이는데 충분히 빨리 수행된다.

비록 당신이 GCD에서 동시수행을 코드에 사용했었어도, 얼마나 병렬수행이 필요한지 GCD가 정하기 나름이다. 병력수행은 동시에 일어나는 것을 요구한다. 그러나 ‘동시’는 보장된 병렬을 제공하지 않는다.

중요한 포인트는 ‘동시’는 사실 구조에 관한 것이다. 당신이 GCD를 코드에 사용할 생각이 있을 때, 당신은 동시에 일어날 수 있는 일의 부분을 당신 코드 구조체에 노출한다. 게다가 한개는 반드시 동시에 일어나지 않는다. 만약 당신이 이 주제에대해 더 깊게 알고싶으면 this excellent talk by Rob Pike를 확인해보아라.

Queues

GCD는 코드의 블럭들을 다룰 수 있게
dispatch queues를 제공한다; 큐(queue)들은 당신이 GCD에 제공한 테스크를 관리하거나 FIFO 명령에 의한 테스크를 실행시킨다. 첫 테스크를 큐에 추가하는데 이점은 첫번째 테스크가 큐에서 시작되고 두번째로 추가된 테스크가 두번째에서 시작할것이며, 차례로 될 것이다. 

모든 dispatch queues는 당신이 멀티쓰레드에서 동시에 접근하려해도 스스로 쓰레드 세이프하다. 이러한 GCD의 이점은 어떻게해서 dispatch queues가 당신의 코드의 부분에서 쓰레드-세이프한 것을 제공하는지 이해할때 나타날 것이다. 이것의 핵심은 옳은 dispatch queue 종류와 옳은 dispatching function을 골라서 당신의 일의 queue에 보내기 위함이다.

이 섹션에서 특별한 GCD queue가 제공하는 두가지 종류의 dispatch queue를 보게 될 것이다, 그리고 GCD dispatching function과 함께
큐에 어떻게 추가하는지 알려주는 형상화한 예제를 돌려볼 것이다.

Serial Queues

serial queues에서 데스크는 한번에 한회만 실행된다, 각 테스크는 이전 실행되는 테스크가 끝나고나야지만이 시작된다. 뿐만아니라, 아래 그림과같이 우리는 한 블럭의 끝나는 점과 다음 블럭의 시작하는 점의 시간차이를 알지도 못한다.

이 테스크의 동작 타이밍은 GCD의 컨트롤 아래에 이루어진다; 당신이 알고 있는 GCD의 이점은 한번에 한번만 수행한다는것과 queue에 추가되어서 명령을 받으면 테스크를 실행한다는 것이다.

Seiral queue에서는 절때 동시에 두 테스크가 실행될 수 없으나, 동시실행에서 같은 섹션을 접근할 위험은 없다; 이 테스크를 오직 수행한다는 점에서 race condition으로부터 섹션이 위험해지는것을 막는다. 그러므로 위험한 섹션에 접근 할 수 있는 유일한 방법은 dispatch queue에 테스크를 담아서 보내는 방법이다, 그리고 위험한 섹션이 안전하다는 것을 검증받을 수 있다.



Concurrent Queues

concurrent queues에 테스크들은 추가하라는 명령에서 시작하는 보장을 가지고... 그것은 당신이 완전히 보장됬다는 것을 의미한다! 요소들은 어떤 명령들도 끝낼 수 있고 당신이 다음 블럭이 언제 시작될건지 모르거나 많은 블럭이 얼마동안 시간을 잡아먹으면서 동작하는지 몰라도된다. 이것이 GCD의 모든것이다.

아래 그래프는 GCD에서 4개의 동시수행을 동작하는 테스트 샘플이다.


Block 1, 2, 3 모두가 어떻게하면 빨리 연이어서 실행될지 주목하라, Block0이 시작된 후 블럭1이 시작되는 동안 일어날 것이다. 또한 Block3은 Block2 다음에 시작되지만 Block2보다 빨리 끝난다.

block이 언제 시작되는지의 결정은 완전히 GCD에서 한다. 만약 block의 실행 시간이 다른것과 겹칠때, 다른 코어에서 실행시킬지, 한개만 사용할것인지 혹은 코드를 다른 block에 콘텍스 스위칭을 할지 정하는건 GCD가 하기 나름이다.

단지 흥미로운 점은 GCD가 각자 다른 queue 타입으로부터 정해서 적어도 5가지의 각 queue들을 제공한다는 점이다.


Queue Types

처음으로, 이 시스템은 당신이 main queue라고 알고있는 특별한 종류의 큐를 제공한다. 다른 queue들과 같이, 이 큐에서도 한번에 테스크를 수행한다. 그러나 이것은 모든 테스크를 메인 스레드에서 수행할 것이라는 뜻이며, 당신의 UI를 유일하게 업데이트 할 수 있는 스레드이기도하다. 이 queue는 UIView에 메세지를 보내거나 노티피케이션을 보내는 것에 사용되어야하는 유일한 queue이다.

시스템은 또한 여러 동시 queues를 제공한다. 이것은 우리가 알고있는 Global Dispatch Queues라고 불리는 놈이다. 이것은 다른 우선순위를 가진 4개의 global queue가 있다: backgroundlowdefault, and high

마지막으로, 당신은 커스텀화된 종류나 동시 queues 또한 생성할 수 있다. 이 말은 당신이 적어도 5개의 queue들을 마음대로 생성 소멸시킬 수 있다는 뜻이다: main queue, 4개의 global dispatch queues, 추가로 당신이 커스텀화시켜 만든 queues 까지!

그리고 dispatch queue들의 큰 그림이 있다!

GCD의 예술적인 면은 queue에 당신의 일을 적당한 queue dispatching function을 골라 보내는 역할을 한다는 것이다. 이것을 경험하기 가장 좋은 방법은 권장해놓은 길을 따라서 예제를 실행해보는 것이다.



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

,


-(void) setMaskTo:(UIView*)view byRoundingCorners:(UIRectCorner)corners cornerRadius:(CGFloat)cornerRadius{
    UIBezierPath* rounded = [UIBezierPath bezierPathWithRoundedRect:view.bounds
                                                  byRoundingCorners:corners
                                                        cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];
    
    CAShapeLayer* shape = [[CAShapeLayer alloc] init];
    [shape setPath:rounded.CGPath];
    view.layer.mask = shape;
}


view.frame = CGRectMake...하고나서
[self setMaskTo:view byRoundingCorners:...];
을 호출해주어야지 정상적으로 라운딩된 뷰를 볼 수 있다. 


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

,

1. 일단 뷰를 생성해 둔다.

// view 생성
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor colorWithWhite:0.f alpha:1.f];
view.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:view];

x, y : 100, 100
w, h : 100, 100
배경색 : 검정 


2. 테두리

// 테두리
view.layer.borderColor = [UIColor colorWithRed:1.f green:0.f blue:0.f alpha:1.f].CGColor;
view.layer.borderWidth = 1.f;

빨간색의 너비가 1인 테두리를 만든다


3. 둥근 모서리

// 둥근 모서리
view.layer.cornerRadius = 10.f;

 

아래와 같이 둥근 모서리를 이용한 원 형태의 뷰도 만들 수 있다.

UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"profile.jpg"]];
iv.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:iv];


iv.clipsToBounds = YES;
iv.layer.cornerRadius = iv.frame.size.width/2.f;



4. 스케일 변경 및 애니메이션

// 2배로 크게
view.transform = CGAffineTransformMakeScale(2.0, 2.0);

각 너비와 높이의 스케일 값을 넣는다
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeScale(2.0, 2.0);
}];



5. 각도 변경 및 애니메이션

// 180도 회전
view.transform = CGAffineTransformMakeRotation(M_PI);

M_PI는 미리 정의된 3.141592... 파이값이다. 저 값만 바꿔주면 되며 
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeRotation(M_PI);
}];





CALayer

The CALayer class manages image-based content and allows you to perform animations on that content. Layers are often used to provide the backing store for views but can also be used without a view to display content. A layer’s main job is to manage the visual content that you provide but the layer itself has visual attributes that can be set, such as a background color, border, and shadow. In addition to managing visual content, the layer also maintains information about the geometry of its content (such as its position, size, and transform) that is used to present that content onscreen. Modifying the properties of the layer is how you initiate animations on the layer’s content or geometry. A layer object encapsulates the duration and pacing of a layer and its animations by adopting the CAMediaTiming protocol, which defines the layer’s timing information.

CALayer 클래스는 컨텐츠를 기반으로한 이미지를 관리하거나 컨텐츠의 애니메이션 기능을 수행하는 역활을 한다. 레이어는 보통 뷰들의 역할을 보완하는데 사용되기도 하지만 뷰 없이도 콘텐츠를 나타내는데 사용될 수 있다. 레이어의 주된 기능은 당신이 제공한 콘텐츠를 보여주는 역할을 관리하는 것이다. 그러나 레이어 그 자신이 배경색이나 테두리, 그림자와 같은 시각적인 속성을 가지고 있다. 추가적으로 시각적으로 콘텐츠를 관리하기 위해, 레이어는 화면에 보여지고 있는 기하학적인 정보들(좌표, 크기, transform등등)을 가지고있다. 레이어의 속성을 변화시키는 것은 콘텐츠의 레이어나 기하학적으로 애니메이션을 시작할 수 잇는 방법이다. 레이어 오브젝트는 레이어의 지속시간이나 페이스를 담고있고 이것은 CAMediaTiming를 적용시킴으로써 레이어에 정의된 시간적인 정보에 따라 애니메이션 시킬 수 있다.




If the layer object was created by a view, the view typically assigns itself as the layer’s delegate automatically, and you should not change that relationship. For layers you create yourself, you can assign a delegate object and use that object to provide the contents of the layer dynamically and perform other tasks. A layer may also have a layout manager object (assigned to the layoutManager property) to manage the layout of subviews separately. 

만약 뷰에 의해 레이어 오브젝트가 생성되었다면, 뷰는 일반적으로 레이어 델리게이트에 자동으로 지정되있을 것이고 당신은 이 지정을 바꿀 수 없다. 당신이 직접 만든 레이어의 한해서, delegate 오브젝트에 어싸인 할 수 있고 그 델리게이트 오브젝트는 콘텐츠 레이어의 동적인 행동이나 다른 일들을 제공하는 것들을 사용할 수 있다. 레이어는 또한 차별적으로 subview들의 레이아웃을 관리하기 위한 (
layoutManager 속성에 어싸인된) 레이아웃 메니저 오브젝을 가진다.







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

,

1. 블럭 코딩
나는 애니메이션 효과를 쓸 때는 블럭코딩을 주로 사용하는 편인데, 보기도 편하고 사용하기 좋다.
사용법은 아래와 같이 사용하면 된다.

[UIView animateWithDuration:3.f animations:^{
    // 애니메이션 ..
}];


2. CGRect 변경 (좌표 및 사이즈) 애니메이션

view.frame = CGRectMake(0, 0, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.frame = CGRectMake(100, 100, 50, 50);
}];



3. 투명도 변경 애니메이션

view.frame = CGRectMake(100, 100, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.alpha = 0;
}];







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

,



구글 애널리틱스가 뭔지부터 설명하겠다. 이놈은 구글에서 만든 "서비스 분석기”로 웹이나 앱에서 사용자가 서비스(웹이나 앱등을)를 어떻게 실행했는지 얼마나 실행했는지를 분석해서 통계를 내주고 도식화해주는 그런 놈이다. 그러니까 쉽게 예를 들어보면 '하루에 몇명이 접속했는지’, ‘실시간으로 몇명이 접속해있는지’ 이런 정보들을 제공한다. 또한 이런 방대한 기능에 비해서 앱에 적용해 사용하기가 쉽다! (아이고 감사합니다) 한번 구글 애널리틱스를 사용해본 개발자라면 다음부턴 항상 사용하게 될 것을 장담한다.

좀 더 자세한 설명을 보고싶다면 구글애널리틱스 자세히 알아보기 여기 들어가면 구글에서 자세히 설명해놓았다.

이번 포스팅에서는 iOS앱에서 간단히 적용시키는 것에 초점을 맞춰서 글을 써내려갈 생각이다.



구글 애널리틱스 사이트에 내 앱 등록하기

1. 구글 아이디 만든다 (웬만하면 있지요들? 근데 개발자용 하나 따로 만들어 놓으면 편합니다)

2. 구글 애널리틱스 홈페이지가서 가서 구글 애널리틱스 가입을 합니다. (구글 계정이 필요) 오른쪽 상단에 계정만들기 눌러서 진행하면 됨

3. 서비스 등록
- 처음 가입했다면 바로 아래와 같은 창이 뜰것이고

- 이미 가입한 상태라면 

상단에 “관리”라는 탭이 있다. 그걸 눌러서 

“새 속성 만들기”를 누르면 새로운 서비스를 등록할 수 있다.

4. 이것저것 작성하고나서 "추적 ID가져오기” 버튼을 누르면 추적 아이디가 생긴다.
예) DB-271282139-1 이런 모양으로 생겼다. 기억해두고 나중에 코드에 삽입해야한다.


구글 애널리틱스 SDK를 앱에 심기
1. 구글 애널리틱스 SDK를 다운받는다. 다운로드 링크
zip파일로 되있고 3.10버전(14년11월15일 기준)이라고 되있다.


2. 프로젝트에 추가해야할 .h파일들과 .a파일이다.
libGoogleAnalyticsServices.a
그 외 .h파일들을 집어 넣는다.


이렇게 GoogleAnalytics폴더 하나 만들어서 필요한 파일들 넣어주면 된다.

3. 이제 필요한 프레임 워크를 넣을거다

아이고 친절하다

  • CoreData.framework
  • libAdIdAccess.a
  • AdSupport.framework
  • SystemConfiguration.framework
  • libz.dylib
  • libsqlite3.dylib


환경 세팅은 끝났다.
코드 작성하러 가자

4. 코드 작성

AppDelegate.m에 임포트 시킨다.

#import "GAI.h"
#import "GAIDictionaryBuilder.h"
#import "GAIFields.h"

AppDelegate의 application:didFinishLaunchWithOptions:메서드에 다음 코드를 추가한다.
(그냥 초기화, 초기세팅 정도의 기능을 하지만 사실 없어도 동작하더라..)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Optional: automatically send uncaught exceptions to Google Analytics.
    [GAI sharedInstance].trackUncaughtExceptions = YES;
    // Optional: set Google Analytics dispatch interval to e.g. 20 seconds.
    [GAI sharedInstance].dispatchInterval = 5;
    // Optional: set Logger to VERBOSE for debug information.
    [[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelVerbose];
    // Initialize tracker.

    return YES;
}


그리고 이제 실제로 서버에 Hit을 날려줄 코드를 작성한다.
나같은 경우 ViewController.m에 viewDidLoad에다가 넣었다.

#define GOOGLE_ANALYTICS_KEY @"DB-271282139-1"

아까 내가 기억해놓으라고 했던 추적ID를 이제 쓸 차례가 왔다.

- (void) viewDidLoad {
    [super viewDidLoad];

    id<GAITracker> tracker = [[GAI sharedInstance] trackerWithTrackingId:GOOGLE_ANALYTICS_KEY];
    [tracker set:kGAIScreenName value:@"테스트화면1"];
    [tracker send:[[GAIDictionaryBuilder createAppView] build]];
}


마무리
이렇게 하면 이제 앱이 켜지고 저 화면이 켜질때마다 알아서 구글 애널리틱스에 hit을 시킨다. 위치정보나 시간정도 이런거 주서다가 보내주는 모양이다. 나중에 구글 애널리틱스 홈페이지 들어가서 통계자료를 보면 되고,
실제 앱이 잘 연동되었는지 확인을 하려면 앱을 실행시켜놓고 구글애널리틱스 홈페이지 들어가서 전체 모바일 데이터 (All Mobile Data)를 눌러서 실시간 왼쪽탭의 메뉴를 눌러보면 1명이 활성화 된것을 확인 할 수있다. 오오 신기하다.

왜 구글 애널리틱스인가?
일단 수많은 이유가 있겠지만
첫째 공짜이고 
둘째 공짜이고 
셋째 공짜이고 
넷째 연동하기 너무 쉽고 
다섯째 통계를 다양한 관점에서 잘 분석해 놓았다.(앱이 강제 종료 되는것도 알아서 통계가 나옴)

그리고 최종적인 목적은 내 서비스를 다음에 어떤식으로 개발해야할지 업데이트 해야할지 방향이 서기 때문이다.




아무튼 글을 마치겠습니다. 오류 지적이나 여러가지 의견은 댓글로 달아주시면 감사하겠습니다.


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

,

준비할것

카카오톡 API 홈페이지 들어가서 준비를 한다.
- 나머지 프레임 워크도 추가한다.

#import <MessageUI/MessageUI.h>
#import <KakaoOpenSDK/KakaoOpenSDK.h>
#import <Social/Social.h>
#import <Accounts/Accounts.h>


MFMessageComposeViewControllerDelegate 델리게이트를 등록한다. 메시지 전송 팝업을 컨트롤 할때 사용한다.

@interface ViewController () <MFMessageComposeViewControllerDelegate> {

}
@end


아래 코드는 실제 동작하는 코드

- (void) shareWithIndex:(NSInteger)buttonIndex text:(NSString *)text image:(UIImage *)image imageURLString:(NSString *)imageurl/*카톡 이미지 공유에서 쓰임*/ url:(NSURL *)url { if (buttonIndex==0) { // 문자 메세지 [self shareMessageWithText:text image:image url:url]; } else if (buttonIndex==1) { // 카카오톡 if ([KOAppCall canOpenKakaoTalkAppLink]) { // 카카오톡 공유 [self kakaoWithText:text image:image imageURLString:imageurl url:url]; } else { // 카카오톡 설치 [self openInstallKakaoAlert]; } } else if (buttonIndex==2) { // 페이스북 [self shareWithServiceType:SLServiceTypeFacebook Text:text image:image url:url]; } else if (buttonIndex==3) { // 트위터 [self shareWithServiceType:SLServiceTypeTwitter Text:text image:image url:url]; } } #pragma mark - 메시지 - (void) shareMessageWithText:(NSString *)text image:(UIImage *)image url:(NSURL *)url { if(![MFMessageComposeViewController canSendText]) { UIAlertView *warningAlert = [[UIAlertView alloc] initWithTitle:@"메시지 보내기 기능을 지원하지 않습니다." message:@" " delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [warningAlert show]; return; } else { MFMessageComposeViewController *controller = [[MFMessageComposeViewController alloc] init]; if([MFMessageComposeViewController canSendText]) { controller.body = [NSString stringWithFormat:@"%@\n\n%@", text, url]; // controller.recipients = recipients; controller.messageComposeDelegate = self; NSData *data = UIImageJPEGRepresentation(image, 0); [controller addAttachmentData:data typeIdentifier:@"image/jpg" filename:@"thumbnail"]; [self presentViewController:controller animated:YES completion:nil]; } } } #pragma mark - 메시지 전송 delegate - (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { [controller dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - 카카오톡 - (void) kakaoWithText:(NSString *)text image:(UIImage *)image imageURLString:(NSString *)imageurl url:(NSURL *)url { // 카카오톡 KakaoTalkLinkAction *androidAppAction = [KakaoTalkLinkAction createAppAction:KakaoTalkLinkActionOSPlatformAndroid devicetype:KakaoTalkLinkActionDeviceTypePhone marketparam:nil execparam:@{@"kakaoFromData":[NSString stringWithFormat:@"{seq:\"%@\", type:\"%@\"}", self.dataInfo[@"contentsSeq"], self.dataInfo[@"contentsType"]]}]; KakaoTalkLinkAction *iphoneAppAction = [KakaoTalkLinkAction createAppAction:KakaoTalkLinkActionOSPlatformIOS devicetype:KakaoTalkLinkActionDeviceTypePhone marketparam:nil execparam:@{@"kakaoFromData":[NSString stringWithFormat:@"{seq:\"%@\", type:\"%@\"}", self.dataInfo[@"contentsSeq"], self.dataInfo[@"contentsType"]]}]; NSString *buttonTitle = @"앱으로 이동"; NSMutableArray *linkArray = [NSMutableArray array]; KakaoTalkLinkObject *button = [KakaoTalkLinkObject createAppButton:buttonTitle actions:@[androidAppAction, iphoneAppAction]]; [linkArray addObject:button]; /*[NSString stringWithFormat:@"%@ (%@)",[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"], LOC(@"msg_invite_kakao", @"경영전문대학원 MBA 모바일 주소록 앱")]*/ if (text) { KakaoTalkLinkObject *label; label = [KakaoTalkLinkObject createLabel:text]; [linkArray addObject:text]; } if (imageurl && image) { KakaoTalkLinkObject *kimage = [KakaoTalkLinkObject createImage:imageurl/*self.dataInfo[@"thumbnail1"]*/ width:image.size.width height:image.size.height]; [linkArray addObject:kimage]; } [KOAppCall openKakaoTalkAppLink:linkArray]; } - (void) openInstallKakaoAlert { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"카카오톡이 설치되어 있지 않습니다." message:@"카카오톡을 설치하겠습니까?"// @"Do you want to install the KakaoTalk?" delegate:self cancelButtonTitle:@"취소" otherButtonTitles:@"확인", nil]; alert.tag = 141; [alert show]; } #pragma mark - Alert View Delegate - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (alertView.tag==141) { if (buttonIndex==[alertView cancelButtonIndex]) { // cancel } else { // 카카오톡 링크로 이동 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://itunes.apple.com/kr/app/id362057947"]]; } } } #pragma mark - 페이스북 트위터 - (void) shareWithServiceType:(NSString *)serviceType Text:(NSString *)text image:(UIImage *)image url:(NSURL *)url { if ([SLComposeViewController isAvailableForServiceType:serviceType]) { SLComposeViewController *mySLComposerSheet = [SLComposeViewController composeViewControllerForServiceType:serviceType]; if (text) [mySLComposerSheet setInitialText:text]; if (image) [mySLComposerSheet addImage:image]; if (url) [mySLComposerSheet addURL:url]; [mySLComposerSheet setCompletionHandler:^(SLComposeViewControllerResult result) { switch (result) { case SLComposeViewControllerResultCancelled: NSLog(@"Post Canceled"); break; case SLComposeViewControllerResultDone: NSLog(@"Post Sucessful"); break; default: break; } }]; [self presentViewController:mySLComposerSheet animated:YES completion:nil]; } else { [[[UIAlertView alloc] initWithTitle:@"실패" message:@" " delegate:nil cancelButtonTitle:@"확인" otherButtonTitles:nil] show]; } }


페이스북, 트윗 등등 다른 기본 소셜 공유기능을 사용하려면

SOCIAL_EXTERN NSString *const SLServiceTypeTwitter NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeFacebook NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeSinaWeibo NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeTencentWeibo NS_AVAILABLE(10_9, 7_0);
SOCIAL_EXTERN NSString *const SLServiceTypeLinkedIn NS_AVAILABLE(10_9, NA);

상수 스트링을 사용하면 된다. 이 포스팅에서는 SLServiceTypeFacebook, SLServiceTypeTwitter 만 사용했다.


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

,

키보드 올라오는 Delegate받기

- (void)viewDidAppear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(keyboardOnScreen:) name:UIKeyboardWillShowNotification object:nil];
    [center addObserver:self selector:@selector(keyboardHideScreen:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewDidDisappear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [center removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    //...
}
-(void)keyboardHideScreen:(NSNotification *)notification {
    //...
}



- (void) viewDidAppear:(BOOL)animated ; 는 화면이 나타나기 바로직전에 호출되는 함수이고

- (void) viewDidDisappear:(BOOL)animated ; 는 화면이 없어지기 바로 직전에 호출되는 함수이다.

viewDidAppear에 "키보드 올라오면 말해주세요" 용도의 노티피케이션을 설정하고

viewDidDisappear에 "아까 등록한 노티피케이션을 지워주세요"라고 설정한다.




#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    NSDictionary *info  = notification.userInfo;
    NSValue      *value = info[UIKeyboardFrameEndUserInfoKey];
    
    CGRect rawFrame      = [value CGRectValue];
    CGRect keyboardFrame = [self.view convertRect:rawFrame fromView:nil]; /*키보드 프레임*/

    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];  /*키보드가 올라오는 동안의 시간*/
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];       /*키보드 애니메이션 옵션 효과 (ease)*/
    UIViewAnimationCurve animationCurve = curveValue.intValue;
    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}

-(void)keyboardHideScreen:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];
    UIViewAnimationCurve animationCurve = curveValue.intValue;


    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}


// ... 부분에 애니메이션 코드를 넣어주면 키보드에서와 동일한 애니메이션 ease가 연출된다.



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

,