'분류 전체보기'에 해당하는 글 132건


Swift로 깨끗한 코드를 짜려 할 때, Swift API Design Guidelines은 매우 중요한 것 중 하나이다. 이것을 본 적이 있는가? 사실 나는 애플의 WWCD 비디오를 보기 전까지 이것을 본 적이 없었다. WWDC 영상을 기다리면서 내 직장 동료와 이것저것 이야기 했는데, 영상이 눈에 잘 안들어올 여지가 있었다. 내가 직접 작업하는 것 말고는 다른사람이 사용하기 위해 "프레임워크" 형태의 코드를 작성하진 않는 사람 중 한명으로서, 나에게 이 영상의 설명과 직접 연관되있지 않을것이라 생각했다.

Swift3은 명확하고 정교한 Swift 코드를 위해 고유의 특징을 만들어 담은 새 API 설계 가이드라인을 소개한다. 이 이야기는 Swift API 가이드라인 너머에 있는 철학을 탐구하고 Swift 표준 라이브러리, Cocoa, Cocoa Touch API를 통한 그 응용프로그림을 탐험할 것이다. 어떻게 이 API 변형이 여러분 Swift 코드에 영향을 줄 수 있는지 확인해보고, 어떻게 Swift3으로 부드럽게 변형할 수 있는지 배워보자. Swift3이 어떻게 Objective-C API를 불러오는지, 어떻게 기존의 Objective-C 라이브러리를 Swift 인터페이스로 만드는지 배워보자.

비디오를 보고 나서 Swift.org에서 실제 설계 가이드라인을 읽어 보았는데, 의미있었고 여러분도 한번 읽어보기를 추천한다.

Swift API 설계 가이드라인은 여러분의 코드에서 변수, 파라미터, 메소드의 네이밍의 "단어"를 어떻게 만들어낼 것인지에대한 설명의 메뉴얼이다. 이것은 여러분의 코드에서 단어들을 어떻게 잘 조합할 수 있는지 연관된 문서를 제공하면서 이야기한다. Swift API 설계 가이드라인과 친해질수록 커뮤니티의 코드와 일관성을 가지게 될 것이며, 애플이나 Swift팀의 프레임워크에서 만들어진 API이다(원문: the APIs that are being created within the frameworks right out of Apple and the Swift team). 이것은 윈/윈의 방법이다.

아래는 내가 뽑은 Swift API 설계 가이드라인의 중요 포인트이다.

"ed/ing" 규칙
WWDC 영상의 발표자는 이 규칙은 "ed/ing" 규칙이라고 말했다. Swift.org 문서에서는 strive-for-fluent-usage">String for Fluent Usage 섹션에서 그 사이드 이펙트에의한 함수와 메소드 이름으로서 소개되어있었다. 가이드라인에서는 "가변의 메소드가 종종 유사한 의미에서 불변 변형을 가지는데 이때는 그 자리에서 인스턴스를 갱신하는 것 보다는 새로운 값을 반환하는게 낫다"고 한다. ed/ing 규칙에 따라 불변 버전 뒤에 붙인인다. 아래 예시이다.

가변(Mutating)
x.sort()
x.append(y)
불변(Nonmutating)
z = x.sorted() 
z = x.appending(y)

필요없는 단어는 생략하기
Objective-C로부터 남아있는 나쁜 습관이 하나 있다면, 장황한 메소드 이름인데, 특히 반복적으로 사용되는 단어들이다. 예를들어 Objective-C의 NSMutableArray 클래스 문서를 열어 "Object"가 몇번 나오는지 한번 세어보자. 이런 메소드는 Objective-C Foundation API 전반에 걸쳐 나타난다.
- (void)addObject:(ObjectType)anObject; 
- (void)removeObject:(ObjectType)anObject;
NSMutableArray의 클래스 문서에 "Object"라는 단어가 몇번이나 들어갔는지 세어보아라. 결과적으로 내가 쓴 코드에도 그런식으로 하게 만든다.

반면 Swift에는 이런것이 없다. Swift 창시자들은 간결함과 명확성의 조화를 위해 힘쓰고 있는 중이다.

Swift 코드가 너무 생략되버릴 수 있는데, 작은 단어 갯수로 코드를 최소화시키는 것이 그 목표는 아니다. 너무 짧은 Swift 코드가 있는 곳에는 강타입 시스템의 부작용이다...

이 섹션의 마지막 부분과 연관이 있다. 강타입 시스템으로서 NSMutableArray API를 아래처럼 다시 설계해볼 수 있다.


func add(_ anObject: AnyObject) 
func remove(_ anObject: AnyObject)

메소드 이름에서 얼마나 "Object"라는 단어가 없어졌는지 보라. 이것은 가이드라인에서 말하는 "불필요한 단어 생략"의 결과물이다. 이렇게 하는 이유는 이 API 사용자들이 파라미터에 정의된 타입으로만 오브젝트를 메소드로 보내기 위해 Swift의 강타입 시스템에 의존할 수 있기 때문이다. 이것은 Swift API 디자인 가이드라인 안에서 얘기하는, 불필요한 단어 생략을 통해 더 명료한 코드를 만드는 하나의 예시일 뿐이다. 이제 이것을 알았다면 가이드라인을 읽으면서 그것들을 여러분의 API 설계에까지 적용시켜볼 수 있다.

또한 Swift API 설계 가이드라인에서 변수, 파라미터, 연관타입, 네이밍에 해당하는 규칙들과 약타입 정보에 대한 보장 부분을 확인해보아라. 여기에 자세한 예제가 있지는 않지만, 이 가이드라인에서는 어떻게 필요없는 단어를 생략하여 명료함을 달성할 수 있는지에대한 좋은 안내서를 제공한다. 단 명료함을 넘어 축약이 되지 않도록 조심하자. WWDC 발표자가 언급한 것처럼 API는 꽤 간결하기 때문에 API 문서가 자꾸 바뀌길 원하지는 않을 것이다.

문법적인 영어 구(Grammatical English Phrases)
메소드와 함수의 이름은 문법적인 영어의 구처럼 읽을 수 있어야 한다. 나는 이 부분을 굉장히 좋아하는데, 자바 프로그래머로서 암흑기로부터 나를 끄집어내준 기분이 든다. 이것은 Swift API 설계 가이드라인의 strive-for-fluent-usage">String for Fluent Usage 섹션에서 이야기하고있다. 메소드와 파라미터 이름이 옳바른지 확인해보려면 코드를 소리내서 읽어보면 된다. "이것이 영문법적으로 옳바른가? 회화식으로 말할 수 있는가?" 이 질문에 대답이 그렇다면 아마 좋은 API 일것이다. 그렇지 않으면 다시 한번 생각해보아라. 여기 문법적으로 이해하기 쉬운 Swift.org에서 말하는 좋은 API 설계의 예시이다.
x.insert(y, at: z) // “x, insert y at z” 
x.subViews(havingColor: y) // “x's subviews having color y” 
x.capitalizingNouns() // “x, capitalizing nouns”
나쁜 설계는 소리내서 읽기 힘들다.
x.insert(y, position: z) 
x.subViews(color: y)
x.nounCapitalize()

다른 멋진 것들
문서
내가 전문적으로 사용해본 프로그래밍 언어 중에 가이드라인이 이렇게나 잘 정의된 코드 문서는 본적이 없다. Swift API  설계 가이드라인의 fundamentals">Fundamentals 섹션 상단에 있다. Swift 코드를 위한 문서를 작성할 때 고려해야하는 짧고 친절한 요점을 제공한다. 또한 만들어 놓은 문서를 Xcode가 어떻게 보여줄지와 함께 어떻게 그것과 일관된 주석을 달 수 있는지도 알려준다.

컨벤션(약속)
컨벤션 섹션은 매우 마음에 든다. 빅오(Big-Oh) 문서를 만들때나, 변수, 메소드 이름을 정하는 것들로부터 모든 것을 분명하게 설명해주는 1회성 코드들이 여기에 있다.

요약
나는 여러분에게 Swift.org에있는 Swift API 설계 가이드라인을 확인하라고 강요할 순 없다. 그래도 오늘 한 것이 여러분이 Swift 코드를 더 좋게 만드는데에 직접 행동으로 옮겨볼 수 있는 방법중 하나라는 생각이 든다. 가이드라인은 간단하고 깔끔하며 이해하기 쉽도록 배려해 놓았다. 여러분의 코드에 일관성이 생기는 것 뿐만 아니라 커뮤니티 어디에서나 쓰일 수 있는 일관성도 갖추게 될것이다. 이것을 누가 바라지 않겠는가?

즐거운 클리닝하기 바란다. 




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

,





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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

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

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

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

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

...그리고 구현이다.

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

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

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

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

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

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

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



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

,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

다시 말하면, 우리는 개별의 CocoaPod에대해 이야기하는게 아니라, 여러분의 코드가 실행될 수 있는 유일한 플랫폼에대해 이야기하고 있는 중이다. 굉장히 장기적인 시각으로 다뤄야할 중요한 문제이다.

명백히 위압하는(Patently daunting)
리액트 네이티브는 복제를 허용하는 BSD-스타일 라이센스와함께 페이스북의 추가적인 특허권 문서(Additional Grant of Potent Right)를 만들었다.(버전2) 이 파일을 넣은 페이스북의 입장은 분명하지 않고, 이 파일 자체도 불분명하다.

양극의 문서이다. 문서가 끝나자마자 리액트 네이티브를 사용하기 위해서는 "perpetual(영속의), worldwide(널리), royalty-free(무료저작권), non-exclusive(독점불가능한), irrevocable(변경불가능한)" 라이센스를 따라야한다고 한다. 아래 조항들을 제공받으면서 말이다.

The license granted hereunder will terminate, automatically and without notice, if you (or any of your subsidiaries, corporate affiliates or agents) initiate directly or indirectly, or take a direct financial interest in, any Patent Assertion: (i) against Facebook or any of its subsidiaries or corporate affiliates, (ii) against any party if such Patent Assertion arises in whole or in part from any software, technology, product or service of Facebook or any of its subsidiaries or corporate affiliates, or (iii) against any party relating to the Software.

나는 변호사에게 이것을 명확하게 해달라고 직접 물어보았다(여러분이라도 그렇게 할 수 있었을 것이다). 그 변호사 말에 따르면 이 조항은 다음으로 요약된다. 내가 만약 페이스북에게 특허권 위배를 주장하며 소송을 걸면 리액트 네이티브를 사용하는 라이센스가 즉시 사라질 수 있다.(아래 Appendix에서 그의 더 세부적인 설명을 인용했다)

실제로는 특허권으로 페이스북으로 고소하지 못하게 하는 것처럼 보였다. 이런 의미에서, 이것은 페이스북이 내 특허권을 침해할 수 있는 기회를 주는 것처럼 보였다.("당신이 여기에있는 훌륭한 양자 물리학 기술은 당신의 앱에 나쁜 일이 생길 경우 부끄러운 일이 될 것입니다.")

위 설명이 비판적이라고 생각할지도 모르겠다. 그러나 소프트웨어 라이센스와 특허권을 평가할 때는 비판적으로 해야한다. 알고리즘이 최악의 시나리오에서 얼마나 잘 동작하는지 그 효율을 평가한다. 똑같은 이유로서 소프트웨어 라이센스와 특허권도 그렇게 평가해야한다고 생각한다.

리니어 서치(linear search) 알고리즘은 타겟값이 리스트의 첫번째로 일어나는 시나리오에서는 최적화된 동작을 할 것이다. 그 이유 하나로, 모든 시나리오에 리니어 서치 알고리즘을 사용하라고 나를 설득하기엔 만족스러운 이유가 아니다. 만약 타겟값이 리스트 마지막에 있을 수 있다면, 내 리니어 서치 알고리즘의 평균 퍼포먼스는 굉장히 나빠질 것이다.

비슷한 맥락에서, 페이스 북이 내일 일련의 IP를 침해하는 것이 가능하고 React Native를 사용하기위한 라이센스를 취소함으로써 내측으로부터의 보복을 처벌 할 수 있다면, 페이스북이 오늘 일련의 IP 침해를 하지 않았다는 사실은 나를 조금 안심시킨다.(원문: Likewise, the fact that Facebook happens not to be a serial IP infringer today reassures me little if it is possible for Facebook to become a serial IP infringer tomorrow and punish any retaliation from my side by revoking my license to use React Native.) 만약 내일 페이스북이 내 IP를 침해하고(그러나 그것이 내 소프트웨어와 관련은 없다) 그들에게 소송을 걸면,  페이스북에게 내 앱에서 리액트 네이티브를 빼내게 만들 기회를 주는 셈이 되버린다.

내 앱이 그 플랫폼에 의존적인 것 뿐만 아니라 영리한 내 프로퍼티들까지도 위험하게된다.

iOS 앱들은 완전히 애플의 재량으로 앱스토어에 들어간다. 나는 쉽지 않은 느낌을 두배로 느끼고 싶지 않다.

명백히 침묵하는(Patently silent)
위의 해석이 정확한가? 최악의 시나리오 가능성인가? 이런것들을 걱정할 만큼 좋은 이유가 있는가? 걱정하지 않아도 될 좋은 이유가 있는가?

수많은 깃헙 이슈와 포럼 포스팅에서, 그들의 법률 부서가 이런 조항 때문에 리액트나 리액트 네이티브를 쓰지 마라고 조언한다고 이야기해왔다. 볍률 부서가 없는 개발자들에게는 유감스럽게도 이 문제들이 모호하게 남아있다.

좀 더 걱정스럽게도, 페이스북은 이 문제를 더 명확하게 하기 위한 근본적인 노력을 해오지 않고 있다. 2015년 페이스북 오픈소스 블로그 포스팅에서는 이러한 "혼란"에대해 인정했고, 기꺼히 추가적인 특허권 문제에대해 명확하게 할것이라 알렸었다. 깃헙에서 그것과 연관된 5개의 이슈 이후에도 그 불분명함은 사라지지 않고있따.

여러 페이스북 개발자들이 이 이슈에대해 응답해오고 있는데, 그들이 적어놓은 것에는 안심될만한게 하나도 없으면서 그것을 명확하게 하는 동안에 개발자들을 안심시키려고 노력하고 있다. 그들 중 한명은 Reddit과 HackerNews에있는 이 이슈에대한 토론 링크를 주기도 했었다(도움이 되지 않을 뿐더러 오해시키기까지 했다).

이것은 해석학과 추측의 작업으로 바뀌어버렸다. 페이스북은 리액트 네이티브 라이센스를 취소 할 수 있을까? 그렇다면 어떤 조건에서?

자바스크립트
Swift에서 리액트 네이티브로 전환할때 생기는 심각한 부정적인 면은 기술적 역행이다. 여러분은 자바스크립트에 적응하고 사용해야하는데 이 언어는 다음과 같은 특징을 담고있다.
  • 기술적으로 불완전함
  • 언세이프
  • 늦은 발전
왜 그렇게 말하는지 보자.

여기 이후에 예제에 나오는 모든 자바스크립트 코드는 ES2016에 유효하다.

자바스크립트의 결점
내가 좋아하는 법퍼 스티커에서 하는 말이다.
안전은 사고가 없는 것이다(Safety is no accident)
처음 봤을땐, 이 말은 중의적이었다. 우리는 안전의 정의를 사고가 나지 않는 것과 여러 안전장치 제품의 제안으로 알고 있다.

자동차에 안전벨트나 에어백이 탑재된 여러 안전장치의 제품은 안전하다고 할 수 있는가?

물론 대답은 같은 조건에서 둘 다이다.

운전자는 수많은 안정장치가 탑재된 차를 더 선호할 수 있다. 운전하기 어렵게 만들지라도 막을 수 있는 사고는 최대한 줄일 것이다.

비슷한 의미에서, 프로그래밍 언어는 프로그래머 에러에 대비한 안전장치를 제공할 수 있다.

수많은 운전자가 생산성을 위해 안전벨트를 착용하지 않고 운전한다는 사실은 옳바른 주장이 아니다. 비슷하게, 수많은 자바스크립트 개발자들이 생산성을 위해 태생의 언세이프 언어를 쓴다는 것도 옳바른 주장이 아니다.

프로그래밍 언어에서 안전의 중요성은 iOS 개발 툴이 진화하고 있는것의 진가를 인정하는 것이기도 하다.

오토 레퍼런스 카운팅이 Objective-C에 처음 나타났을때, 여러분의 iOS 프로젝트에서 그 옵션을 끄고 작업했을 수도 있다. 왜 ARC를 끄는게 나쁜 생각이었을까? 그 이유는, 이제 컴파일러가 당신의 오브젝트 라이프타임 계산을 할 수 있게 되었는데, 당신이 계산하는 것보다 더 빠르게 해준다. "컴파일러가 당신보다 더 똑똑해졌다"는 진언이고, 레퍼런스 카운팅에 관해서는 확실해졌다. 결과적으로 EXC_BAD_ACCESS 런타임 크레쉬를 줄였다는것을 알게되었을때 얼마나 만족했었는지 기억한다.

Obejctive-C는 한 변수 타입을 id로 설정할 수 있는데, 이것은 "어떤 것이든 모든 타입"의 의미이다. 그러나 컴파일러가 막을 수 있는 크레쉬임에도, 이러한 습관때문에 런타임 크레쉬가 일어날 수 있다. 컴파일러가 해결할 수 있는 문제라면, 컴파일러가 해결하게 놔두고 당신은 다른 문제를 해결하러 가면 된다.

여러분은 unrecognized selector sent to instance 크레쉬를 기억할 것이다. 응답하지 않는 오브젝트에 메소드를 호출할 때 생기는 크레쉬이다. 타입에러. 내 버그에서 3번째이다.

당연하게도 Swift를 사용하고나서 나의 첫 반응은 "Objective-C에서 런타임 크레쉬를 막고 싶었던 누군가가 만들었구만"이었다.

Swift는 안전하다. String을 받길 예상한 함수에 Int값을 넣도록 컴파일러가 허용하지 않는다. 사실 컴파일러가 타입 추론을 할 수 없었다면, 명시적으로 그렇게 할 수 있었을 것이다. 

그러나 자바스크립트는 프로그래머 에러에 대비한 안전장치가 부족하며, 여러분의 루틴에서 런타임 크레쉬를 막아야하고, 프로그래머 에러를 막아야한다.

타입 에러
자바스크립트는 변수나 함수에서 파라미터의 타입을 정해주지 않는다.

어떤 변수라도 언제든 무엇이든 될 수 있다.

자바스크립트는 class, typeof, instanceof와같은 키워드로 타입과 클래스 개념이 있다고 믿게 만든다.

여기서 우리는 다음을 보자.
  • 자바스크립트에서의 "class", "type", "instance" 개념은 주요 프로그래밍 세계에서의 개념과 완전히 다르다.
  • 자바스크립트에서는 타입을 너무 신뢰할 수 없게 정의해서, 이것들이 유용한 용도를 제공하지 못한다.

이  unrecognized selector sent to instance 크레쉬가 기억나는가? 이것이 이제 여러분 곁을 떠날것이라 생각하는가? 아래에는 리액트 네이티브의 것들이 있다.


옵셔널 결핍
Objective-C 코드(혹은 다른 수많은 오래된 언어)에서 엄청나게 많은 양의 버그들은 프로그래머가 부주의하게 nil에다가 메소드를 호출해서 생긴다.

리액트 네이티브와 자바스크립트 세계에서는 아래 에러를 종종 볼 수 있다.

그리고 이론적으로나 실무적으로나 막을 수 있다.

Swift는 옵셔널을 만들어냄으로서 이 문제를 해결했는데(옵셔널이란 nil일 수도 있고 값이 들어있을 수도 있는 타입이다), 이것을 사용할 때는 여러분에게 강제로 nil 체크를 거쳐서 사용하게 만든다.

함수 기호(function signature)의 결핍
자바스크립트에서는 함수가 리턴타입을 가지고 있지 않으며, 이 함수가 어떤 타입을 반환할지 모르거나 함수가 어떤것이든 반환할 수 있다.

좀 더 재미있게 만들어보자. 자바스크립트에서는 어떤 식이든 어떤 함수에의해 어떤 시간에든 계산될 수 있다. 자바스크립트 표준 라이브러리에 있는 map과 parseInt 함수를 사용한 예제를 생각해보자. map은 아마 Swift의 map과 동일한 것이고, parseInt는 문자열을 숫자로 파싱해주는 함수이다.
parseInt 함수는 2개의 파라미터(val, radix)를 받는데 반해, map은 3개의 파라미터(currentValue, index, array)를 보내기 때문에 이런 엉망의 결과가 생긴다. 이런것들이 여전히 합법적인 자바스크립트이다(일부 사람들이 함수형 프로그래밍 언어에 유용할것이라고 생각하는 언어).

불변성
자바스크립트가 지원하는 불변성은 매우 안 좋다.

const 연산자가 있는데, 이것은 기본타입(primitive)이 바뀌지 않는 것을 보장하는데 도움을 준다. 그러나 기본타입이 아닌 나머지 모든 것은 젤리처럼 유연하다.

유일한 복사? 아니다. 어떤 유일한 객체라도 언제든지 여러분의 앱에의해 수정될 수 있다. 멀티스레딩에 행운을 빈다. 정말로.

앱 개발을 힘들게하는 많은 것들은 가변을 쫓고 상태를 유지하는 것이다.

페이스북의 Immutable.js 문서에서 말한 내용이다(자바스크립트에서 불변 데이터 구조를 만들기위해 설계된 프레임워크이다).

그러나 아래는 리액트에서 어떻게 불변성을 만드는지이다.

이 컴포넌트에 넣는 인풋을 props라 부르는데, "properties"의 준말이다. 그들은 JSX 문법으로 속성들을 보낸다. 여러분은 이것을 컴포넌트에 불변할 것이라 생각할 수 있는데, 이것은 절때 this.props에 덮어 씌울 수 없다.

여러분은 상태를 바꾸지 말자고 공손히 물어본다. 그렇다 위 글은 리액트 네이티브 문서에서 가져온 것이다.

배열을 믿을 수 없다.
여러분은 배열이 "행과 열로 된, 비슷한 객체들의 질서정연한 배치"라 생각하는가? 아래를 보고 다시한번 생각해보아라.
자바스크립트에서 배열은 우리가 원래 배열이라 부르는 그것보다는, 보통의 자바스크립트 객체에 더 가깝다. 정확한 수서성의 결여와 가변성은 잘 동작하기 힘들게 만든다.

에러 핸들링하기 힘들다
자바스크립트에서는 경고 없이 런타임이나 예외를 던지는 함수를 만들 수 있다.
예외로 당신이 원하는 무엇(문자열, Date, 함수 등)이든 던질 수 있다. 팀원의 코드를 잠제적으로 크레쉬할 수 있다는 표시를 함수에 한다던가, 예외의 과정이 어떻게 되는지 명시할 메커니즘이 따로 없다. 문서에서는 대신에 if문을 사용하라고 권장한다.

당신의 예상하지 못한 예외 처리를 다루기 위해서는 마지막 라인의 방어로 예외를 남겨놓는게 최고이다. 그리고 예상되는 에러를 컨트롤 플로우 문으로 관리하기에도 최고이다.

디시멀(Decimal)을 지원하지 않는다.
하드웨어에서 대부분의 소수 자릿수들은 이진수로 정확하게 표현되지 않으며, 많은 프로그래밍 언어(자바스크립트와 Swift를 포함한)는 종종 수학적으로 잘못된 소수 계산을 내놓을 것이다.
이것이 왜 다른 언어의 표준 라이브러리가 소수 자릿수를 지원하는지 이유이다(예를들어 Swift에서는 Decimal을 사용할 수 있다). 자바스크립트에서는 서드파티 라이브러리나 여러분이 직접 만든 코드에 의지해야한다.

믿지 못하는 수학
초등학교 산수에도 주의를 기울여야함을 기억해라. 자바스크립트는 0과의 관계가 복잡하며 이것은 숫자가 아니다.

언세이프한 초기화
자바스크립트는 속성을 초기화 할 필요가 없으므로 객체를 만든 후 모순된 상태로 둘 수 있다.

if 다음에 선택적으로 가능한 커리 중괄호(curly brace)
if문 뒤에 커리 중괄호는 선택적이다.
여러분의 컨트롤 플로우 문에 모험의 취향을 추가한다.

모호한 커리 중괄호
프로그래머의 의도가 정확하게 추론된 것이 아니라면, 이 언어는 커리 중괄호를 선택적으로 할 수 있게 놔두지 않는다.

fallthrough 전환
switch  절에서 break를 깜빡했다면, 아래로 쭉쭉 떨어질 것이다. 또한 Swift에서는 케이스 철저성을 확인하지 않아도 된다.

어떤것이 '무(없음)'일까? (What's nothing)
 모든 의도나 목적에서 null이 아닌 것이 무가 될 수 있다. 도움이 되지 않는 구별이다.

그리고 변수에 어떤 데이터가 담겨있는지 아닌지 알고 싶다면 어떨까? 음, 확인이 너무 거추장스럽게 null과 undefined 둘 다 체크해야한다.


빈약한 표현력
  • 열거형이 없다. 연관타입을 가진 열거형은 말할것도 없다. 신뢰성 높은 상태 표현에 행운을 빌 수 있을까.
  • guard문이 없다.
  • 제네릭이 없다.
  • 컨트롤 플로우문의 표현력을 늘리기위한 where이 없다.

지극히 느린 진화
ES2016 때 자바스크립트에 새로 추가된 기능을 보라.
1. 배열을 위한 includes 메소드
배열이 특정 값을 가지고 있는지 확인한다. 아래에 어떻게 사용하는지 나와있다.
이건 사용하지 말자.
2. **연산자
지수 연산에 사용하기위한, a**b는 Math.pow(a, b)의 축약이다.

한번 생각해보면, 파이썬의 **연산자는 파이선1에 있었고, 루비의 **연산자는 20년도 전에 있었던 연산자이다.

따라서 자바스크립트는 이 기본적인 산수 연산자와 배열을 위한 꽤 제한된 맴버쉽 체크 메소드를 추가하는데 20년이나 걸렸고, 이 특징이 한 해중에 가장 의미있는 것이었다.

Flow로 구조받자!(Flow to the rescue!)
Flow는 위의 수많은 불평으로부터 내놓은 페이스북의 대답이다. 이것은 자바스크립트를 위한 정적 타입 체커이고, 여러분 코드에서 변수의 타입을 추론하고 추적할 수 있는 능력이 있으며, 여러분에게 일어날 운명(옮긴이: 실행되었을 때 일어날 오류)을 경고한다.

위에서 보았던 함수 기호의 결핍으로부터 나온 문제의 예제를 다시 보자.(number가 들어오길 예상한 divideByFour 함수는 문자열을 받는다) 여기엔 Flow가 이것을 어떻게 다루는지 보여준다.

함수 기호 부재 때문에 생기는 많은 문제를 고쳐준다.

제네릭 배열에도 동일하게 할 수 있다.

Flow는 nullability를 잘 다룰 수 있게해주고, 값이 null이되면 안되는데 null일 수 있을때 여러분에게 경고를 띄워 줄 것이다.

만약 j가 string으로 가정하지만, 언제든 null이 될 수 있으면, Flow는 그 사실을 우리에게 적절하게 알려주려 할 것이다. 따라서 인자로 string을 받되 null이면 안되는 곳에서 인자를 넘겨주기 전에 굳이 null 체크를 할 필요가 없다면 불만을 가질 수 있을 것이다.

Flow의 기능들은 타입체킹과 주석을 달아주는 것을 넘어, 새로운 구성체(new construct)를 지원한다. 그 중 하나는 리터럴 타입인데, 우리가 만드는데 사용할 수 있으며, 그 예로 열거형이 있다.

그리고 Flow는 direction이 "North"도 아니고 "South"도 아닌 것을 반환하려하면 뭔가 불평하고 있을 것이다.

내가 찾은 또다른 유용한 구성체는 유니온(Union) 타입이다. 이것은 딱 하나의 값으로 제한하고, 미리 정의해놓은 타입들 중에서 하나로 제한한다. 아래 예제는 Flow 문서에서 온 것이다.

Flow은 도움이되는한 꽤 도움이 되는 리액트 네이티브와 훌륭한 동반자이다. Flow 문서에서는 구성 요소의 유형에 올바르게 주석을 추가 할 때 어떤식으로 동작하는지 보여주는 좋은 예시를 제공한다.

저 주석은 Flow가 어디서 경고를 내는지 알려주며, 여러분 계약에 만족스럽지 않다고 충분히 꾸짖는다.

Flow는 전적으로 강력한 도구이다. 여기에는 여러분이 해볼 수 있는 유용한 커멘드라인 인터페이스 예제가 있다.
  • suggest는 주어진 파일에대해 타입 주석 제안을 보여준다(suggest Shows type annotation suggestions for given files)
  • type-at-pos는 주어진 파일과 위치에 타입을 보여준다(type-at-pos Shows the type at a given file and position)
  • get-def는 변수나 프로퍼티의 선언 위치를 받는다(get-def Gets the definition location of a variable or property)

Flow는 flossing와 비슷하다
이제 자바스크립트가 고쳐졌을까? 아니다.

Flow가 했을 공학의 노력에 감명받았고, 이것은 자바스크립트의 부모집합(superset)으로 남았으며, 따라서 당신을 태생적으로 약한 파운데이션에서 만들게 한다. 자바스크립트의 결점을 치료하는 어느 종류로서 Flow가 빗발치는 것은 (Monty Python and the Holy Grail으로부터) Swamp Castle and its King을 떠올리게 했다.

내가 여기 처음 왔을때, 여기는 전부 늪이였어. 여기 늪에 성을 세울거라는 말에 모두가 나를 얼간이라 했지만, 똑같이 만들어서 그냥 보여주었지. 그것이 늪으로 가라앉았고, 나는 두번째 것을 지었어. 그것도 늪으로 가라앉았고 세번째것을 또 지었어. 그것은 불에타고 쓰러져서 늪에 가라앉았어. 그러나 네번째는 버티고 있어. 그리고 이게 너가 얻어야 할 것이고, 영국에서 가장 강한 성을 얻어냈지.

안전하지 않은 파운데이션을 만들수 있을거라는 말이 파운데이션을 더 안전하게 만들진 않고, 더 효율적인 과정이지도 않을 것이다. 그리고 이 행동을 저지하려는 주장은 그 불합리함을 놓치게 만든다.

아마 더 의미있게, 자바스크립트 부모집합, 린터(linters), 정적 분석기는 당신이 더 안전한 언어를 고를 수 없는 플랫폼을 다룰때 완화시키는 방법으로서 일시적으로 억제해줄 것이다. 그게 된다면, 그것들을 사용하면서 도움을 구하지 않아도 될 것이다.

이런 일시적인 조치에는 또다른 근본적인 문제가 있다. 이것이 실제로 그 권위자가 쓰지도 않고 커뮤니티에서도 존경받지 못하면 법같은 이 안전장치는 아주 조금만 의미있게 된다.

Flow는 당신이 코드바운드로 리액트 네이티브 앱을 만들고 실행시키는 것부터 런타임 크레쉬를 생성하는 것까지 당신의 작업을 멈추게 하지는 않는다. 그리고 그것은 프로그래밍 언어에대한 기본적인 안전 요구사항이다. 만약 에러를 막을 수 있으면, 그 언어는 능동적으로 막으려고 할 수 있고, 안전하지 않은 코드를 짜고 실행시키는 것을 디폴트에의해 방해할 것이다(그 후에 하는게 아닌).

리턴값이 모호한 함수를 짜라고 팀에게 말할 수는 없을 것이고, 그것에 응답하지 않는 객체에 메소드를 호출하라고 나에게 말할 수도 없으며, 컴포넌트에 proptypes을 정의하지 않고 proptypes이 정의되있다는 코드 리뷰를 하는 동안 나에게 손수 끄집어내라고 할 수 없을 것이다.

유닛 테스트와 flossing과같은 Flow는 유익하고 선택적이고 지루한것의 저주를 낳는다. It’s in your next year’s resolutions.

그리고 여기에 현실이 있다. 깃헙에 공개된 .js 파일에 Flow를 사용하는 파일(즉, Flow가 체크할 수 있는 @flow가 포함되있는 파일)이 몇개나 될까? 어림잡아 80,000,000개의 .js파일에서 1,400,000개정도이다. 안전한 자바스크립트를 짜기 위해 2%보다 작은 곳에서 이 툴을 사용하고 있었다.

여기에 관련된 노트인, awesome-react-native에서 100개가 넘는 리액트 네이티브 저장소들이 있는데, 여기서 Flow를 제대로 사용하고 타입 주석을 사용한 저장소는 한개도 발견할 수 없었다. 오직 리액트 네이티브 튜토리얼에만 Flow를 사용하고 있었다.

자바스크립트 생태계: 속박과 굴레
자바스크립트 개발자를 제외한 나머지 모두는 자바스크립트의 결핍에대한 깊은 인상을 받은것 같다. 위에서 내가 설명한 자바스크립트는 흉측한 사마귀가 아니라는 사람들에게는, they’re “quirks” or “gotchas” that you, not your language, have to be on the lookout for.

왜냐하면 자바스크립트 개발자들은 자바스크립트가 불충분하다고 믿지 않기 때문이다.

언어에서 불변성을 지원하지 않는다? 그럼 만들면 된다. 언어에서 typing을 지원하지 않는다? 그럼 만들면 된다. 언어에서 decimal을 지원하지 않는다? 그럼 만들면 된다. 언어에서 안전한 함수형 프로그래밍 언어를 허용하지 않는다? 그럼 만들면 된다. 언어에서 nullability를 지원하지 않는다? 그럼 만들면 된다.

혹은... 당신이 이 기능들이 근본적으로 중요하다고 인정하면, 여러분은.. 바깥세상에서의 그것을 지원하는 언어로 전환해버리면 될까?(잘 모르겠다. 그냥 생각난것을 말한거다)

내 생각엔, 자바스크립트가 태생적으로 결핍하다고하여 그것을 계속 갈고 닦는게 아니라 다른것으로 대체하려 한다면 그것은 일반화된 고집의 거부(generalized obstinate refusal)이다. 갑자기 증가한 개선이나 버팀목의 결과는 살아있는 생태계의 신호로 보이나, 실제로 이것이 의미하는 것은 이 언어가 근본적으로 중요한 기능의 결핍이 있다는 것이다.

아래 그림은 이러한 상황을 만화로 잘 표현했다.

삽질으로부터의 자유는 당신이 필요한 것이 내장된 언어를 선택하는 것에 달렸다. 이러한 삽질은 에너지 낭비이다. 자바스크립트는 다음의 이유로 좋은 소프트웨어를 제작하는데 도움이 되지 않게 하고 있다: 자바스크립트는 우선 당신이 개발하게 만들고, 다른 언어가 제공하는 것에 의존적이게 만든다.

사슬(Chains)
자바스크립트는 튀긴 커다란 생선을 가지고 있다. 이 언어는 다양한 버전의 인터넷 브라우저의 몇십억 사용자 입맛에 맞춰주어야한다. 이것이 언어의 발전을 더디게 만든다.

typeof(null)==='object'를 기억하는가? 음, 예전에 type of null을 null로 바꾸자고 제안했었다. 그러나

이렇게하면 현존하는 수많은 사이트가 망가지게 될 것이다. 자바스크립트의 스피릿에 적합하지 않다.

그리고는 이 제안이 거절되었다. ES06에의해 null은 여전히 object이다.

자바스크립트의 발전 과정은 필요에의해 입맛에만 맞추고 있다.
  • 수많은 구식 버전의 브라우저 사용자들
  • 각색의 브라우저 벤더 집합
  • 수십억의 사이트와 그 사이트의 각 개발자들
이것들은 훌륭한 민주주의이긴하나 한편으로는 다른 언어에비해 굉장히 더디게 발전한다. 그리고 이것은 이식성이 좋은 만큼 개발자의 인간환경공학은 좋지 않을 것이다.

넓은 시각(Wider angles)
역사적인 관점에서, 한 언어가 인기를 얻고나서, 그것을 넘겨받고싶은 점유자들이 거부하게되는 패턴은 겉으로보기에 불충분한 언어에게 나타나는 익숙한 패턴이다.

여기 따뜻한 모닥불 옆에 앉아서 내 얘기를 좀 들어봐라.

1994년으로 돌아가보자. Richard Stallman은 "왜 Tcl을 쓰면 안되는가"라는 다소 모호한 타이틀로 comp.lang.tcl 뉴스그룹(news group)에 글을 하나 썼다. Stallman은 프로그래밍 언어로서 Tcl의 단점을 심술궂게 꾸짖었고, 목적에 맞지 않는 Tcl을 고발했다. 자바스크립트에대해 내 불평과 유사하게 매우 장관이었다.

Emacs의 주요 수업에선 확장을 위한 언어는 단지 "확장 언어"가 될 수 없다고 한다. 실제 프로그래밍언어가 되야하고, 상당한 양의 프로그램을 짤 수 있어야하며, 그것을 유지보수할 수 있게 설계되야한다. 왜냐하면 사람들이 그것을 바라니까! [...] Tcl은 진지한 프로그래밍 언어로 설계되지 않았다. 이것은 "스크립트 언어"가 되기위해 설계되었으며, "스크립트 언어"라 가정하는 것은 실제 프로그래밍 언어가 되려고 할 필요가 없었음을 의미한다. 따라서 Tcl은 하나의 능력을 빼먹었는데, 배열이 부족하다; 링크드리스트를 만들 수 있는 구조체를 빼먹었다. 그것은 가짜로 넘버를 가지고 있는데, 매우 느리지만 동작은 한다. Tcl은 작은 프로그램을 만드는데 적합하나, 그 넘어를 하기엔 적합하지 않을 것이다.

이 글은 1994년에 Tcl 전쟁을 촉발시킨 글이다. 그가 들었던 대답들중에 가장 인상적이었던 John Ousterhout(Tcl을 만든 사람)의 대답이다.

언어 설계자로서 이 언어가 왜 선천적으로 더 낫고 더 나쁜지 우열을 가리는 토론을 사랑한다. 그러나 이런 건방진 주장에는 많은 문제가 있다. 궁극적으로 모든 언어의 이슈는 그 사용자가 원하는 것을 투표할 때 합의가 생긴다. 만약 Tcl이 그들이 사용하는 것보다 더 생산적이게 만들었다면(혹은 이미 그렇게 되었다), 다른 언어 사용자들이 이것이 더 낫다고 하면서 온다면, 그때 사람들은 언어를 바꾼다. 이것이 법칙이고 이게 맞다.

Outsterhout의 반격은 내가 활동하고 있던 많은 사람들 앞에서 이루어졌고, 그의 일격은 더 가까이서 볼 수 있었다.

(몇몇 독자들은 Ousterhout가 순회적으로 말했다는 것을 알아차릴 것이다. "왜 사람들이 언어를 바꿀까? 그게 더 낫기 때문이다. 왜 그것이 더 나을까? 사람들이 많이 쓰기 때문이다." 그의 주장은 더 많은 설명이 필요함에도 불구하고 조금도 하지 않았는데, 우리는 종종 특정 기술 스택(예를들어 여러분이 생각하고 있는 리액트 네이티브)에 여러 주장에서 이런 추런 라인을 보았었기 때문이다)

Ousterhout에 따르면, 프로그래밍 언어는 그 언어가 더 낫다고 말할 수 있는 두가지 특징을 가지고 있는데, 바로 선천적인 특징들(프로그래밍 패러다임, 문법, 표준 라이브러리. 이식성과같은 그 고유의 특성)과 후천적인 특징들(수많은 개발자들이 폭넓게 채택한것)이다. 만약 언어A가 언어B보다 후천적으로 낫다면, 선천적으로 언어B가 언어A보다 더 낫다고해도 별로 소용없어질것이다.

다른말로 하자면, 채택되는 것이야말로 기술 우선순위에서 최고봉이다.(언어 설계자가 어떤것을 우위로 정의했는지 상관없이 말이다)

Ousterhout 주장에 딱 맞아보이는 앨범 커버이다.

더 많은 팬을 가진 가수가 그를 더 낫게 만든다. 요점은 팬이나 히트 기록의 양에의한 인기이며, 이것이 가수를 평가하는 좋은 기준이다.

그러나 언어가 널리 채택되고 그것으로 만들어진 멋진 앱들을 자랑할 수 있다는 사실로는 그 메리트로 당신의 판단에 설득시킬 수 없을지도 모른다.

복잡해보이거나 보기에 멋진 앱이 기술X로만 만들었다는 의미는 기술X가 복잡해보이거나 보기에 멋진 앱을 만들기에만 최적화되있고, 보통 일에는 최적화되있지 않다는 뜻일 수도 있다.

전세계에서 열리는 모래성 대회는 당신도 만드는데 쓸 수 있는 모래와 물로 아름다운 구조물을 만들어서 보여준다. 이것은 모래가 좋은 장난감임을 주장할 수 있는 근거가 될 수는 있으나, 모래성 대회를 근거로하여 모래로 여러분의 집을 지을 수 있다고 납득하기는 어려울 것이다. 그러나 많은 이들이 납득되어 버렸다.

The King of Swamp Castle은 수많은 해커의 왕이다.

비슷하게, 인기는 프로그래밍 언어를 평가하기엔 신뢰할 수 없는 기준이다. 아주 기본적인 이유로, 인기는 벤더를 속박시키거나(브라우저에서만 돌아가는 프로그래밍 언어가 된다던지), 벤드웨이건 효과나, 레거시(legacy) 코드베이스의 결과를 매우 잘 만들지도 모른다.

프로그래밍 언어의 고유 특징들(프로그래머 에러에 대비한 안전장치, 표준 라이브러리, 이식성 등)은 여러분의 기준에 기반해야한다. 이것은 생산성 향상의 기본요소이다.

사실은 많은 것들이 부정적인 생산성을 이끌기도 한다. 그 예이다.
  • 피할 수 있는 에러를 만드는 당신을 막는 안전장치의 부족
  • 많은 목적에 맞지 않는 작은 표준 라이브러리
  • 당신의 의도를 명확하게 표현하지 않는 문법과 의미론(syntax and sementics)
  • 더딘 발전
  • 위의 모든 것들(자바스크립트)

의존성
리액트 네이티브는 총 648개의 의존성을 가지고 있다.

특별히 놀랄 것 없게도, 의존성 체인은 세상의 npm 중에 길수 있으므로, 리액트 네이티브의 패키지 매니저이다.(원문: Not particularly surprising, as dependency chains can be long in the world of npm, React Native’s package manager.)

이 광경은 오픈소스의 동료관계이다. 여러분의 앱은 600명 이상의 사람들의 지속된 노력으로 만들어졌다.

이게 함정이기도한데, 여러분은 648명의 자원 봉사자가 그 라이브러리를 유지해주어야 앱이 잘 돌아가며, 그 자원봉사자들로부터 어떤 약속 따윈 없다.

그들의 라이센스가 당신의 소프트웨어에 맞게 유지될까? 희망적으로?

그리고 그 구현이 모두 보안면에서 최고의 실천을 하고 있을까? 혹은 648개의 별개의 잠재적인 보안 리스크를 묵인할 수 있겠는가?

더 나은 제안들
크로스-플랫폼 개발 때문에 리액트 네이티브를 선택하게 되었다면, 다른 옵션도 생각해볼 필요가 있다.

리액트 네이티브는 지금 Xamarin과 Appcelerator라는 두개의 크로스 플랫폼 개발과 경쟁하고 있다.

Xamarin과 Appcelerator 둘 다 iOS, 안드로이드, 윈도우 폰을 지원한다. 그리고 둘 다 아래의 것들도 지원한다.
  • 더 종합적인 API
  • 더 성숙한 IDE
  • 더 나은 문서
  • 명확하고 친숙한 라이센싱
  • 동등한 퍼포먼스(더 낮진 않더라도)

Xamarin 개발은 C#으로 하는데, 이것은 자바스크립트에 비해서 버그를 줄여주는 경향이 있고, 더 표현력이 좋다(more expressive). 만약 자바스크립트를 쓰는 개발자라면 Appcelerator 개발이 자바스크립트로 할 수 있다.

리액트 네이티브와 비교하여 Xamarin과 Appcelerator 둘 다 멀리 보았을때 더 나은 가능성을 가지고 있다. Appcelerator(Titanium의 아버지)(3억  5천만 기기에 앱으로 실행 되었었다)는 2016년 1월에 인수되었고 Xamarin(포춘지 선정 500 대 기업에서 100 기업 이상 사용했다)는 마이크로소프트에의해 2016년 2월에 인수되었다. 둘 다 이전보다 8천만달러 이상의 주식형펀드가 올랐다.

나에게 지원해주기 위한 비즈니스의 회사가 빽으로 있다면, 프로젝트들은 이 크로스 플랫폼이라는 레드오션에서 신뢰할 수 있는 소프트웨어 개발 플랫폼으로 살아남고 성장해야한다(최근에 세어보니 10개 이상의 활발한 개발 프로젝트들이 있었다).

(의미있는 언급으로 Flutter(이하 플루터)에 대해 이야기 하겠다. 이것은 구글이 만든 크로스-플랫폼 개발 플랫폼이다(iOS, 안드로이드 둘다 지원한다). Dart라는 언어를 사용하는데, 이 언어는 자바스크립트보다 안전하고 표현력도 더 좋다. 또한 메트리얼 디자인 원칙을 따르게 도와주고, 네이티브하게 컴파일 된 코드를 만들어준다. Xamarin과 Appcelerator와는 다르게 오픈소스이며, 아직 제품으로 준비되지는 않았지만 그럼에도 불구하고 약속해놓았다.)

결론
좋은 소프트웨어 개발 플랫폼은 4가지 필수 특징을 가진다.
  • 이식성 - 한가지 이상의 플랫폼을 타겟으로 한다.
  • 생산성 - 성숙한 IDE와 다른 개발 툴, 문서, 그리고 표현력 있는 그 개발 언어
  • 안전 - 당신이 만들 수 있는 실수를 그 플랫폼이 얼마나 막을 수 있는지의 정도
  • 지속성(longevity) - 당신의 앱이 살아있는 동안 플랫폼이 얼마나 오래 가는지
비록 우리 산업이 아직까지는 이것을 가늠하기위한 확립된 오픈 표준이 없지만, 나는 내 경험과 조사를 바탕으로 내 평가를 공유하고 싶다.


리액트 네이티브의 강점을 이식성과 생산정의 면에서 보자면 너무 과하고, 안전, 장기간 프로젝트에는 불확실성, 위압적인 특허 라이센스에는 부족함이 있다.


Xamarin과 Appcelerator와같은 성숙한 플랫폼은 최고의 이식성(윈도우 폰을 지원한다), 성숙된 개발 툴, 프로그래밍 언어를 선택할 수 있게 해주는 것을 제공하는데, 언어 선택으로 생산성과 안전성을 얻어낼 수도 있다. 그리고 이것들이 잘 투자받은 회사의 핵심 제품이라는 사실이 장기간의 면에서 안심시켜준다.


Swift 개발은 안전성, 지속성, 생산성 면에서는 좋다. 이식성의 면은 상대적으로 낮으나 무시해도 되는 수준이다. 안드로이드 버전을 개발 할 수 있게 해주는건 아니지만 macOS 버전이나 백엔드 서버 앱을 만들 수 있게 한다.





여기까지가 내가 왜 리액트 네이티브 개발자가 되지 않는지의 그 이유였다.

감사
이 글의 초안에 도움을 준 Rami Chowdhury와 Alkis Papadakis에게 감사를 표한다. 그리고 난해한 IP 텍스트들을 해독하는데 도움을 준 Greg McMullen에게도 감사하다.

바뀐 이력
이 글이 바뀐 이력은 여기에서 확인할 수 있다.

커멘트
이 글에 대한 토론 에 팔로우하여 참여하고 싶으면 Hacker News, Hacker News(Again)/r/programming로 와라.

참조
 



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

,

기억할것: Swift는 여전히 ABI-안정성이 지원되지 않음!
이 글을 읽기 전에 모두가 알아야 할 사실은 Swift가 여전히 ABI에 안정적이지 않다는 사실이다. 이 말은 이전 버전의 Swift 컴파일러로 만든 바이너리가 현재 버전과 호환이 되지 않는다는 의미이다. 좀 더 알아듣기 쉽게 설명하자면 여러분의 모든 파일과 의존성들은 같은 버전의 Swift로 컴파일해야한다는 의미이다: 다른 버전의 Swift로 작성된 코드들은 합칠 수 없다.

애플은 현재 최신버전의 Swift3.0을 깨부수면서 (약속한것은 아니지만) 이것을 해결하려고 노력중에 있으며 Swift4에서 ABI 안정성을 지원할 계획이다.

2.3으로 가야할까 3.0으로 가야할까?
만약 여러분이 우리 VTS처럼 중요한 Swift 코드베이스를 가지고 있다면 아래에 해답이 있다.

그렇다. 두가지 다이다.

여러분의 Swift 코드베이스가 지저분하지 않다면 2.3을 생략하고 3.0으로 넘어가면 된다.

아래에 염두할 것들이다.
  1. 의존성은 중요하다. 여러분의 의존성이 Swift2.3이나 Swift3.0을 지원하는지 확인해야한다. 대부분 주요 라이브러리/프레임워크들은 둘 다 지원하고 있다(Alamofire, Charts). 그러나 기억해야할 것은 Swift3.0을 업그레이드 하는 방법은 하나밖에 없다. Swift3.0으로 새 개발을 시작하고 나면 2.3버전의 모든 의존성이 업데이트 없이는 작동하지 않을 것이다.
  2. 8.x 버전의 Xcode는 Swift2.3 지원을 멈출 것이다. Swift2.3은 Swift3.0으로 업그레이드 하기 힘든 프로젝트를 위한 매개 단계의 경향이 있다.
  3. 만약 여러 프로젝트에 걸쳐진 개인의 CocoaPods이 있다면 먼저 이것부터 업그레이드 해야한다.

2.3으로 업그레이드 할 수 있으나 어느정도 지연이 될지도 모른다. Xcode9가 배포되기 전에 Swift3으로 갈아타야하기 때문이다.

Swift2.2에서 2.3으로 마이그레이션 이슈
마이그레이터는 아마 Range<T>를 CountableRange<T>로 변환할 것이다. Swift3에는 오직 CountableRange<T> 밖에 없기 때문이다.

마이그레이터는 dispatch_queue_set_specific() 사용을 위해 /*Migrator FIXME: Use a variable of type DispatchSpecificKey*/ 주석을 달아 줄 것이다. 이것은 미래의 Swift3으로 마이그레이션 하는 것과 관련이 있다.

Swift2.x에서 Swift3.0으로의 마이그레이션 이슈
Swift의 3.0버전은 "모든걸 깨부순(break everything)" 배포이다. Swift3은 모든 것을 깨부수고 언어적 긴 용어가 필요없는 모든 것을 없앨 것이다. 따라서 많은 변화가 생기고, 프로젝트를 안정화시키기위해 그 많은 것들을 고쳐야할 것이다.

Swift2.3
Swift2.3은 굉장히 마이너한 업데이트이다. 어떤 식으로 마이너할까? 사실 딱 한가지가 바뀌었다. 컴파일타임 nullability가 바뀐 애플 SDK의 Objective-C 코드를 체크하는 것이다.

몇몇 마이너한 이름 재정의과 수많은 옵셔널 생성자와 같은 다른 변화들은 이제 옵셔널 대신에 필요한 오브젝트를 반환한다. 이것은 번들로부터 nib을 인스턴스화하는 것과 같은 것들에 적용됐다.

Swift3.0

(인지할만한)주요 변화들
private의 정의는 fileprivate로 바뀌었다.

공식적으로 private라 불리던 것이 이제 fileprivate로 바뀌었다. fileprivate 변수는 extension으로 접근할 수 있다. private 변수는 클래스에서 extension으로 접근할 수 없다.


public의 정의는 open으로 바뀌었다.
현재 클래스나 파라미터 public 정의는 두가지 기능을 제공한다.
  1. 외부모듈이 클래스나 맴버를 사용할 수 있다.
  2. 외부모듈이 클래스나 맴버를 오버라이드 할 수 있다.

Swift3에서의 public은 외부적으로 사용가능함이지, 오버라이드 가능함은 아니다.
이전의 이 기능은 open으로 되었다.


동사와 명사
함수 이름이 -d(과거형)으로 끝나는 것들은 그 오브젝트의 새 인스턴스를 반환한다.
이러한 변화는 reverse와 reversed, enumerate와 enumerated 등에 적용한다.

Objective-C의 불리언은 이제 is라는 단어로 시작되고 Foundation 타입은 더이상 접두에 NS를 사용하지 않는다(NSString처럼 String과 충돌이 일어나는 경우들은 빼고 말이다.).

Foundation 타입은 Swift의 let과 var 선언에서 아지 잘 동작할 것이다.



맴버로 불러오기
설명이 달린 C 함수들을 메소드로 부를 수 있다. 임포터(importer)는 보통 이러한 맵핑을 자동으로 추론할 수 있아며 Swift에 예전의 C-API를 자연스럽고 네이티브하게 사용할 수 있게 해준다. Case and point, CoreGraphics API. 참고로 CoreGraphics는 이번 배포때 공식적으로 업데이트 되지 않았다.


케멀케이스(CamelCase) 변화들
열거형이나 프로퍼티에서 머리문자(CG, NS등)로 시작하는 것들이 UpperCamelCase에서 LowerCamelCase로 대체되었다. 
// Before
let red = UIColor.redColor().CGColor
// After
let red = UIColor.red.cgColor

(개인적으로 가장 작은 변화라 생각되는)상태절 변화

이제부터는 guard, if, while절에서 where 키워드를 사용할 수 없다. where 키워드는 for-in-where 형태의 for문에서는 사용할 수있다.
case 절에서도 마찬가지로 바뀌었다.


첫번째 인자의 문자 일관성
이렇게 이해하면 쉽다: 첫번째 파라미터의 이름은 디폴트로 필요하다.


(내가 좋아하는 변화인)묵시적으로 언랩핑된 옵셔널 다루기
여러분이 여러 언어(shared-language)로 프로젝트를 할 때 이 점 덕분에 마이그레이션 작업이 의미있어진다고 생각이 든다. 그것이 무엇일까?

이전에 ImplicitlyUnwrappedType! 프로퍼티를 가진 Objective-C 타입이 이제 WrappedType?로 되었다. 아래의 경우를 제외하고 모두 적용된다.

더 나은 Objective-C API 변환
네이밍이 더 명확해졌다.

최신식 디스패치

컬랙션 타입이 새로운 모델을 가지게 됨
이전에 컬랙션 타입에서 한 인덱스에서 시작하여 탐색할 때는 index의 successor 메소드를 사용해야 했다. 이제는 이 책임이 컬랙션으로 넘어가게 되었다. c.index(after:index) 이런식으로 작성한다. 컬랙션은 이제 어떤 comparable 타입의 인덱스를 가진다.
(아래 부분은 Range 오브젝트를 손수 만들때 적용된다. 보통 이렇게 할 일은 드물거라 생각된다.)

이런 변화에서 사이드 이팩트로서 Range가 여러 타입으로 쪼개어졌다(Range, ClosedRange, CountableRange, CountableClosedRange) ClosedRanged는 이제 그 타입(0...Int8.max)의 최대값 범위를 포함한다. Range와 ClosedRange는 더이상 반복(iterate)을 할 수 없다. 그 의무로서 오직 Comparable 오브젝트만 필요하다. 따라서 Range<String>을 만들 수 있다.

Objective-C의 id는 Swift의 Any 타입으로 불러와진다.
애플의 말을 인용하자면
이제부터 id는 'AnyObject'가 아닌 'Any'로 불러오기 때문에, 이전에 여러분이 'AnyObject'로 동적인 검색을 수행하는 곳에서 애러가 뜰 수도 있다.

(인식하지 못할지도 모르는)작은 변화들
옵셔널 비교연산자가 제거됨
현재 nil 타입은 비교가능하다.

이 점은 아래와같이 버그를 만들기 쉽다.

이제는 이것들을 비교하기 전에 반드시 언랩핑을 해주어야한다.
이것이 굉장히 좋은 점 중 하나이기도 하지만 유닛테스트를 망가뜨릴지도 모른다.

클러저 파라미터 이름과 레이블
수많은 클로저 파라미터 이름이 재정의되고 선택적으로 바뀌었다.

flatten이 join으로 명칭이 바뀌었다.

UnsafePointer<T> 다루기
오브젝트가 아닌 것들의 포인터 타입의 nullability는 옵셔널을 사용해 표현할 수 있다.

부동 소숫점 값을 반올림하는 기능은 이제 그 값이 가지고 있다.
이전에는 부동 소숫점을 반올림하기 위해 전역의 C함수(float나 ceil)을 사용할 수 있었다. 아직 이 함수들도 사용가능하나 디프리케이트 될지 고려되고 있다.

대신 아래와 같이 사용할 수 있다.
추가적인 반올림 기준이다.
  1. toNearestOrAwayFromZero
  2. toNearestOrEven
  3. towardZero
  4. awayFromZero

제네릭 타입 에일리어스

연산자 정의 문법의 변화

Objective-C의 상수는 이제 Swift 타입이다.
이제 Objective-C inter-op 식의 문자열을 사용할 수 없다.

굳이 걱정하지 않을 정도로 작은 것
NSError의 Bridging이 강화되었다.

nulTerminatedUTF8CString이 utf8CString으로 이름이 바뀜

문자열의 UnicodeScalar 생성자 중복을 제거

실패할 수 있는 UnicodeScalar 생성자는 이제 옵셔널을 반환한다.

더이상 튜플 splatting이 안된다.

더이상 curry한 func 선언 문법이 안된다.

이 모든 변화가 Swift3에서 일나날까?
아니다.

아직 이번 배포를 위해 검토하는 계획 안이다.(옮긴이: Swift3은 16.9.13에 정식 배포가 되었는데, 이 글은 16.8.31에 쓰여졌다)  몇 계획은 Swift3.x에서 달라질 것이고, 우리가 일반적으로 사용하지 않는 API 호출의 제거/병합과 같은 아주 마이너한 변화들은 포함시키지 않았다. 

추가로 나는 이 개별적인 변화를 깊게 볼 필요가 없었으며, 대신 각 변화에대한 높은 수준에서 개괄적인 설명을 하였다. 몇 변화는 그 영역에서 파급표과를 가지고 있으며, 여러분이 이러한 변화에 관심이 있으면 Swift EVO project를 한번 방문해 보아라.

이번 배포에서 검토 진행/대기중인 변화들
  1. Sequence-기반 생성자를 추가하고 Dictionary에 메소드를 합친다.
  2. 약 참조(weak reference)에서 강 참조(strong reference)로 self를 업그레이드하여 옵셔널 바인딩을 사용할 수 있게 해준다.
  3. 표준 라이브러리에 AnyHashable을 추가한다.

3.x 배포에서 바뀐것들
  1. 순환 알고리즘
  2. .self 제거
  3. 커스텀 Objective-C 표현을 제공하는 것을 Swift 타입에서 허용
  4. 동적 케스터로부터 연결 변환 동작을 제거
  5. Sequence end-operation 이름을 합리화

보너스
제거되어서 우리 모두가 기쁜 것들

  1. Swift 언어에서 where문 제거
  2. 인스턴스 맴버를 접근하기 위해 self가 필요함
  3. extension에서 접근 변경자 제거



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

,


이 튜토리얼은 비도오 시리즈로 나와있다.
라이브 코딩과 함께 비디오 튜토리얼을 보고 싶다면 이 글의 내용으로 녹화한 비디오 시리즈를 보아라: Egghead - Introduction to Reactive Programming

여러분은 Rx, Bacon.js, RAC등의 다양한 것들을 포함해서 리액티브 프로그래밍이라 불리는 새 것을 배우는데 관심이 있을 것이다.

이것을 배우기는 쉽지 않은데, 좋은 도구가 없다는 점이 한 몫을 더한다. 내가 처음 배우려할 때도 튜토리얼을 찾아보고자 했다. 나는 소량의 가이드를 찾아냈지만 그것도 겉핥기 식이었지 전체 구조를 리액티브로 만드는 가이드는 없었다. 또한 라이브러리는 당신이 몇몇의 기능만 이해하려 할 때 크게 도움이 되지 않을 때가 종종 있다. 아래를 보면 무슨 의미인지 공감할 수 있을 것이다.

Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
Projects each element of an observable sequence into a new sequence of observable sequences by incorporating the element's index and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.

아이고..

나는 두가지 책을 읽었는데, 하나는 그냥 큰 그림을 설명하는 것이었고, 다른 하나는 리액티브 라이브러리를 어떻게 사용하는지 깊이 이야기한 책이었다. 나는 마침내 실제 만들어 보면서 리액티브 프로그래밍 배우는 것을 힘겹게 끝냈다. 나의 Futurice라는 일에서 리액티브 프로그래밍을 실제 프로젝트에 사용하였고 내가 문제가 생겼을 때 몇몇 동료로부터 도움을 얻을 수 있었다.

이것을 배우면서 가장 어려웠던 점은 리액티브하게 생각해야한다는 것이었다. 자꾸 이전의 명령형이나 상태를 가지는 전형적인 프로그래밍으로 가려는 것을 강제로 새로운 패러다임으로 작업하게 만들었다. 이 부분에 대해서는 인터넷에서 어떤 가이드도 찾지 못했다. 나는 사람들이 새로 이것을 시작할 수 있게 어떻게 리액티브하게 생각하는지에 대한 실질적인 튜토리얼이 필요하다고 생각했다. 이 생각이 조금 바뀌고 나면 라이브러리의 문서가 좀 이해가 될 것이며, 이 글이 당신에게 도움이 되길 바란다.

"리액티브 프로그래밍이 뭔가?"
인터넷 상에는 좋지 않은 설명과 정의가 있었다. 위키피티아는 너무 일반적이고 이론적이게 설명해 놓았다. 스택오버플로우의 인기있는 답변은 새내기들에게 적합하지 않아 보인다. 리액티브 매니페스트는 당신 회사에 프로젝트 매니저나 영업자에게나 보여줄 법 하다. 마이크로소프트의 Rx용어 "Rx = observables + LINQ + Schedulers"는 마이크로소프트한것이 우리에게 혼동만 남겨준다. "리액티브"나 "변화의 전파(propagation of change)"와 같은 용어는 일반적인 MV * 나 즐겨쓰는 언어가 이미 한 것과 다른 내용이 아니다. 물론 내 프레임워크의 뷰는 모델에게 리액트한다. 물론 변화는 전파된다. 그렇게 하지 않으면 아무것도 렌더링 되지 않을 것이다.

그럼 이제 잡소리는 집어치우자.

리액티브 프로그래밍은 비동기 데이터 스트림으로 프로그래밍 하는 것이다.

뭔가 새로운 것이 있는게 아니다. 이벤트 버스나 여러분의 일반적인 클릭  이벤트는 굉장히 비동기적인 이벤트 스트림인데, 이것을 옵져브 할수도 있고, 다른 추가적인 효과를 줄 수도 있다. 리액티브 스테로이드(옮긴이: 화학에 관련된 용어)로부터 떠오른 아이디어이다. 여러분은 클릭이나 마우스 호버(hover) 이벤트 뿐만 아니라 어떠한 데이터 스트림도 만들 수 있다. 변수, 유저입력, 프로퍼티, 캐시, 데이터 구조체등 어떤것도 스트림이 될 수 있으며, 스트림은 싸고 범용적이다. 예를들어 당신의 트윗 피드가 클릭  이벤트와 같은 방식으로 데이터 스트림이라고 상상해보아라. 당신은 그것을 듣고(listen)있다가 적절하게 반응을 하면 된다.

최상부에는 어떤 스트림에서도 합치고, 생성하며, 필터링할 수 있는 툴박스를 제공한다. 이것이 "함수형"의 마법 효과이다. 한 스트림은 다른 것의 입력으로 사용될 수 있다. 여러개의 스트림까지도 다른 스트림의 입력으로 사용될 수 있다. 두 스트림을 합칠 수 있다(merge). 원하는 이벤트만 골라내어 새 스트림으로 필터링할 수 있다(filter). 한 스트림을 새 스트림으로 각 값들을 매핑할 수 있다(map).

리액티브에서 스트림이 중심이 되면, 스트림을 잘 살펴보자. 우리는 친숙한 "버튼 클릭" 이벤트로 이 이야기를 시작하려한다.


한 스트림은 시간 순서로 정렬된 이벤트 순서이다. 이 스트림 값(어떤 타입), 에러, '완료'신호 3개의 상태를 내뱉을 수 있다. "완료" 지점이 되었다고 하자. 예를들어 버튼이 있는 현재 윈도우나 뷰가 닫혔을 때 이다.


우리는 발생된 이벤트를 비동기적으로 잡아내야하는데, 값을 내뱉을 때 실행되는 함수, 에러를 내뱉을 때 실행되는 함수, '완료'를 내뱉을 때 실해오디는 함수를 선언해놓고 잡아낼 수 있다. 때론 뒤에 두가지 함수는 생략하고 값을 위한 함수만 사용할 수도 있다. 스트림을 "듣고(listen)"있는 것은 '구독하고 있다'고 부른다. 우리가 정의한 함수는 옵저버이다. 스트림은 옵저베이블(observable)이다. 이것이 바로 옵저버 디자인 패턴이다.

이제는 위 그림 대신에 아스키(ASCII)로 설명을 대신하겠다.
--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
여기까지는 꽤 친숙할 것이고 여러분을 지루하게 하고 싶지 않다. 이번에는 원래의 클릭 이벤트 스트림에서 변형하여 만든 새 클릭 이벤트 스트림을 만들어보자.

먼저 숫자를 세는 스트림을 만들자. 이 스트림은 버튼이 얼마나 클릭됐는지 나타낸다. 일반적인 리액티브 라이브러리에서는 map, filter, scan과 같이 제공되는 많은 함수를 가지고 있다. 만약 당신이 clickStream.map(f)처럼 함수를 호출하면 clickStream으로부터 새 스트림을 반환받는다. 원래의 clickStream은 손대지 않고 말이다. 이러한 특징을 불변성이라 부르며, 마치 팬케잌에 시럽이 좋듯 리액티브에 좋게 해준다. 이것은 clickStream.map(f).scan(g)와 같이 함수 체이닝을 가능하게도 해준다.
  clickStream: ---c----c--c----c------c-->
               vvvvv map(c becomes 1) vvvv
               ---1----1--1----1------1-->
               vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f) 함수는 당신이 만든 f함수에 따라 만들어진 각 값으로 대체한다. 우리의 경우 각 클릭마다 숫자 1로 매핑시켰고 scan(g) 함수가 값 x=9(acculated, current)를 실행하여 스트림의 모든 이전 값을 합한다. 여기서 g함수는 단지 더하는 함수이다. 그리고 counterStream은 클릭이 일어난 수를 내뱉는다.

리액티브의 실제 힘을 보여주기 위해 여러분이 갑자기 '더블 클릭' 이벤트의 스트림이 필요하다고 해보자. 좀 더 흥미롭게 만들기 위해 더블클릭처럼 트리플클릭도 되는지 혹은 다중클릭(더블클릭 이상)까지도 되는지 보자. 깊게 숨을 들이마쉬고 생각해보자. 전통적인 명령형의 상태와 함께 사용한 방식에서는 어떻게 구현했는지 상상해보라. 아마 꽤 지저분하게 몇몇 변수는 상태를 가지고 있었을 것이고 몇몇 변수는 시간차를 세고 있을 것이다.

흠 리액티브에서는 꽤 간단하게 해결된다. 사실 로직은 4줄의 코드 밖에 안된다. 그러나 지금 당장은 코드를 보지 말자. 당신이 초보자이든 숙련자이든 스트림을 이해하게 만드는 데는 그림으로 설명하는 것이 최고이다.


회색 상자들은 한 스트림에서 다른 스트림으로 변경시키는 것이다. 먼저 250ms동안 이벤트 발생이 없으면 클릭했던 이벤트들을 리스트로 모은다. (이게 buffer(stream.throttle(250ms))가 무슨 일을 하는지의 이야기이다. 이 시점에서 굳이 깊게 이해하려고 하지 말자. 지금은 단지 리액티브로 데모를 만들고 있다.) 그리고 map()을 적용시켜 각 리스트의 크기를 정수로 매칭시키고, 매핑한 리스트의 스트림으로 결과가 나온다. 마지막으로 filter(x>=2)함수를 이용해 정수 1은 무시한다. 우리가 의도한 스트림으로 만들기 위해 사용한 것은 3개의 오퍼레이션이 다다. 이제 우리가 원하는 곳에 반응해주기위해 이것을 구독할 수 있다.

여러분이 이 아름다운 방법을 잘 음미했길 바란다. 이 예제는 사실 빙산의 일각이다. API 응답 스트림처럼 다른 스트림을 이 오퍼레이션으로 적용할 수 있고 또 수많은 다른 함수들도 사용할 수 있다.

"왜 RP 적용시키기를 고려해야할까?"
리액티브 프로그래밍은 당신 코드의 추상화 수준까지 올라왔으므로, 수많은 세부구현을 끊임없이 할게 아니라 비즈니스 로직을 정의하는 이벤트의 상호 의존에 초점을 맞출 수 있었다. RP의 코드는 더 간결해 질것이다.

데이터 이벤트에 관련한 수많은 UI 이벤트와 높게 소통하는 현대 웹앱과 모바일앱에서는 그 이점이 더욱 분명하다. 10년전에는 백엔드에 긴 형식으로 보내면 간단하게 프론트엔드에 렌더링 시키는 것이 웹페이지의 인터렉션이었다. 앱은 더 실시간으로 진화되었다. 몇 컨텐츠는 연결된 다른 사용자에게 실시간으로 반영되는 것처럼 가볍게 한 필드를 수정하는 것이 바로 백엔드에 저장하는 트리거가 될 수 있다.

오늘날의 앱은 사용자에게 높은 소통을 경험하게 해주는 수많은 종류의 실시간 이벤트가 들어가있다. 우리는 이것을 다루기위해 적절한 툴을 골라야하는데, 리액티브 프로그래밍이 그 해답이다.

예제와함께 RP처럼 생각하기
실제 예제로 들어가보자. 어떻게 RP처럼 생각하는지 단계별로 설명하는 실세계의 예제이다. 가짜 예제도 아니고 개념의 일부만 설명하기 위한 예제도 아니다. 이 튜토리얼이 끝날때쯤 우리가 왜 이것들을 했는지 아는체로 실제 함수형 코드를 만들 것이다.

나는 다음의 이유로 자바스크립트RxJS를 사용할 것이다. 자바스크립트가 현재 가장 인기있는 언어이며 Rx* 라이브러리 패밀리가 많은 언어나 플랫폼에서 가장 폭넓게 쓰이고 있다.(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy 등등) 따라서 당신이 어떤 툴을 쓰든, 이 튜토리얼을 따라오면서 구체적인 이점을 얻을 수 있을 것이다.

"Who to follow" 제안 박스 구현하기
트위터에서는 당신이 팔로우할 수 있는 다른 계정들을 제안하는 UI 요소가 있다.

우리는 이 핵심 기능을 모방해 볼것이다.
  • 시작하면 API로부터 계정 데이터를 불러오고 3가지 제안을 띄운다.
  • "Refresh"를 클릭하면 3개의 또다른 계정을 띄운다.
  • 계정중에 'x' 버튼을 누르면 그 계정은 사라지고 다른 계정을 띄운다.
  • 각 줄은 계정의 아바타를 띄우고, 누르면 그들 페이지로 이동한다.
다른 기능이나 버튼은 부수적이기 때문에 남겨 둘 것이다. 그리고 트위터 API는 최근들어 승인이 필요하게 되었으므로 대신에 깃헙의 팔로잉 사람들을 위한 UI를 만들어보자. 여기에 사용자를 얻어내기 위한 Github API(링크)가 있다.

완성된 코드는 http://jsfiddle.net/staltz/8jFJH/48/ 여기에 준비되있다.

요청과 응답
어덯게 Rx로 이 문제를 접근할까? 흠, 시작하기 앞서, (대부분) 모든 것은 스트림이다. 이것이 Rx의 주문이다. 가장 쉬운 "시작하면 API로부터 3가지 제안을 띄운다" 기능부터 시작해보자. 특별한 것은 없어 보이며 간단하게 (1)요청을 날리고 (2)응답을 받고 (3)그 응답을 표시하면 된다. 그럼 이제 우리의 요청을 스트림으로 표현해보자. 처음 봤을때는 좀 힘들어 보이지만 우리는 기초부터 시작해야 한다.

시작하면 한 요청만 날리면 되고, 데이터 스트림으로 모델링하면 하나의 값만 스트림이 될 것이다. 그 후에는 우리도 알듯 많은 요청을 보내게 될테지만, 지금은 하나만 해보자.
--a------|->

Where a is the string 'https://api.github.com/users'
이것은 우리가 요청할 URL의 스트림이다. 요청 이벤트가 발생하면 언제, 무엇이 발생했는지 알려준다. 요청이 "언제" 호출될지는 이벤트가 언제 발생되는지와 같은 시점이다. 그리고 "무엇이" 요청됐는지는 발생된 값이다.(URL을 담고 있는 문자열)

Rx* 한 값으로 이런 스트림을 만드는 것은 굉장히 간단하다. 스트림에서 공식적인 용어는 "Observable"이다. 이것을 Observe 할 수 있다고 하지만 이렇게 바보처럼 말하지 말고, 이것을 스트림이라 부르겠다.

var requestStream = Rx.Observable.just('https://api.github.com/users');

이제부터는 이것이 문자열의 스트림이고 다른 동작을 하지 않으며, 값이 나올때 우리가 필요한대로 어떻게 처리할 수 있다. 이것을 subscribing으로 스트림에 할 수 있다.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  jQuery.getJSON(requestUrl, function(responseData) {
    // ...
  });
}

우리는 요청 오퍼레이션의 비동기 처리를 위해 jQuery Ajax 콜백(여러분은 이미 알고 있다고 가정한다)을 사용한다. 그런데 가만보자. Rx는 비동기 데이터 스트림을 다루기위해 있다. 그 요청에 대한 응답이 곧 있다가 받을 데이터를 담은 스트림일 수는 없을까? 음, 개념상으로는 가능해보이니 한번 시도해보자.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  var responseStream = Rx.Observable.create(function (observer) {
    jQuery.getJSON(requestUrl)
    .done(function(response) { observer.onNext(response); })
    .fail(function(jqXHR, status, error) { observer.onError(error); })
    .always(function() { observer.onCompleted(); });
  });

  responseStream.subscribe(function(response) {
    // do something with the response
  });
}

Rx.Observable.create()가 하는 일은 각 Observer(혹은 다른말로 '구독자')에게 데이터 이벤트(onNext())나 에러(onError())를 명시적으로 알리는 커스텀 스트림을 만든다. 우리가 한 일은 그냥 jQuery Ajax Promise을 감싼 것이다. 잠시만요, 이게 Promise이란게 Observable을 의미하는 건가요?




그렇다.

Observable은 보장++이다. Rx에서 당신은 var stream = Rx.Observable.fromPromise(promise)하여 쉽게 Promise를 Observable로 변환할 수 있다. 그러니 이것을 사용해보자. 다지 다른 점은 Observable이 Promise/A++를 따르지 않지만 개념적으로 충돌은 없다. 간단하게 한 보장은 하나의 발생된 값과 함께 한 Observable이다. 꽤 좋아보인다. 적어도 Observable이 Promise만큼 강력한지 보여준다.

여러분이 Promise의 속임수라 믿는다면 Rx Observable이 어떤 것이 유능한지 눈으로 지켜보자.

이제 예제로 다시 돌아와서, 새 subscribe()를 또 하나더 그 안에서 호출하면 콜백지옥 같은 형태가 되버린다. 또한 responseStream 생성은 requestStream에의해 결정된다. 이전에도 들었듯 Rx에서는 새 스트림을 변형, 생성해내는 간단한 매커니즘이 있으므로 이것을 시행해보자.

이제부터는 알아야할 기본함수인 map(f)가 있다. 이것은 스트림A의 각 값을 받아서 f()를 적용시키고 스트림B로 만든다. 이것을 우리 요청과 응답 스트림에 하려면 요청 URL을 응답 Promise(스트림처럼 만든)에 매핑할 수 있다.
var responseMetastream = requestStream
  .map(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

그다음 우리는 스트림의 스트림인 "metastream"을 하나 만들 것이다. 아직 당황하지 마라. metastream은 각각 발생했던 값이 있는 또다른 스트림이다. 여러분은 이것을 '포인터'라 생각할 수도 있겠다. 각 발생한 값은 다른 스트림을 가리키는 포인터이다. 우리 예제에서는 각 요청 URL이 Promise 스트림을 가리키는 포인터로 매핑되는데, 이 promise 스트림은 해당되는 응답을 가지고 있다.


응답을 위한 한 metastream은 혼란스러워 보이며 우리에게 크게 도움이 되지 않는 것 같다. 우리는 각 발생된 값이 'promise'의 JSON 객체가 아닌 그냥 간단한 응답 스트림이 필요할 뿐이다. Flatmap씨(링크)에게 인사하자. "branch" 스트림에서 나오는 모든 "trunk" 스트림을 내보냄으로서 동작하는, flatmap은 한 metastream을 "flattens"하게 만드는 버전의 map()이다. Flatmap은 "고정"된 것이 아니고 metastream은 일종의 버그가 아니다. 이것들은 Rx에서 비동기로 응답을 다루기위한 실제 툴이다.
var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });


좋다 응답 스트림이 요청 스트림에 맞춰 정의되었으므로, 후에 요청 스트림에서 이벤트가 발생하면 예상한대로 응답 스트림에서 발생한 응답 이벤트들을 가질 수 있을 것이다.

requestStream:  --a-----b--c------------|->
responseStream: -----A--------B-----C---|->

(lowercase is a request, uppercase is its response)

이제 마침내 응답 스트림을 가지게 되었다. 우리가 받은 데이터를 화면에 띄우면 된다.

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});

지금까지의 코드를 모두 합쳐보자.

var requestStream = Rx.Observable.just('https://api.github.com/users');

var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});
새로고침 버튼
나는 아직 응답의 JSON이 100명의 유저라는 것에 대해 말하지 않았다. 이 API는 한 페이지 크기를 설정할 수 있는게 아니라 페이지 번호를 설정할 수 있게 해놓았다. 따라서 우리는 오직 3개의 데이터만 쓰고 나머지 97개의 데이터는 버려진다. 우선은 이 문제를 무시할 것이다. 그리고 나중에 이 응답을 어떻게 캐신할 수 있는지 살펴볼 것이다.

새로고침 버튼을 누를때마다 요청 스트림은 새 URL을 발생시킬 수 있으므로 새 응답을 얻어낼 수 있다. 우리는 다음 두가지가 필요하다. 새로고침 버튼의 클릭 이벤트 스트림(주문: 모든것이 스트림이 될 수 있다). 그리고 새로고침 클릭 스트림에 따라 요청 스트림을 바꿀 수 있는 것도 필요하다. 기꺼히 RxJS는 이벤트 리스너로부터 Observable을 만들 수 있는 툴을 제공한다.
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

새로고침 클릭 이벤트가 그 자신의 API URL을 관리하지 않으므로 우리는 각 클릭을 실제 URL로 매핑해주어야한다. 이제 요청 스트림을 새로고침 클릭 스트림으로 바꾸었다. 새로고침 클릭 스트림은 매번 랜덤의 페이지 값으로 API endpoint를 바꾸어 매핑한다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

내가 좀 어리석고 자동 테스트를 못해서 이전에 만든 기능 중 하나를 가져왔다. 한 요청은 더이상 시작할 때 일어나지 않고 새로고침을 눌렀을때만 일어난다. 아아. 나는 이 요청이 새로고침을 누를때나 사이트를 켰을때나 둘 다 동작하게 하고 싶다. 


우리는 이 상황별 스트림을 어떻게 분리하는지 알고있다.
var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

그러나 이제 '쪼개진' 두 스트림을 어떻게 하나로 합칠까? merge()가 있다. 아래 다이어그램에서 어떻게 되는건지 설명했다.

stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
          vvvvvvvvv merge vvvvvvvvv
          ---a-B---C--e--D--o----->

이제 쉬워졌다.

var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

var requestStream = Rx.Observable.merge(
  requestOnRefreshStream, startupRequestStream
);

매개 스트림 없이 만들 수 있는 깔끔한 방법의 대안이다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .merge(Rx.Observable.just('https://api.github.com/users'));

더 짧아지고 가독성도 더 좋아졌다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .startWith('https://api.github.com/users');

startWith() 함수는 당신이 생각하는데로 정확히 그렇게 동작할 것이다. 입력 스트림이 어덯게 생겼든 상관없이 startWith(x)로부터 나온 결과 스트림은 시작부분에서 x를 가질 것이다. 그러나 나는 아직 DRY 하지 못하다. 나는 API endpoint 문자열을 반복에서 쓰고 있다. 이것을 고치기 위한 한가지 방법은 startWith()refresgClickStream에 붙이는 것이다. 이것은 시작 시점에 새로고침 클릭을 강제로 시행하기 위함이다.

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

좋다. 만약 내가 자동 테스트를 쪼개었다는 곳으로 돌아가보면 이전 것과 다른 점이 startWith()만 추가한 것 밖에 없다.


스트림으로 3가지 제안을 모델링하기
이제부터 터치된 제안 UI 요소만 가지고 있는데, responseStream의 subscribe에서 일어난 렌더링 단계에서 일어났다. 이제 새로고침 버튼에 문제가 있다. '새로고침'을 누르자마자 현재 3개의 제인이 명확하지 않다. 새로운 제안이 응답이 도착한 후에 화면에 나타나는데, UI적으로 좀더 좋게 만들기 위해 새로고침을 누를 때 현재 있던 제안들을 지울 필요가 있다.
refreshClickStream.subscribe(function() {
  // clear the 3 suggestion DOM elements 
});

아니다.. 빠르지 않다. 우리는 제안의 DOM 요소에 영향을 주는 두 구독자가 있기때문에 그렇다. 그리고 이것은 일을 쪼개는 것(sepration of concerns)처럼 보이지도 않는다. 리액티브의 주문이 기억나는가?


이제 제안을 스트림으로 모델링할것인데, 각각 발생한 값은 제안 데이터를 가지고 있는 JSON 객체이다. 우리는 3가지 각 제안마다 이 일을 할것이다. 이것은 1번 제안을 위한 스트림이 어떻게 생겼는지 보여준다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  });

그리고 suggestion2Streamsuggestion3Stream은 그냥 suggestion1Stream을 복사하여 붙여 넣은 것이다. 이것은 DRY는 아니나 튜토리얼의 예제를 간단하게 만들기위해 이렇게 하였고, 추가로 이 경우에는 어떻게 중복을 피할수 있을지 생각해볼수 있는 좋은 기회라 생각된다.

responseStreamsubscribe()에서 렌더링이 일어나게 하는 것 대신에 이렇게 했다.
suggestion1Stream.subscribe(function(suggestion) {
  // render the 1st suggestion to the DOM
});

"새로고침을 누르면 제안들을 지운다"로 돌아가서, 우리는 간단하게 새로고침 클릭을 제안 데이터에 null로 매핑하여 suggestion1Stream에 넣는다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  );

그리고 화면에 띄울때는 null은 따로 분기처리하여 "데이터가 없음"이라하고 그 UI 요소를 숨긴다.

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

여기 큰 그림이다.

refreshClickStream: ----------o--------o---->
     requestStream: -r--------r--------r---->
    responseStream: ----R---------R------R-->   
 suggestion1Stream: ----s-----N---s----N-s-->
 suggestion2Stream: ----q-----N---q----N-q-->
 suggestion3Stream: ----t-----N---t----N-t-->

Nnull을 의미한다.


보너스로, 시작할때 '빈' 제안들이 화면에 나타날 것이다. 이제 제안 스트림에 startWith(null)을 추가하면 된다.
var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

그 결과이다.

refreshClickStream: ----------o---------o---->
     requestStream: -r--------r---------r---->
    responseStream: ----R----------R------R-->   
 suggestion1Stream: -N--s-----N----s----N-s-->
 suggestion2Stream: -N--q-----N----q----N-q-->
 suggestion3Stream: -N--t-----N----t----N-t-->
제안을 닫고 캐싱된 응답을 사용하기
이 기능이 마지막 남은 구현이다. 각 제안은 그것을 닫을 수 있는 'x'버튼을 하나씩 가지고 있고, 그것을 누르면 그 자리에 다른 제안이 들어온다. 닫기 버튼이 눌러지면 새 요청을 만들어야 한다고 생각해볼 수 있다.
var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button

var requestStream = refreshClickStream.startWith('startup click')
  .merge(close1ClickStream) // we added this
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

위의 것은 제대로 동작하지 않는다. 이것은 하나만 눌렀는데 모두 닫으면서 모든 제안을 다시 갱신할 것이다. 이 문제를 해결하기 위한 방법에는 여러개가 있을 것이다. 우리는 이전 응답을 재사용 함으로서 이 문제를 해결할 것이다. API 응답의 페이지 크기는 100명의 사용하지만 우리는 딱 3명만 사용하고 있으므로 남은 데이터를 사용할 수 있을 것이다. 새로운 요청 없이 말이다.


다시 말하자면, 스트림으로 생각해보자. 'close1' 클릭 이벤트가 발생하면 우리는 가장 최근에 responseStream에서 가져올 랜덤 유저 한명을 사용하고 싶다.
    requestStream: --r--------------->
   responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->

Rx*에서는 우리에게 필요한 것 처럼 보이는 결합함수, combineLatest가 있다. 이 함수는 인풋으로 A, B 스트림을 받고, 어떤 스트림이 값을 발생시키든 그때마다 combineLatest는 가장 최근에 두 스트림으로부터 a, b 값을 합친다. 아웃풋 값은 c=f(x,y)인데, f는 여러분이 정의한 함수이다. 다이어그램으로 이해하는게 더 좋을 것이다.

stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
          vvvvvvvv combineLatest(f) vvvvvvv
          ----AB---AC--EC---ED--ID--IQ---->

where f is the uppercase function

우리는 close1ClickStreamresponseStreamcombineLatest()를 적용시킬 수 있으므로, 닫기버튼1이 클릭될 때 마다 마지막에 발생된 응답을 얻어와서 suggestion1Stream에 새 값으로 만든다. 한편 combineLatest()는 대칭적인데, responseStream에서 새 응답이 나올 때 마다 새 제안을 만들어내기위해 마지막 '닫기1' 클릭을 합친다. 이 부분이 재미있는데, 이전에 suggestion1Stream 코드를 간단하게 만들어준다.

var suggestion1Stream = close1ClickStream
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

아직 한 부분이 빠졌다. combineLatest()는 두 소스 중 가장 최신의 것만 사용한다. 그러나 이 소스 중 하나가 아직 아무것도 발생시키지 않았다면, combineLatest()는 아웃풋 스트림에 데이터 이벤트를 만들 수 없을 것이다. 위의 아스키 다이어그램을 보면 첫번째 스트림이 a를 만들었을때 아웃풋에 아무것도 없음을 확인할 수 있을 것이다. 두번째 스트림에서 b 값을 만들었을때 비로소 아웃풋 값이 만들어질 수 있다.


다른 해결 방법이 있는데, 시작할 때 '닫기버튼1'을 모의로 눌러보는 것이다.

var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
  .combineLatest(responseStream,             
    function(click, listUsers) {l
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
합쳐보기
이제 끝났다. 우리가 만든 코드의 완성본이다.
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// and the same logic for close2 and close3

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var responseStream = requestStream
  .flatMap(function (requestUrl) {
    return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
  });

var suggestion1Stream = close1ClickStream.startWith('startup click')
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
// and the same logic for suggestion2Stream and suggestion3Stream

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

http://jsfiddle.net/staltz/8jFJH/48/ 여기서 예제가 돌아가는 것을 확인할 수 있을 것이다.

이 코드 조각은 작지만 빽빽하다. 여기에는 적당히 일을 분배하여 다중 이벤트를 관리하고 응답을 캐싱하는 것까지 기능이 들어있다. 함수형  스타일은 명령형 스타일보다 더 서술적인 코드를 만든다. 우리는 실행되야할 인스트럭션의 순서를 만든게 아니라, 스트림 사이의 관계를 정의함으로서 그냥 어떤것인지 말했을 뿐이다. 예를 들어 if, for, while과같은 컨트롤 플로우 요소들이 없고 자바스크립트에서 주로 쓰는 일반적인 콜백기반 컨트롤 플로우가 없다는 것을 기억에 남기자. 위의 subscribe()에서 당신이 원하면 filter()를 이용하여 ifelse를 없앨수도 있다. (구체적인 구현은 여러분의 연습 과제로 남기겠다) Rx에서는 map, filter, scan, merge, combineLatest, startWith 등과 같이 이벤트 주도 프로그램의 흐름을 컨트롤하는 많은 스트림 함수를 가지고 있다. 이 함수 툴셋은 작은 코드로 더 강한 영향력을 제공할 것이다.

다음으로 해야할 것은 무엇인가
만약 Rx*가 당신의 리액티브 프로그래밍의 주 라이브러리가 된다면, Observable을 변형, 결합, 생성하기위한 많은 함수들을 익힐 필요가 있다. 이 함수들을 스트림 다이어그램으로 이해하고 싶으면 RxJava's very useful documentation with marble diagrams에 들어가 보아라. 당신이 뭔가 시도하다가 문제가 생기면 다이어그램을 그려 생각해보고 그 많은 함수들을 본 다음 다시 생각해 보아라. 내 경험에는 이 방법이 가장 효율적이었다.

한번 Rx와의 프로그래밍을 파악하기 시작하면 Cold vs Hot Observable 개념을 반드시 이해해야한다. 그렇지 않으면 당신을 계속 괴롭힐 것이다. 여러분은 경고받아왔다. 함수형 프로그래밍을 배우고 Rx에 영향을 미치는 사이드 이팩트같은 이슈를 익히면서 당신의 기술을 더 갈고 닦아라.

하지만 리액티브 프로그래밍이 단지 Rx만 있는게 아니다. Bacon.js라는 것이 있는데, 이것은 가끔 Rx에서 마주칠 수 있는 관습(quirks)없이 직관적으로 작업한다. Elm언어는 그 자신만의 카테고리 안에 있다. 이 언어는 자바스크립트+css+HTML로 컴파일하고 타임 트레블링 디버거(time travelling debugger)기능이 있는 함수형 리액티브 프로그래밍 언어이다. 꽤 멋지다.

Rx는 이벤트가 중심인 프론트엔드와 앱에서 잘 동작한다. 그러나 이게 클라이언트단에서만 그런 것이 아니라 백엔드와 데이터베이스 접근에도 잘 동작한다. 사실 RxJava는 Netflix API에서 서버단 동시성을 가능하게 해주는 핵심 요소이다. Rx는 특정 타입의 앱이나 언어에 종속된 프레임워크가 아니다. Rx는 이벤트 주도 소프트웨어를 사용할 수 있게 해주는 패러다임이다.

이 튜토리얼이 마음에 들었다면 트윗해 달라. 



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

,

내 이름은 Eugene Obrezkov이고 오늘 나는 NodeJS라는 무시무시한 플랫폼에대해 이야기 해보려고 한다. "NodeJS는 어떻게 동작하는가?"라는 아주 복잡한 질문에 대한 답을 해나갈 것이다.

나는 마치 NodeJS가 존재하지 않는다고 생각하고 글을 써내려 갈 것이다. 이렇게하면 그 내부가 어떻게 동작하는지 이해하기 쉬워진다.

여기서 사용한 코드는 실제 NodeJS에서 발췌한 코드이며 이 글을 읽고나면 NodeJS에대해 더 편안합을 느낄 수 있을 것이다.


이렇게 하려면 무엇이 필요한가?
위 이야기를 보면 막상 "이렇게 하려면 무엇이 필요한가?"라는 질문이 먼저 떠오를 것이다.

Vyacheslav Egorov는 이렇게 말했다: "JS VM은 자바스크립트 소스를 0과 1로 만드는 미지의 블랙박스라 생각하고, 더 많은 사람들이 그것을 그만 들여다본다." 이 발상은 NodeJS에도 적용시켜보았다. "NodeJS는 저수준 API를 돌아가게 해주는 미지의 블랙박스라 생각하고, 더 많은 사람들이 그것을 그만 들여다본다."

그냥 해보자!
2009년으로 돌아가면 NodeJS가 만들어지기 시작하는 시점이다.

우리는 백엔드에서 자바스크립트를 실행시키고 싶었고, 자바스크립트가 저수준 API에 접근하고 싶었다. 또한 우리의 자바스크립트를 CLI와 REPL로부터 실행시키고 싶었다. 사실 자바스크립트로 뭐든 다 하고싶었던 것이다!

이게 어떻게 가능할까? 내 머릿속에 처음 드는 생각은 브라우저였다.

브라우저
브라우저는 자바스크립트를 실행시킬 수 있다. 따라서 브라우저를 우리 앱에 합쳐 자바스크립트를 동작시키게 할까?

설마! 아래에 반드시 답해야할 질문들을 보자.

브라우저가 저수준 API를 자바스크립트로 만들까? — 아니다!
다른 곳에서 자바스크립트를 돌릴 수 있을까? — 반반이다. 좀 더 복잡한 이야기이다.
브라우저가 제공하는 모든 DOM 기능이 필요할까? — 아니다!
브라우저가 조금이라도 필요할까? — 아니다!

브라우저는 필요 없다. 자바스크립트는 브라우저 없이 실행된다.

그럼 브라우저가 자바스크립트를 실행시키는게 아니라면 무엇이 자바스크립트를 실행할까?

가상머신(Virtual Machine-VM)
가상머신(VM)이 자바스크립트를 실행한다!

VM은 고수준 프로그래밍 언어라는 추상화를 제공한다.(시스템의 저수분 ISA 추상화와 비교해보아라)

VM은 추상화이고 플랫폼에 독립적인 프로그램 실행환경을 만들기 위해 단일 컴퓨터에서 동작하도록 설계되었다.

가상머신에는 Google의 V8, 마이크로소프트의 Chakra, 모질라의 SpiderMonkey, Apple의 JavaScriptCore 등 수많은 것들이 있다. 여기서 신중하게 고르지 않으면 남은 인생동안 분명 후회하며 살 것이다.

나는 V8을 선택하려 한다. 왜일까? 그 이유는 다른 VM보다 더 빠르기 때문이다. 당신도 아마 백엔드에서 실행속도는 중요하다는 것에 동의할 것이다.

이제 V8을 살펴보면서 이것이 NodeJS 구성에 어떤 도움을 줄 수 있는지 살펴보자.

V8 VM
V8은 어떤 C++ 프로젝트와도 합칠 수 있다. 단지 간단한 라이브러리처럼 V8 소스를 거기에 인클루드(include)시키면 된다. 이렇게만 하면 이제 자바스크립트 코드를 컴파일하고 실행할 수 있다.

V8은 C++을 자바스크립트에서 사용할 수 있게 해준다. 자바스크립트로 저수준 API를 사용할 수 있게 해주는 중요한 역할을 한다.

위 두가지 포인트가 "어떻게 자바스크립트로 저수준 API에 접근할 수 있는지"에대한 대략적인 구현을 생각해볼 수 있을것이다.

다음 챕터부터는 C++ 코드로 설명을 시작할 것이기 때문에 위의 것들을 한데모아 생각해보자. 가상머신을 선택하고(우리의 경우 V8을 선택했다) -> 우리 C++ 프로젝트에 그것을 통합시킨뒤 -> V8이 C++을 자바스크립트로 사용할 수 있게 한다.

그러나 어떻게 C++코드로 작성하고 그것이 자바스크립트에서 가능할까?

V8 템플릿
V8 템플릿들을 통해 가능하다!

한 템플릿은 자바스크립트에서의 함수와 객체를 위한 청사진이다. C++ 함수와 데이터 구조체를 자바스크립트로 감싸기 위해 템플릿을 사용한다.

예를 들어보자. 구글 크롬은 C++ DOM 노드를 자바스크립트 객체로 감싸고 전역에 함수를 만들어 두기 위해 템플릿을 사용한다.

여러분도 템플릿들을 만들어 그것을 사용할 수 있다. 따라서 당신이 원하는 만큼 템플릿을 만들면 된다.

그리고 V8은 두가지 타입의 템플릿을 가지고 있다: 함수 템플릿(Function Templates)객체 템플릿(Object Templates)이다.

함수 템플릿은 한 함수의 청사진이다. 당신이 자바스크립트 함수로 만들고 싶은 컨텍스트에서 템플릿의 GetFunction 메소드를 호출하여 자바스크립트 인스턴스의 템플릿을 만들어 낼 수 있다. 자바스크립트 함수 인스턴스가 호출될 때 함수 템플릿이 호출되는데, 이 함수 템플릿과 C++ 콜백을 연관시킬 수도 있다.

객체 템플릿은 객체 초기화에서 함수 템플릿과 함께 만들어진 객체를 구성하는데 익숙하다. 객체 템플릿은 두가지 C++콜백 타입과 연관이 가능하다: 접근자 콜백(accessor callback)인터셉터 콜백(intercepter callback). 접근자 콜백은 특정 객체 프로퍼티가 스크립트에의해 접근될 때 호출된다. 인터럽트 콜백은 어떤 객체 프로퍼티라도 스크립트가 접근하면 호출된다. 즉 C++ 객체/구조체를 자바스크립트 객체로 감쌀 수 있다.

간단한 예제를 살펴보자. 이것은 C++ 메소드인 LogCallback을 전역 자바스크립트 컨텍스트에서 사용하는 것이다.

두번째 라인에서 새 ObjectTemplate를 생성한다. 그리고 3번째 라인에 FunctionTemplate를 생성하고 그것을 LogCallback이라는 C++ 메소드와 연관시킨다. 그 다음 이 FunctionTemplateObjectTemplate에 세팅한다. 새 컨텍스트에서 자바스크립트를 실행시킬때 전역에서 log 메소드를 사용하도록 이 새로운 컨텍스트에 ObjectTemplate 인스턴스를 보낸다. 결과적으로 FunctionTemplate 인스턴스와 연관된 LogCallback C++ 메소드가 트리거 될 것이다.(As a result, C++ method, associated with our FunctionTemplate instance, LogCallback, will be triggered.)

여러분도 보았듯 C++에서는 자바스크립트에서 객체를 정의하는 것과 비슷하다.

그러나 이제 C++ 메소드를 어떻게 자바스크립트에 드러낼 것인지 배울 것이다. 방금처럼 수정된 컨텍스트에서 어떻게 자바스크립트 코드를 실행시키는지 살펴볼 것이다. 사실 단순하다. 그냥 요소를 컴파일하고 실행시키자.

V8 컴파일 && 자바스크립트 실행
우리가 만든 컨텍스트에서 자바스크립트를 실행하려면 V8에 컴파일(compile)과 실행(run)의 간단한 API만 호출하면 된다.

새 컨텍스트를 만들면서 자바스크립트를 실행시키는 이 예제를 살펴보자.

2번째줄을 보면 새로운 자바스크립트 컨텍스트를 만들었다(위에서 설명한 템플릿들과 함께 수정할 수 있다). 5번째줄은 자바스크립트 코드를 컴파일하고 실행하기 위해 컨텍스트를 활성화 시킨다. 8번째줄은 자바스크립트 소스로부터 새 문자열을 생성한다. 위처럼 하드코딩 할 수도 있고 파일을 읽거나 다른 방법으로도 가능하다. 11번째줄은 우리의 자바스크립트 소스를 컴파일한다. 14번째줄은 실제 실행시켜보고 결과를 기다린다. 이게 다다.

마침내 위에서 말한 테크닉들을 조합하여 우리는 간단한 NodeJS를 만들 수 있다.

C++ -> V8템플릿 -> 자바스크립트를 실행 -> ?
V8 머신을 만든다 -> 원하는 만큼 C++ 콜백과 연관된 FunctionTemplate를 만든다 -> ObjectTemplate 인스턴스를 만들고 그것에 만들어놓은 FunctionTemplate 인스턴스를 할당한다 -> 우리의 ObjectTemplate 인스턴스를 전역 객체로 한 자바스크립트 컨텍스트를 만든다 -> 이 컨텍스트에서 자바스크립트를 실행한다 -> NodeJS. 여기까지이다!

그러나 챕터 타이틀에 물음표부분의 "자바스크립트를 실행" 이후에는 무엇이 올까? 위 구현에는 약간의 문제가 있다. 굉장히 중요한 점을 하나 지나쳤다.

fs, http, crypto등과 함께 동작하는 수많은 C++ 메소드(약 10K SLOC)를 작성했다고 생각해보자. 우리는 [C++ 콜백들]을 ObjectTemplate에 할당하고 [FunctionTemplate]를 ObjectTemplate에 불러온다. 이 ObjectTemplate 자바스크립트 인스턴스를 쓰면 자바스크립트 전역에서 모든 FunctionTemplate 인스턴스에 접근할 수 있게 된다. 그리고 모든 것이 잘 동작하는 것처럼 보일 것이나..

당장에 fs이 필요없다면? crypto의 모든 기능이 필요 없다면? 전역에 모듈을 죄다 불러오지 않고 그들이 요구하는 것만 불러준다면 어떨까? 모든 C++ 콜백이 한 파일에 긴 C++ 코드를 작성하지 않는 것은 어떨까? 그래서 위 타이틀에서 "?"의 의미는..

모듈화이다!

각 C++ 모듈이 각 fs, http 나 다른 기능에 일치하기 때문에 이 모든 C++ 메소드는 모듈로 쪼개지고 각 다른 파일에 위치한다.(이 점이 개발을 쉽게 만들어준다) 자바스크립트 컨텍스트도 같은 로직이다. 전역에서 반드시 모든 자바스크립트 모듈에 접근할 수 있는게 아니라 요구에 따라 접근이 가능하다.

위의 방법을 기반으로 우리만의 모듈로더(module loader)를 구현해야 한다. C++ 코드에서 요구하는 데로 모듈을 가져오고 자바스크립트 컨텍스트도 마찬가지로 그러기 위해 모듈로더는 C++ 모듈과 자바스크립트 모듈을 불러오는 역할을 한다.

먼저 C++ 모듈부터 시작해보자.

C++ 모듈로더
이제 좀 많은 C++코드가 있을텐데, 마음 단단히 먹고 시작해보자 :)

모든 모듈로더의 기초부터 이야기 해보자면, 각 모듈 로더는 모든 모듈(혹은 그것을 어떻게 얻어내는지에대한 정보)을 담고 있는 변수를 반드시 가지고 있어야한다. C++ 모듈에 대한 정보를 가지고 있는 C++ 구조체를 정의하고 이 이름을 node_module이라 하자.

우리는 이 구조체 안에 현재 존재하는 모듈에 대한 정보를 담았다. 결과적으로 사용가능한 모든 C++ 모듈의 딕셔너리를 가진다.

위 구조체의 각 필드를 다 설명하진 않겠지만, 그중 몇개만 이야기 하고 싶다. nm_file에서는 어디서부터 그 모듈을 불러오는지 파일 이름을 저장하고 있다. nm_register_func와 nm_context_register_func에는 모듈이 필요할 때 호출하는 함수들을 담고있다. 이 함수들은 Template 인스턴스로 만들어질 것이다. 그리고 nm_modulename은 모듈 이름을 저장한다 (파일 이름이 아니다) .

다음으로 이 구조체를 도와주는 helper 메소드들이 필요하다. 우리 node_module 구조체에 정보를 저장할 수 있는 간단한 메소드를 만든 뒤 우리의 모듈 정의에서 이 메소드를 사용할 수 있다. 이것을 node_module_register라 부르자.

위에서 보이듯 여기서 하는 일이라곤 모듈 정보를 node_module 구조체에 저장하는 일 뿐이다.

이제 메크로를 이용해 이 과정을 간단하게 만들 수 있다. 당신의 C++ 모듈 안에 메크로를 정의하자. 이 메크로는 단지 node_module_register 메소드를 감싸는 용도이다.

첫번째 메크로는 node_module_register 메소드를 감싸는 메크로이다. 다른 하나는 첫번째 메크로에 미리 정의된 인자를 박아둔 메크로이다. 결론적으로 modnameregfunc라는 두 인자를 받는 메크로가 완성되었다. 이 메크로를 호출하면 새 모듈 정보를 우리의 node_module 구조체에 저장한다. 그럼 modnameregfunc는 무엇일까? 음.. modnamefs처럼 모듈의 이름을 뜻한다. regfunc는 이전에도 우리가 이야기한 모듈 메소드이다. 이 메소드는 V8 Template 초기화와 ObjectTemplate에 이것을 할당하는 역할을 한다.

앞에서 보았듯 각각의 C++ 모듈은 모듈 이름(modname)과 초기화 함수(regfunc)를 인자로 받는 메크로로 정의할 수 있는데, 이 메크로는 모듈이 필요할 때 호출되는 것이다. 우리에게 필요한 것은 단지 node_module  구조체로부터 정보를 읽고 regfunc 메소드를 호출할 수 있는 C++ 메소드를 만드는 것이다.

node_module 구조체에서 이름으로 모듈을 검색할 수 있는 간단한 메소드를 만들어보자. 이 메소드를 get_builtin_module이라 부를 것이다.

이 메소드는 nm_modnamenode_module 구조체의 이름이 일치하면 앞서 정의된 모듈을 반환한다.

node_module 구조체의 정보를 기반으로 C++ 모듈을 불러오고 우리의 ObjectTemplate에 V8 Template 인스턴스를 할당하는 간단한 메소드를 만들 수 있다. 그러면 이 ObjectTemplate는 자바스크립트 인스턴스에 따라 자바스크립트 컨텍스트에 보내질 것이다.

위 코드에 관해 몇가지 짚고 넘어가자면, Binding은 모듈 이름을 인자로 받는다. 이 인자는 당신이 메크로를 통해 주는 모듈 이름이다. 그리고 우리는 get_builtin_module 메소드에서 나온 이 모듈에 관한 정보를 찾아온다. 만약 찾으면 exports와 같은 유용한 인자들을 보내면서 이 모듈의 초기화 함수를 호출한다. exportsObjectTemplate 인스턴스이므로 exports에서 V8 Template API를 사용할 수 있다. 이 모든 동작이 끝나고 Binding ㅁ[소드의 결과물로 나온 exports 객체를 받는다. 여러분이 기억하는데로 ObjectTemplate 인스턴스는 자바스크립트 인스턴스와 Binding이 한 것을 반환할 수 있다.

마지막으로 해야할 것은 이 메소드를 자바스크립트 컨텍스트에서 사용할 수 있게 하는 것이다. 이것은 마지막 라인이 하는 일인데, FunctionTemplate에서 Binding 메소드를 감싸고 전역변수 process에 할당하는 일을 한다.

이 단계에서 process.binding('fs')를 호출하여 네이티브 바인딩을 할 수 있다.

단순함을 위해 로직을 뺀, 내장된 모듈의 예시이다.


위 코드는 process.binding('V8')를 호출하여 자바스크립트 컨텍스트로부터 이 자바스크립트 객체를 얻어내려고 자바스크립트 객체를 내보내는 "V8"이라는 바인딩을 만든다.

고맙게도 여러분들은 아직 잘 따라오고 있다.

이제 우리는 require('fs')와같이 깔끔하게 되도록 도와주는 자바스크립트 모듈 로더를 만들 것이다.

자바스크립트 모듈로더
좋다. 마지막 개선(improvements)에 감사하다. 우리는 process.binding()을 호출하고 자바스크립트 컨텍스트로부터 C++ 바인딩에 접근할 수 있게 되었다. 그러나 아직 자바스크립트 모듈에 관한 이슈는 해결된 바가 없다. 어떻게 자바스크립트 모듈을 작성하고 필요할 때 그것을 require 할 수 있을까?

먼저 모듈에는 두가지 타입이 있음을 이해해야한다. 그 중 하나는 C++ 콜백과 함께 작성된 자바스크립트 모듈이고 NodeJS에 내장된 fs, http 등과 같은 모듈들이다. 이 모듈을 NativeModule이라 부르자. 다른 모듈은 여러분이 작업하는 디렉토리 안에 있는 모듈이다. 이것을 그냥 Module이라 하자.

우리는 두가지 타입 모두 require 할 수 있어야한다. 이 말은 NodeJS로부터 NativeModule을 부르는 법과 작업 디렉토리에서 Module을 부르는 법을 알아야한다는 뜻이다.

NativeModule부터 먼저 이야기해보자.

자바스크립트에 있는 모든 NativeModule은 C++ 프로젝트로 다른 폴더에 위치한다. 이 말은 모든 자바스크립트 소스가 컴파일 시간을 가진다는 뜻이다. 이렇게하여 우리가 나중에 사용할 자바스크립트 소스를 C++ 헤더파일에 감싸 넣을 수 있게 한다.

이것을 위한 js2c.py(tools/js2c.py에 위치한)이라는 파이썬 툴이 있다. 이것은 자바스크립트 코드로 감싸진 node_natives.h 헤더파일을 생성한다. node_natives.h는 C++에서 자바스크립트 소스를 얻어내는 어떠한 C++ 코드에서도 포함될 수 있다.

이제 C++ 컨텍스트에서 자바스크립트 소스를 사용할 수 있다. — 한번 시도해보자. node_natives.h에서 자바스크립트 소스를 가져오고 그것을 ObjectTemplate 인스턴스에 할당하는 DefineJavaScript 메소드를 구현했다.

위 코드에서, 우리는 각 네이티브 자바스크립트 모듈들을 통해 돌면서 ObjectTemplate 인스턴스에 키로서 모듈이름을, 값으로서 모듈 자체를 넣었다. 마지막으로 우리가 할 일은 타겟으로 ObjectTemplate 인스턴스와 함께 DefineJavaScript를 호출하는 것이다.

이때 Binding 메소드가 유용하다. C++의 Binding 구현(C++ 모듈로더 부분)을 보면 하드코딩된 constantsnatives라는 두 바인딩이 있을 것이다. 그러므로 바인딩 이름이 natives이면 environmentexports 객체가 DefineJavaScript 메소드와 함께 호출될 것이다. 결과적으로 자바스크립트 NativeModuleprocess.binding('natives')가 호출될 때 반환될 것이다.

그래 좋다. 그러나 node.gyp 파일에서 GYP 작업을 정의하고 이것으로부터 js2c.py 툴을 호출하여 또다른 개선을 할 수 있다. 이것은 NodeJS가 컴파일 될 때 자바스크립트  소스가 node_natives.h 헤더파일로 감싸지기 때문에 이 개선을 만들 것이다.

이제부터 우리는 process.binding('natives')로 사용할 수 있는 NativeModule의 자바스크립트 소스들을 가지고 있을 수 있다. 이제 NativeModule을 위한 간단한 자바스크립트 껍데기를 만들어보자.

이제 모듈을 불러오기 위해 NativeModule.require() 안에 모듈 이름을 넣어 호출한다. 먼저 모듈이 이미 캐시에 있는지 확인한다. 있으면 캐시에서 꺼네오고 그렇지 않으면 모듈을 컴파일하고 캐시에 넣은 뒤 exports 객체로 반환한다.

이제 좀 더 아까이서 cachecompile 메소드를 살펴보자.

cache가 하는 일은 NativeModule에 위치한 스태틱 객체 _cacheNativeModule 인스턴스를 세팅한다.

compile 메소드가 더 흥미롭다. 먼저 (process.bind('natives')로 세팅해둔 이 스태틱 프로퍼티)_source에서 필요한 모듈의 소스를 꺼낸다. 그리고 wrap 메소드로 그 소스를 감싼다. 위 소스에서 볼 수 있듯 함수의 결과물은 exports, require, module, __filename, __dirname 인자를 받는다. 그 후 필요한 인자와 함께 이것을 호출한다. 결과적으로 NativeModule.exports를 가리키는 exports, NativeModule.require를 가리키는 requireNativeModule 그 자체를 가리키는 module, 현재 파일 이름의 문자열인 __filename을 가지는 영역에서 우리 자바스크립트 모듈이 감싸진다. 이제 여러분은 module이나 require이 자바스크립트 코드 어디에서부터 오는지 안다. 그것들은 단지 NativeModule 인스턴스를 가리키고 있다. 😃

다른 하나는 Module 로더 구현이다.

Module 로더의 구현은 기본적으로 NativeModule과 같다. 그러나 다른 점은 소스가 node_natives.h 헤더파일로부터 오는 것이 아니라 우리가 fs 네이티브 모듈이라 부르는 파일로부터 온다. 따라서 wrap, cache, compile 하는 일은 같지만 파일로부터 읽는 소스만 다르다.

좋다, 이제 우리는 어떻게 네이티브 모듈이나 여러분의 작업 디렉토리로부터 모듈을 요청하는지 안다.

마지막으로, 우리는 위의 일들을 사용하여 만든 NodeJS 환경을 실행(run)하고 준비(prepare)할 때 마다 실행되는 간단한 자바스크립트 모듈을 작성할 수 있다.

NodeJS 런타임 라이브러리?
런타임 라이브러리가 무엇일까? 이 라이브러리는 전역변수인 process, console, Buffer등을 세팅하여 코딩할 수 있는 환경을 갖추고, NodeJS CLI에 인자로 보내는 메인 스크립트를 실행한다. 이 라이브러리는 NodeJS 런타임시 모든 자바스크립트 코드가 실행되기 전에 실행되는 간단한 자바스크립트 파일로 아키브(achieve)될 수 있다.

첫번째 단계로는 전역에 모든 네이티브 모듈을 프록싱(proxying)하고 다른 전역 변수를 설정하는 것이다. 이 일은 단지 global.Buffer = NativeModule.require('buffer')global.process = process와 같은 일들이다.

두번째 단계는 NodeJS CLI에 인자로 보내는 메인 스크립트를 실행한다. 로직은 간단하다. process.argv[1]를 파싱하여 그 값을 객체 초기화때 값으로 Module 인스턴스를 생성한다. 따라서 Module은 파일로부터 소스를 읽을 수 있고 -> NativeModule이 했던 것처럼 미리 컴파일된 자바스크립트 소스로 캐시와 컴파일 해 둘 수 있다.

여기에 내가 더 추가할 수 있는게 없다. 굉장히 매우 간단하며, 만약 그대로 더 세부적인 것을 원한다면 노드 저장소의 src/node.js 파일을 한번 확인해보아라. 이 파일은 NodeJS 런타임시 실행되고 이 글에서 말한 모든 테크닉을 사용한다.

이것이 바로 NodeJS가 어떻게 당신의 자바스크립트 코드에서 저수분 API에 접근하며 실행하는지에관한 이야기였다. 멋지지 않는가?

하지만 아직 비동기 처리에대한 문제가 남았다. 여기서는 fs.readFile()같은 연산은 완전히 순차적으로 실행된다.

비동기 처리를 위해 어떤 것이 필요할까? 이벤트 루프이다.

이벤트 루프
이벤트 루프는 프로그램 내에서 이벤트나 메시지를 기다리고 디스패치(dispatch)하는 메시지 디스패처이다. 이것은 내부/외부 이벤트 프로바이더(보통 이벤트가 도착하기 전까지 요청을 블락시키는 것)에 요청을 만듦으로서 작업한다. 그리고 적절한 이벤트 핸들러를 호출한다(이벤트를 디스패처 한다). 선택되거나 채택될 수 있는 파일 인터페이스를 따르는 이벤트 프로바이더라면, 이벤트 루프는 리엑터(reactor)와 협력하여 사용할 수 있다. 이벤트 루프는 항상 메시지 제공자와 함께 비동기로 처리한다.

V8 환경을 생성하면 V8은 인자로 이벤트 루프를 받을 수 있다. 그러나 V8에 이벤트 루프를 세팅하기 전에 먼저 그것을 구현해놓아야한다.

이제 libuv라 불리는 그 구현을 이미 가지고 있다치자. libuv는 파일 읽기와 같은 모든 비동기 처리의 책임을 가지고 있다. libuv가 없는 NodeJS는 단지 순차적으로 자바스크립트/C++를 실행할 수 있는 도구일 뿐이다.

따라서 기본적으로 NodeJS에 libuv 소스를 인클루드 할 수 있고, 거기에 있는 libuv 기본 이벤트 루프와 함께 V8 환경을 만들 수 있다. 여기에 그 구현이 있다.

CreateEnviroment 메소드는 루프 인자로 libuv 이벤트 루프를 받는다. 우리는 V8 네임스페이스에서 Enviroment::New를 호출할 수 있고 libuv 이벤트 루프를 보낸 다음 V8 환경으로 구성할 수 있다. 여기까지가 NodeJS를 어떻게 비동기 처리 할 수 있는지에 관한 이야기였다.

libuv에대해 더 이야기하고 어떻게 동작하는지 말해주고 싶지만, 이 이야기는 다음으로 미루도록 하자. :)

Thanks!
이 글을 끝까지 읽어준 모든이에게 감사하다. 여러분이 여기까지 읽으면서 뭔가 즐겁게 배웠기를 바란다. 만약 뭔가 문제를 발견한다면 자유롭게 이 글에 커멘트해주면 된다. 그러면 가능한 빨리 내가 답변해주도록 하겠다.(옮긴이: 자유롭게 이 블로그에 댓글 달아주시면 됩니다!)

Eugene Obrezkov aka ghaiklor, Technical Leader at Onix-Systems, Kirovohrad, Ukraine.



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

,

Swift는 Objective-C와 같은 벤더를 가지고 있기 때문에 Objective-C의 후계자이다. 이것은 비슷하게 생기지도 않고 똑같이 동작하지도 않으며 비슷한 느낌이 들지 않는다. Objective-C에서 (메소드 이름처럼) 잘 동작했던 패러다임은 새로운 Swift 세계로 넘어오기 위해 천천히 바뀌고 있다. 예를들어 아래 메소드는 Objective-C에서 이런식의 긴 형태로 호출되었다.
[string dataUsingEncoding:NSUTF8StringEncoding];

Swift2.2에서는 다소 어색하게 바뀌었고

string.dataUsingEncoding(NSUTF8StringEncoding)

Swift3에서 이 메소드는 비로소 간결해졌다.

string.data(using: .utf8)

Swift3 버전의 메소드가 Swift에게 알맞는 것이고, 같은 의미에서 Objective-C 버전은 Objective-C 버전에 알맞는 것이다. 여러분의 메소드를 어떻게 Swift에 맞춰 사용할 수 있는지 다루는데 이 글이 도움을 줄 것이다.


우리 앱을 만들기 위해 사용하는 것들 중에 Swift하게 바꿔야하는 프레임워크나 언어적인 부분들이 있다. 오늘 나는 델리게이트 네이밍에대해 이야기하려 한다.

Swift에서 델리게이트는 Obejctive-C에서처럼 부드럽게 잘 바뀌지 않는다. Objective-C는 "sender"와 "receiver"에대해 굉장히 친숙하다. 애플의 Objective-C 문서에서는 이 용어를 자주 사용한다. 예를들어 UIResponer에있는 isFirstResponder 메소드 문서를 확인해 보아라.

receiver가 첫 응답자(first responder)인지 나타내는 불리언 값을 반환한다.
- (void)buttonTapped:(UIButton *)sender { }
델리게이트는 비슷한 방식으로 동작한다:: Objective-C에서 델리게이트 메소드의 첫 인자도 항상 sender였다. 이것이 왜 유용할까? 만약 receiver가 같은 타임의 여러 오브젝트 델리게이트라면 그것들을 구분해야할 것이다. 델리게이트는 첫번째 인자를 제공하면서 그것을 구분할 수 있게 해준다.

나는 Backchannel SDK 예제로 몇 클래스 이름을 간단히 해볼 것이다.

여기엔 두가지 타입의 델리게이트 메소드가 있는데, 첫번째는 이벤트가 일어났는지 나타낸다.
- (void)messageFormDidTapCancel:(BAKMessageForm *)messageForm;
Swift로 번역하면 다음과 같다.
func messageFormDidTapCancel(_ messageForm: BAKMessageForm)
이것은 더이상 Swift3에 알맞지 않다. Swift3에서는 불필요한 것(두번 나타난 messageForm)을 제거하고, 언더바를 사용해서 없에는게 아니라 자동으로 첫번째 인자 이름을 따른다.

두번째 타입의 델리게이트 메소드는 이벤트가 일어나고 거기에 몇 데이터를 가지고 있는지 나타낸다. 아래 예제를 보자.
- (void)messageForm:(BAKMessageForm *)messageForm didTapPostWithDraft:(BAKDraft *)draft;
- (void)messageForm:(BAKMessageForm *)messageForm didTapAttachment:(BAKAttachment *)attachment;
Swift3으로 번역하면 다음과 같다.
func messageForm(_ messageForm: BAKMessageForm, didTapPostWithDraft draft: BAKDraft)
func messageForm(_ messageForm: BAKMessageForm, didTapAttachment attachment: BAKAttachment)
흠, 좋지 않아 보인다. 왜 이 메소드를 둘다 messageForm이라 부르나? 또한 명사로 시작하는 메소드는 별로 좋지 않다. 보통 그 타입의 오브젝트를 반환할때 사용한다(NSStringdata(using:)를 생각해보면 Data를 반환한다). 여기서는 아무 메시지 형식 오브젝트도 반환하지 않을 것이다. 그 "메시지 형식"은 사실 첫번째 파라미터의 이름이다. 매우 혼란스러운 메소드 이름이 아닐 수 없다!

이 두가지 타입의 델리게이트 메소드는 그 줄 마지막에 "sender"를 보내고 동사를 앞으로 옮겨서 고칠 수 있다. 먼저 sender가 델리게이트를 알려주는 이벤트는 messageFormDidCancel대신 didTapCancel이다.
func didTapCancel(messageForm: BAKMessageForm)
굉장히 좋아졌다. 액션이 앞으로 오면서 메소드 이름이 되었다. 그리하여 메소드가 무슨 일을 하는지 더 명확해졌다. 내 생각엔 읽을때 좀 더 좋게 하기 위해서 파라미터 이름 대신에 전치사를 써도 괜찮을 것 같다.
func didTapCancel(on messageForm: BAKMessageForm)
이렇게 써보면서 아직까지는 전치사를 사용하기에 어색해보이는 상황을 발견하지 못했다. 그리고 다른 여러 상황에서도 "on", "for", "with", "in" 모두 유용하게 쓰인다는 것도 알아냈다. 사용자가 "on"형식을 탭 하면서 나는 "on"이 이곳에 적합하다고 생각한다.

이제 뒤에 데이터를 전달하는 델리게이트 메소드도 한번 보자. 동사를 앞으로 보내는게 도움이 될 것이고, 델리게이트 이름에 전치사로 바꾸는 것도 이런 메소드 타입을 깔끔하게 해준다. 아래 예제 대신
func messageForm(_ messageForm: BAKMessageForm, didTapPostWithDraft draft: BAKDraft)
좀 더 Swift하게 만들고
func didTapPost(with draft: BAKDraft, on messageForm: BAKMessageForm)
좀 더 바꾼다.
func didTap(attachment: BAKAttachment, on messageForm: BAKMessageForm)

이러한 규칙은 나를 빼고 누구도 보증하진 않지만 우리가 그런식으로 쓰면 현재 규칙보다 더 이해하기 쉬워질거라 생각된다. 더 나아가 아마 내 Swift 델리게이트 메소드를 이런 구조로 짜기 시작할 것이다.

UITableView의 델리게이트와 데이터소스 메소드를 보면서 이것이 나중엔 어떻게 생겼을지 생각해보자.
func numberOfSections(in tableView: UITableView) -> Int
numberOfSections는 이 스킴을 따르고 이미 꽤 좋아보인다.

그러나 아래 메소드는 그렇게 좋아 보이진 않는다.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
여기 좀 더 사랑스러운 방법이다.
func numberOfRows(inSection section: Int, in tableView: UITableView) -> Int
func cellForRow(at indexPath: IndexPath, in tableView: UITableView) -> UITableViewCell
func didSelectRow(at indexPath: IndexPath, in tableView: UITableView)

 



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

,

여러분 앱이 탭, 스와이프, 팬, 다른 인터렉션을 사용한다면 화면 뒤에서 이벤트를 사용하고 있는 것이다. 이러한 이벤트들은 잘 만들어진 과정을 거쳐 이동하며, 우리는 이것들이 어떻게 동작하는지 한번 볼 것이다. 텍스트 인풋이나 원격 제어 이벤트를 중심으로 트리키한 문제를 디버깅하면, 이것이 어떻게 동작하는지 쉽게 이해할 수 있다. 또한 이 지식을 이용하여 여러분 앱을 통해 커스텀 데이터 흐름을 만들 수도 있을 것이다.

이 글은 이벤트 전달에 관한 시리즈의 첫번째 글이다. 터치 핸들링에대해 다룰 것인데, 그냥 터치가 어덯게 여러분의 앱을 통해 받은 이벤트가 될 수 있는지, 그 이벤트를 다루기 위해 어떻게 요소들에게 기회를 줄 수 있는지 다룬다.

터치 핸들링
터치 이벤트는 iOS 앱에서 가장 주된 형태의 이벤트이다. 어떤식으로 터치를 처리하는지에대한 세부적인 내용은 우리가 매일 사용하는 API들에의해 가려져있다. 앱에서 이 이벤트들을 어떻게 분배하는지 이해하면 여러분의 앱을 어떻게 구성할지 정하는데 도움이 될 것이다. 또한 커스텀 이벤트를 전달하는데 이런 하부조직을 사용할 수도 있다.

Hit Testing
이벤트 전달에대해 이야기할 때 가장 먼저 다룰 것은 시스템이 어떻게 터치 이벤트를 다루고 어떻게 앱을 통해 이 이벤트를 받아드리는지이다. 이벤트 사용자가 화면을 탭하면 발생한다. 적절한 요소가 터치를 다루기 전에, 시스템은 어디에서 터치가 발생했는지, 누가 먼저 터치에 응답을 받을 것인지 정해야한다.

이것이 hit testing이 필요한 곳이다.

hit testing 과정과 관련이 있는 메소드에는 hitTest:withEvent:pointInside:withEvent:가 있다. hitTest:withEvent:는 hit test한 점이 그 바운드 안에 들어오는지 알아보기 위해 pointInside:withEvent:를 사용한다. 만약 바운드 안에 들어가지 않으면 nil을 반환하고 뷰 계층의 모든 브런치를 스킵한다.

테스트한 점이 바운드 안에 들어간다면 각 서브뷰마다 pointInside:withEvent:를 호출한다.  pointInside:withEvent:에서 YES를 받은 서브뷰는 hitTest:withEvent:를 호출한다. 최초의 hitTest:withEvent: 호출로부터 최종 결과는 서브뷰 중 하나이거나 자기 자신이다. 자기 자신을 반환하는 경우는 그 서브뷰가 모두 nil을 반환할 때이다.

아래의 뷰 계층을 보자.

사용자가 View E를 탭했다고 생각해보자. View A에서부터 hitTest:withEvent:로 시작한다. View A에서 pointInside:withEvent:YES면 View B나 View C의  pointInside:withEvent:를 호출한다. View B가 NO를 반환한다. View C가  YES를 반환하고 거기의 hitTest:withEvent:를 호출한다. View C도 View D와 View E로 똑같은 과정을 따른다. View D의 pointInside:withEvent:NO를 반환한다. View E는 YES를 반환하고 더이상 다른 서브뷰가 없으므로 hitTest:withEvent:에서 자기자신을 반환한다.

View D가 View C의 서브뷰라 가정하고, 사용자가 View C 바운드의 View D를 탭했다면 무슨일이 일어날까? (이것은 clipToBoundsNO로 설정되있을 때 가능하다) View A가 위 과정을 시작한다. View B와 View C 둘 다 pointInside:withEvent:NO를 반환하므로 결국 View A가 터치를 받는다.

이제 우리는 hitTest:withEvent:로부터 받은 뷰를 가지고 있다. 이 뷰는 hit-test 뷰이다. 이제 이것은 터치와 연관되있고 터치가 활성화되는 동안 터치 이벤트를 응답하는 어떤 제스처 이후에 첫번째 기회를 줄 것이다.

주어진 터치에 커스텀 구현이 따로 없으면 무슨일이 일어날까? 상황에 따라 다르다. 만약 뷰가 뷰컨트롤러에의해 관리되고 있으면 뷰컨트롤러에게 응답할 기회를 준다. 뷰컨트롤러가 응답하지 않으면 hit-test 뷰의 부모뷰가 그 응답 기회를 가져간다. 이러한 과정은 "Responder Chain"이라 불리는 방법으로서 반복에서 진행된다.

Responder Chain
Responder Chain은 chain-of-responsibility 디자인 패턴으로 구현되있다. Responder Chain의 각 요소는 UIResponder를 상속받는다. UIResponder는 여러 종류의 이벤트를 다루기 위한 메소드들을 가지고 있다. 터치 이벤트를 넘어서 UIResponder는 인풋 뷰, 모션 이벤트, 누름 이벤트, 원격 제어 이벤트를 다루기 위한 메소드들을 정의해 두었다.

여기에서 많은 이벤트 중 firstResponder가 중요하다. firstResponder는 이벤트를 다룰 수 있는 첫번째 기회가 주어진 오브젝트이다. 첫번째 응답자가 이벤트를 다루지 않으면 nextResponder에게 그 이벤트 핸들링 기회를 준다. 그리고 현재 오브젝트가 nil 일때까지 nextResponder를 반복한다. 보통 체인에서 마지막 오브젝트는 어플리케이션 델리게이트이다.

제스처
iOS에서 현재 있는 곳에 제스처 recognizer가 오기 전에, 터치 핸들링 프로세스는 어떻게 앱을 감지하고 제스처를 다룰지의 방법이다. UIGestureRecognizer가 나오면 그들이 감자한 제스처에 따라 터치를 다루고 행동을 취하도록 프로세스를 조금 바꾼다. 우리는 제스처 recognizer를 어떻게 사용하는지 살펴보지는 않겠지만 대신에 제스처를 어떻게 터치 이벤트 전달 시스템에 맞추는지 볼 것이다.

제스처는 항상 한 터치 이벤트를 다루기 위해 첫번째 기회를 얻는다. 디폴트로는, 제스처가 터치를 얻고 그 다음 뷰를 얻는다. 제스처 상태가 "recognized" 일때 차이점은 뷰에 있는 터치가 취소된다는 것이다. 여러분은 UIGestureRecognizer:, cancelsTouchesInView, delaysTouchesBegandelaysTouchesEnded에 프로퍼티로 뷰에 이 터치 이벤트를 어떻게 전달할지 정할 수 있다. 이 프로퍼티를 잘 써서 응답하지 않는 뷰에도 응답하는 것처럼 만들 수 있다.

요약
첫번째 파트는, 터치 이벤트가 어떻게 윈도우에서 뷰까지 탐색하는지 보았는데, 뷰는 responder chain을 통해 비슷한 경로로 돌아와 터치가 일어난다. 또한 제스처 recognizer가 어떻게 이 터치 시스템에 맞춰지는지 다루었다. 두번째 파트에서는 지원하는 다른 이벤트와 responder chain이 어디에 들어맞는지 이야기해볼 것이다.

설계와 개발에 대해 더 알고 싶으면 BPXL Craft를 구독하거나 Blick Pixel 트위터를 팔로우 해달라. 


관련 링크



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

,

매년 우리의 Buffer iOS 앱은 애플의 World Wide Developer Conference(WWDC) 이후에 새로운 iOS 기능과 변화를 지원하기 위해 크게 업데이트 해왔었다.

올해에 큰 변화는 없었지만, 이번 포스팅에서는 퍼포먼스와 미래에대한 대비에대해 이야기 할 것이다.

새로운 버전의 Buffer 앱의 새로운 변화
WWDC가 끝나고나면 우리는 보통 앱을 실행해보고 이전 iOS 릴리즈 버전을 최소 타켓으로 조정한다. 이번의 경우 iOS9를 최소로 조정할 예정이었으나 iOS8 기기까지 지원하게 되었다. 그리하여 앱을 정리하고 이전 호환성에 더이상 필요없는 오래된 iOS8 코드를 제거할 수 있었으며, 몇년간 기술적인 빚을 갚을 수 있었다.

Buffer iOS의 6.0버전에서 증진시키고 싶은 부분중 하나는 바로 테이블이다. 특히 나중에는 레이아웃 부분을 더 유연하게 하고 싶었다. 우리는 2011년부터 UITableView를 사용하고 있었다. 이것은 컨텐트의 목록에는 좋지만, 나중에 v6에는 컨텐트를 그리드(grid)형식으로 보여주고 싶었다.

목표 : 부드러운 스크롤링을 위해 초당 60프레임을 지원
우리는 항상 부드러운 스크롤을 위해 마법처럼 초당 60프레임을 달성하려고 노력해가며 테이블을 업데이트 했다. 몇년간 아이폰이 더욱 강력해졌음에도 이것은 매우 어려운 목표치였고, 스마트폰이 싱글 코어 디바이스일때 설계된것같은 UIKit은 아직또 메인스레드에 묶여있다.

사용자가 프레임이 떨어지는 것을 알아차리지 않으려면 모든 레이아웃과 코드 랜더링이 16ms 안에 실행되야하는데, UIKit은 이것을 개발자에게 떠넘겼다. 시스템이 오버헤드하면 가끔 10ms 정도 휘청거린다.

우리의 UI는 매우 튼튼하고 다채로우므로 이 과제는 매년 포퍼먼스 유지보수를하게 만들었다. 이 기술적인 과제는 사용자가 "로딩" 화면을 보지 않으면서 스크롤 하는동안 어떻게 부드럽게 업데이트를 서버에서 검색하는지와 연관이 있다.

우리의 셀(cell)들은 사용자가 만든 소셜미디어 업데이트의 다양한 높이의 UI를 지원하는데, 이것이 꽤 복잡하다. 그래서 올해 애플이 UICollectionView의 퍼포먼스를 올렸다는 발표를 들었을 때 우리는 매우 흥미로웠다. 테이블 스크롤링을 하면서 처리할게 많을때 우리의 기대치는 60프레임을 유지하는 것이었고, 적어도 55프레임 이상 유지하길 바랬다.

우리는 먼저 앱에서 UICollectionView를 어떻게 이용할지부터 찾아보았지만, 현재의 UITableViews와 비슷한 레이아웃을 만들어내는데 몇 가지 장애물이 있었다. 컬랙션 셀의 오토사이징에대한 새로운 추가 사항과 함께 몇 가지 문제가있었다. 우리는 iOS가 높이를 계산하고나면 스크롤이 점프해서 왔다갔다하는 모습을 보고 있었다. (v5.0에서 가능하면 미리 높이를 계산해두는 방법을 찾았었다) 더 나아가 오토 레이아웃의 편리함은 무거운 처리의 비용으로 돌아오고, 이것은 메인스레드에서 일어났다.

전략: 페이스북의 AsyncDisplayKit을 사용하기
얼마전에 아직 보진 못했지만 페이스북이 AsyncDisplayKit(ASDK)를 만들었다는 소식을 접했다. 페이스북은 그들의 제스처와 애니메이션 특징의 앱(Paper)를 위해 2014년 10월부터 ASDK를 커스텀하여 만들고 있었다. 그들은 UIView의  스레드-세이프 추상화를 통해 메인스레드에서 뷰를 랜더링하는데, 초당 60프레임이 유지되도록하여 앱이 부드럽게 동작하게 만들었다. 추가로 ASDK는 "Intelligent Preloading"이라 불리는 강력한 API를 가지는데, 이것은 서버로부터 응답이 필요한 컨텐트 "목록"을 사용자가 스크롤하는 곳에 네트워크 호출과 불러오기를 효율적인 방법으로 다룬다.


우리는 이번 기회에 이것을 확인하여 사용해보자고 마음을 먹었고, 몇 시간 안에 텍스트만 갱신하면서 스크롤이 점프해서 왔다갔다 하지 않는 v6.0을 만들었다.

AsyncDisplayKit이 셀 랜더링을 처리해줌으로서 heightForRowAtIndexPath:나 estimatedHeightForRowAtIndexPath: 구현 없이 정확한 높이 값을 받아냈다. 이 컨셉 증명을 통해 아이폰5C에서 부드러운 60프레임을 확인할 수 있었다(아이폰6+에서는 이미 더 좋은 퍼포먼스를 냈다)

AsyncDisplayKit은 UI를 만들기위해 "Nodes"를 사용하는데, 우리의 바뀐 셀이 ASCellNode가 되도록 다시 만들어야했다. 또한 다른 요소를 새로운 클래스로 추상화하여 셀의 구조를 정리하는데 시간을 썼으며, 2800줄이나 되던것을 945줄까지 줄이게 되었다. ASCellNode는 또한 컬랙션 뷰나 테이블 뷰 둘다와 함께 양립해서 사용할 수 있다는 이점도 얻을 수 있었는데, 이것이 미래의 기능을위해 리팩토링 한 후에 얻은 레이아웃 유연성이다.

ASDK의 레이아웃은 CSS Flex Box 모델을 기반으로 하는데, 이것은 현재 오토 레이아웃 코드의 시작점중 일부이기도 하다. 그러나 Inset, Overlay, Center, Stack등을 사용하여 레이아웃 기능을 꽤 쉽게 사용할 수 있다. 우리는 빠르게 바뀐 셀을 레이아웃에 구성할 수 있었으며, 아이패드에서 읽을 수 있는 레이아웃 가이드로 방향 전환과 고정된 폭 지원까지 해줌으로서 오토 레이아웃보다 더 깔끔하게 보이게 되었다.

AsyncDisplayKit의 소개때부터 핀터레스트는 페이스북과 협력하여 AsyncDisplayKit v2.0 작업을 함께 하고 있었다. 우리는 항상 페이스북과 핀터레스트 iOS 앱 둘다의 퍼포먼스에 놀랐었다. 핀터레스트 앱에서 순식간에 핀을 통해 스와이핑할때는 정말로 인상적이고, 좀처럼 로딩화면에 도달하지 않는다. 우리가 보았던 그들의 포퍼먼스 증진을 큰 인상을 받아왔는데, Buffer iOS앱의 다른 영역에서 ASDK를 사용하여 탐험할 수 있음에 흥분을 가라앉힐 수 없었다.

아래 비디오는 개발하는 중에 찍었는데, 새로 코딩한 버전의 v5.6 큐를 보여준다. 여러분은 셀 높이를 계산하는 iOS가 스크롤이 점프해서 왔다갔다하는것이 적어지면서, 건더뛰는 프레임도 줄었다는 것을 알 수 있을 것이다.

다음으로 할것 + 여러분의 생각
다른 오픈 소스 애플리케이션을 ASDK 예제와 곧 공유하여 다른 사람들이 자체 애플리케이션에서 ASDK를 사용할 수 있게 도움이 되면 좋겠다. 첫 번째 버전은 Buffer iOS 앱에서 업데이트를 재정렬하는 데 사용되는 코드이다.

프로젝트에서 ASDK를 사용해볼 기회가 있다면, 우리는 여러분의 경험을 들어보고 싶다. 부드러운 스크롤링에대한 다른 팁이나 질문이 있다면 코멘트를 달아달라. 



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

,

당신은 시뮬레이터 빌드 속도를 올리기 위해 CrashlyticsHockeyApp와같은 써드파티 크레쉬 리포팅  서비스를 사용하는가? 오늘 나는 이것이 그냥 이상한 속임수가 아님을 알려주겠다(원문: I've got just the right not-so-weird trick for you today).

이론
지난 몇년동안 빌드 시간을 단축시키는 것에 대한 많은 조언들이 있었다. 많은 글쓴이들은 Debug 구성에서 Debug Information FormatDWARF로 바꾸어라고 제안했다. 오늘날의 Xcode는 이것이 디폴트로 한다. 이 설정으로 디버그 심볼(생각: 클래스와 메소드 이름)은 바이너리에 직접 들어간다.

그 대안으로는, dSYM 파일로 DWARF하는 선택지가 있다. DWARF에 비해 두가지 장점이다.
  • 바이너리를 난독화 시킨다
  • 바이너리 크기가 작아진다

그리고 커다란 단점에는
  • 따로 분리된 dSYM 파일은 각 빌드마다 생성되기 때문에 긴 빌드시간이 걸린다.

이것은 release 구성에서 디폴트이며, 이 경우는 이해해줄 수 있다.

문제점
위에서 말한 설정은 많은 경우에 완벽하게 괜찮다. 그러나 디버거에 붙이지 않고 DWARF 구성으로 앱을 빌드하면 실패한다. 시도해보면 아래처럼 심볼화되지않은 크래쉬 리포트를 보게 될 것이다.
0   YourApp                         0x00000001001e1594 0x100058000 + 1611156
1   YourApp                         0x00000001000ed74c 0x100058000 + 612172
2   UIKit                           0x000000018f2b67b0 -[UIApplication sendAction:to:from:forEvent:] + 96
3   UIKit                           0x000000018f42a5ec -[UIBarButtonItem(UIInternal) _sendAction:withEvent:] + 168

우리는 이것은 symbolicatecrash 툴을 이용해서 손수 심볼화시켜주어야한다.

symbolicatecrash YourApp\ \ 17-10-16\ 23-15.crash YourApp.app/YourApp > symbolicated.crash

크러쉬 난 빌드를 위해서는 .ipa(혹은 .app) 파일에 접근해야할 때가 있음을 인지해라. .ipa 파일은 아카이브 하지 않고는 자동으로 생성되지 않는다.이것은 이 크래쉬 상황의 심볼화 프로세스를 만든다. 그래서 왜 Crashlytics가 각 빌드마다 dSYM 파일을 생성하도록 지향하는지의 이유이다.


(우리가 코드를  수정했다면) 디폴트 설정은 매 빌드마다 앱의 dSYM 파일을 생성하는 것을 보장하는 프로세스이다. 이것이 더 손호하는 구성이므로 개발중이나 내부 테스팅 중에 크레쉬가 나면 그 크레쉬가 난 정확한 라인을 짚어줄 것이다.

내 생각엔 기기에 직접 크레쉬를 심볼화시킬 수 있을지도 모르지만, 더이상 그렇게 할 수 없을 것 같다. 그 이유는 다음과 같다.
왜 써드파티 크레쉬 리포터들이 매 케이스마다 dSYM 파일을 필요로하는지에대한 이유일 것이다. 따라서 이제 왜 dSYM 파일이 필요한지 알았으므로 귀중한 빌드 시간을 어떻게 절약할 수 있는지 보자.

비결
dSYM 파일을 생성하든 말든 그 선택은 확실해 보인다. 모든 희망을 잃었다...

타겟 SDK에따라 Debug Information Format 값을 다르게 설정할 수 있게 되었다!


이 설정이 우리에게 주는 것은:
  • 시뮬레이터 빌드 속도가 더 빨라진다(우리는 거의 항상 연결되있는 디버거를 쓰므로 이제 크레쉬 리포트는 필요하지 않다)
  • 기기 빌드에대해 모든 크레쉬 리포트(디버거 외부에서 동작할 확율이 높은)

요약

우리는 매우 쉬운 방법으로 시뮬레이터 빌드 시간을 단축시켰다. 내 경우는 점점 증가하는 빌드(incremental build)에서 1.5초정도 떨어졌다(전체 빌드 시간의 15%). 큰 차이가 아닌것 같아 보여도, 몰입의 순간에는 매 초가 중요할 것이다. 



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

,

때때로 앱은 크레쉬가 난다. 크레쉬는 사용자의 워크플로우를 방해하는데, 데이터가 손실되거나 백그라운드에서 앱 오퍼레이션이 어떠한 손상을 주기도 한다. 개발자에게는 해결하기 어려운 크레쉬는 다시 재현하기 힘들거나 찾아내기 힘든 것들일 것이다.

나는 최근에 Castro에서 발견하기 힘든 많은 크레쉬들 때문에 생긴 버그를 찾아다가 고쳤는데, 이 이야기를 공유하면서, 여러분이 경험하는 비슷한 이슈에대해 도움이 되길 바란다.

Oisin과 나는 9월에 Castro2.1을 배포했다. 그리고 곧 아이튠즈 커넥트를 통해 Castro 크레쉬가 높게 치솟는다는 보고를 받았다.

2.1버전 이후 Crastro2 크레쉬 리포트를 보여주는 차트2.1버전 이후 Crastro2 크레쉬 리포트를 보여주는 차트


아이튠즈 커넥트 크레쉬 리포트
흥미롭게도 이 크레쉬들은 우리가 주로 쓰는 크레쉬 리포팅 서비스(HockeyApp)에 나타나지 않았다. 따라서 실제로 그 문제를 발견하는데까지 시간이 좀 걸렸다. 또한 모든 크레쉬를 알아내기 위해 개발자들은 아이튜즈 커넥트나 Xcode를 통해 크레쉬 리포트를 확인했다. (업데이트: "써트파티 크레쉬 리포터는 기록을 남기기위해 in-process 핸들러를 사용한다. 그러나 OS가 밖에서 여러분 앱을 죽이면 핸들러는 절대 동작하지 않을 것이다"고 Greg Parker가 알려주었다. 추가적으로 HockeyApp의 공동 창시자인 Andreas Linde가 한 아티클을 소개해주었는데, 글은 HockeyApp이 어떤 종류의 크레쉬를 찾아내고, 찾아내지 못하는지 설명해주는 글이었다.)

여러분이 만약 앱 개발자이고 여러분의 계정으로 로그인 했다면 Xcode가 애플에의해 사용자로부터 모은 크레쉬 리포트를 검토할 수 있게 해준다. 이 기능은 Organizer 윈도우의 크레쉬 탭에서 찾을 수 있다. 앱 버전을 고를 수 있고, 개발자와 정보를 교환하고자 동의했던 사용자로부터 애플이 모은 크레쉬 리포트를 다운받을 것이다.

Xcode에서 이 부분을 사용할 때 크레쉬가 나는 경향이 있다는 것을 발견했다. 특히 크레쉬 리포트에서 쓰레드 방향의 디스클로저를 열고 닫는 토글링시 크레쉬가 나타났다. 쉽게 하는 방법은 리스트 한편에 크레쉬를 오른쪽 마우스로 클릭하고, "Show in Finder"를 선택한다. 나타난 번들을 더 자세히 보면 일반 텍스트 파일로 크레쉬 리포트를 볼 수 있다.

크레쉬 조사하기
크레쉬는 원래 코드 경로에의해 발생되지만, 항상 데이터베이스 쿼리 실행 메소드에서 끝난다.

먼저 나는 스레드 이슈가 아닐까 의심했었다. 지난 몇년동안 스레드 버그러부터 나온 고통을 많이 겪었기 때문에, 나는 항상 그 고통을 먼저 떠올린다. 텍스트 파일로 된 크레쉬 리포트를 열어보니 Xcode가 보여준 것보다 훨씬 세부적인 내용을 볼 수 있었다. 예외타입은 EXC_CRASH (SIGKILL)이고, 노트(note)는 EXC_CORPSE_NOTIFY이며, 종료 원인은 Code 0xdead10cc였다. 나는 0xdead10cc가 무엇을 의미하는지 찾아보았다. 구글과 애플 개발자 포럼에는 이것에대한 이야기가 그리 많지는 않았지만 Technical Note 2151에서 이야기 해주었다.

0xdead10cc 예외코드는 백그라운드에서 돌고있는 앱을 시스템 자원의 이유로 iOS에의해 종료시킨 경우를 말한다.

여기서 내가 알아낸 점은 iOS가 앱을 종료시킬때 우리 코드상에서 뭔가 실수때문만은 아니며 정책 침해 때문일 수도 있다는 것이다. 그러나 Castro는 Adreess Book database를 사용하지도 않고 시스템 리소스와 비교가능한 어떤것을 사용하지 않는다고 생각했다. 나는 앱이 백그라운드에서 너무 오래 동작해버리면 어떻게할지 고민했는데, 우리가 받는 크레쉬 리포트의 몇 경우는 고작 2초정도 동작하고 말았다.

나는 데이터베이스에서 일어나는 크레쉬를 쫒아 수많은 stack trace pointing으로부터 우리의 데이터베이스 SQLite 파일에 문제가 있음을 결국 찾아냈다. 그러나 왜 이러한 크레쉬가 2.1버전이 되서야 나타났을까?

공유된 앱 컨테이너
Castro의 2.1 릴리즈에서 최근 재생한 에피소드를 쉽게 공유하기 위한 기능인 iMessage 앱을 지원했다. 메시지 앱이 데이터베이스에 접근하는 과정에 우리는 공유된 앱 컨테이너로 데이터베이스를 옮겼다.

파일이 공유된 공간에 있으면 그 팔일이 락이 걸리는 정책을 가진다면 어떨지 고민해보았다. 아마 iOS가 앱을 멈출때 다른 프로세스가 그 파일을 사용할 수 있는지 체크하고, 만약 그렇다면 iOS는 그 앱을 종료시킨다. 이것은 무리없이 있을법한 이야기이다.

어떻게 크레쉬를 낼까
크레쉬를 고치기위해 크레쉬를 재현해보는 것은 프로그래머에게 좋은 습관이다. 그 크레쉬를 흉내내기 위해 어느 부분을 임시로 다시 짤 수 있다. 만약 크레쉬가 확실히 일어나게 해보았으면, 이런 의심을 확인해보는것과 어느정도 거리가 있으며 우리에게 이 잼제적인 문제를 테스트 할 수 있는 기회를 제공한다. 다른 대안으로는, 무턱대고 일단 고친다음 배포하여 크레쉬 리포트를 받아보는 것이다. 이떨때는 정말 이 방법밖에 없을 수도 있는데, 너무 장황한 프로세스이며 여전히 사용자에게 크레쉬를 만들고 있다.

특정 어떤 크레쉬는 다시 만들어보기 매우 어려운 경우가 있다. iOS 개발에 대한 적당한 평가가 여기에 있다고 생각한다. OS는 적극적으로 이 정책을 시행한다. 대부분의 경우 보안상이나 베터리 사용량 등에서 이 정책을 시행하면 좋은점이 많다. 그러나 이런 상황에서 테스팅이나 디버깅을 하면 더 힘들다. 정책을 살짝 바꾸고 손수 그 가능한 라이프 사이클 상태를 테스트 하는 것은 매우 번거로울 뿐더러 때론 불가능한 경우도 있다.

이번의 경우 디버거를 연결한 상태에서 앱 정지를 시키는 방법이 없음을 알아냈다. 사실 디버거는 앱 정지를 막으며 시뮬레이터는 정확하게 시뮬레이팅하지 않는다. 나는 디버거 없이 앱을 실행하고 기기의 로그를 살펴보는 방법밖에 없었다.

macOS 시에라의 새로운 콘솔 앱은 연결된 아이폰의 시스템 로그를 볼 수 있게 해준다. 기에라 이전에는 이 기능을 위해 Lemon Jar의 iOS 콘솔에 의존했으나, 애플이 로그에 접근할 수 있게 하면서 이 기술이 애플에게 받아드려지고 지원하게 되었다는 것을 알게되어 매우 기쁘다. 새로운 콘솔 앱을 어떻게 쓰는지 배우는 것도 의미있는 시간이 될 것이다. Xcode 디버거가 혼자서는 할 수 없는, 많은 iOS 동작을 보여준다. 이 로그는 시스템 전체의 로그이기때문에 디버깅에 무의미한 로그들도 많지만, 필터를 적용해서 간편하게 당신의 앱에 관련된 메시지만 뽑아낼 수 있다.

0xdead10cc 크레쉬를 찾기 위해
  • 몇백개의 데이터베이스 쿼리에 applicationDidEnterBackground 메소드를 설정한다.
  • 내 맥에서 콘솔앱을 열고 Castro를 언급한 메시지로 걸러낸다.
  • Xcode를 통해 앱을 설치하되 앱 아이콘을 눌러 앱을 실행한다.
  • 앱을 백그라운드로 보내기위해 홈버튼을 누르고 바로 포켓몬고를 열어 메모리 압박으로 인한 Castro의 정지를 기대한다.
이 단계를 몇번 거치고나면 크레쉬를 다시 만들어내어 콘솔에서 다뤄볼 수 있다. 이 흔적은 실제 크레쉬와 비슷하며 이제는 크레쉬의 원인을 찾아보기 훨씬 수훨해졌다.

그러고나서 데이터베이스에 접근할 수 있는 백그라운드 동작이 있는 앱에서 한번에 찾고 고칠 수 있게 되었다(네트워크 reachability 변화에서 앱은 백그라운드 작업 없이 리프레쉬 하였다). 리프레쉬하는 과정에서 데이터베이스에 접근할때 앱이 일시정지해 있었다면, iOS가 앱을 죽일 것이다.

백그라운드 패치를 해냄
한가지 더 놀라운 사실을 공유하고 싶다. Castro2에서는 우리의 서버가 새로운 에피소드 배포를 알려주는데, 이것이 사용자 피드의 리프레쉬를 발생시킨다. iOS가 이 메시지를 앱에 보내면 완료 블락을 가지고있는 didReceiveRemoteNotification 메소드를 호출한다. 아래는 문서에서 발췌했다.

여러분의 앱은 알림을 처리하고 특정 완료 핸들러 블락을 호출하기 위해 최대 30초까지 시간이 있다. 실제로는 알림 처리가 끝나자마자 핸들러 블락을 호출할 수 있을 것이다. 시스템은 백그라운드 경과시간, 베터리 소모량, 데이터비용을 여러분 앱에서 추적하고 있을 것이다.

이 부분에 이상한 것이 있었다. 앞에서 언급했듯 Castro는 가끔씩 2초만에 죽어버렸다. 스택 추적에서 보면 아직 완료 블락이 호출되기도 전이었다. 따라서 문서에서는 30초까지 안전하다 했어도 어쨌든 정지되어버렸다.

무슨일인지 알아보기 위해 개발자 지원 사건(developer technical support incidents)을 사용하여 꽤 놀라운 점을 발견했다. 굉장히 도움이 된 Kevin Elliott 대답을 찾았는데, 그 엔지니어의 경우가 내 경우와 비슷했다.

dead10cc 이슈가 파일 락 때문에 일어난 것이라하여
무엇이 실제로 이것을 발생시켰냐하면, 당신의 앱이 정지했을때 iOS가 당신의 앱 컨테이너에서 락이 걸린 파일(이 경우에는 SQLite 락)을 발견했기 때문이다. 그렇게 체크하는 이유는 앱 내에서 데이터가 오염되는 것을 줄이고, 관리하기 위한 방법으로 추가된 것이다. 여기서의 문재는, 락이 걸린 파일은 아직 수정중에 있고 합칠 수 없는 상태로 있을 것이다. 다른 말로는, 앱이 주어진 파일에 락을 거는 유일한 이유는 그 파일에대해 추가적으로 일련의 읽기/쓰기를 할 수도 있으며, 그 사이에 다른 쓰기가 끼어들어오지 않게 쓰기를 완료하기 위한 보장을 해주기 위함이다. 좀 더 확장하여, 파일이 아직 락이 걸려있다면 아직 쓰기를 완료하지 못했다는 의미이다. 이러한 상태의 한 파일은 몇몇 잠재적인 문제를 가진다.
  • 앱이 정지해있을때 앱을 죽이면 데이터 쓰기가 이루어질 수 없고 데이터를 오염시킨다.
  • 파일이 두 앱 간에 공유되고 두번째 앱/extension이 실행되면, 두번째 앱은 강제로 락을 풀고 일관된 상태에 파일을 복구하려 할 것이다. 첫번째 앱은 일관되지 않은 상태를 빠져나오거나, 공유된 파일을 완전히 무시한다.

백그라운드 시간의 30초에 관해서는 
... 여기서 올바른 대답은 문제를 완전히 해결하는 것이다 - 만약 델리게이션으로 여러분의 작업을 다 못끝내면 백그라운드 작업을 시작하고 앱을 정지하기 전에 iOS가 (완료 블락에서) 알려줄 것이다 ...

백그라운드에서 앱이 돌아가는 동안 공유된 컨테이너를 통해 파일을 접근할 수 있는 앱은 백그라운드 작업을 만들 수 있으며, 완료 블락이 30초동안 커버할 수 있다고 가정한다. 이런식으로 작업하기 위해 개발자들은 UIApplication에서 beginBackgroundTaskWithName:expirationHandler 메소드를 사용해 백그라운드 작업을 만들고, 백그라운드 작업이 끝나면 endBackgroundTask를 호출한다.

추가로 kevin은 한가지 더 제안을 해주었는데, 치솟는 데이터 처리가 끝났음을 보장하고 좀 더 확실하게 이상한 버그를 해결하기 위한 방법으로, 백그라운드로 넘어갈 때 앱은 데이터베이스를 닫을 것을 제안했다.

일반적으로 오퍼레이션으로 파일을 닫는 것은 불가사의한 버그(백그라운드에서 뭔가 동작하지 않아요)를 좀 더 명확한 버그(내 앱이 백그라운드에서 동작하지 않아요)로 바꿀 수 있다. 이 점이 문제를 직접 해결할 수 있게 도와줄 것이다.

이것은 꽤 스마트해보인다. 백그라운드로 갈 때 앱의 일부를 종료하는 생각이 들지 않았지만, 확실히 의미가 있다. 나는 백그라운드에서 데이터베이스를 닫는 방법을 Castro의 다음 업데이트에서 찾아볼 것이다.

결론
백그라운드에서 계속해서 동작하는 백그라운드 작업을 추가하여 우리의 베타버전에 이 이슈를 고쳤다. 우리는 곧 고친 것을 포함한 업데이트를 배포할 것이다.

아래에는 내가 배운 것들의 요약본이다.

  • 애플은 다른 서비스들이 발견하지 못한 크레쉬를 보고해준다. 아이튠즈 커넥트나 Xcode, 외부 서비스들까지도 크레쉬 리포트를 확인해보자.
  • 파일 락의 정책은 당신의 데이터베이스가 공유된 공간에 있을 때 더욱 엄격해진다.
  • 백그라운드 패치 완료 블락에 의존하는 것으로는 충분하지 않다. 활성 백그라운드 작업없이는 백그라운드에서 어떤 작업도 하지마라.
  • 앱 라이프 사이클의 특정 부분에서만 나타나는 이슈는 디버깅하기 힘들다. 아직 재현해보지 못했다면 새 시에라의 Console.app을 배워서 사용해보아라.
  • 기술 지원 사건들(Technical Support Incidents)을 기억하며, 이것도 우리가 매년 지불하는 개발자 맴버쉽에 포함된다(원문: Don’t forget about Technical Support Incidents, you get 2 included in your developer account payment every year).



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

,

이전 포스팅에서 NSScreencast iOS 앱을 위한 다운로드 시스템을 어떻게 설계하는지에대해 이야기했다.

여기서 사용자에게 강제로 포그라운드(foreground)에 붙잡아두고 다운로드 받게 할 필요가 없었으므로 자연스럽게 백그라운드 다운로드를 지원하게되었다.

겉으로 보았을때는 꽤나 쉬워보였다. 세션을 백그라운드 세션 구셩으로하고, id를 부여해서, 앱과는 분리된 진행으로 다운로드가 일어날 것이다.

백그라운드 세션을 사용할 때, 세션과 델리게이트가 주어진 다운로드에서 나중에 업데이트를 받기 위해 다시 생성될 필요가 있어보였는데, 블락 기반의 작업 API를 쓸 수 없었다. 여러 시나리오를 생각했지만, 먼저 이상적인 방식을 이야기해보자.
  • 사용자가 다운로드를 시작하고 앱을 멈춘다.
  • 몇 초뒤(내 경험으로는 10-30초이다) 앱은 꺼진다.
  • 다운로드는 다른 프로세스에서 계속 진행된다.
  • 다운로드가 끝나면, 앱은 다시 켜지고 당신의 앱 델리게이트가 다운받기 시작할때 사용한 id와 함께 application(handleEventsForBackgroundSessionWithIdentifier:)에서 받는다.
팁: Xcode에서 디버깅할때, 사실상 디버거는 백그라운드에 있는 동안 앱이 꺼지는 것을 막는다. 따라서 나는 Wait for Launch 옵션을 켜고, 수동으로 앱을 키고 다운로드를 시작한 뒤, 디버거를 켜기 전에 앱을 백그라운드에 놔두었다.

이 메소드가 호출될 때, 같은 id로 새로운 세션 구성을 만들어야한다. 그리고 델리게이트 인스턴스를 심는다. 이 시스템은 다운로드 상태를 즉시 여러분의 델리게이트에 알릴 수 있다.

그러나 어떤 다운로드일까?

실제로는 모른다. 당신이 얻은 모든 것은 원래의 요청 URL인데, 충분한 정보일 수도 있고 아닐수도 있다. URL은 가끔 바뀌기도 하므로 유일한 값이 아닐 수도 있으며, 가장 좋은 키 값이 아니다. http와 https 둘 다 가지고 있을때, 이런 경로들은 같은 리소스를 가리킬 수도 있고, 아마 할쪽이 리다이렉트 할것이다. 왜 이런게 불편한지에대한 여러 이유가 있다. 내 경우 일반적인 에피소드 URL들을 연관지어서 가지고 있는데, 이 URL은 아마존 클라으드프론트 URL로 리다이렉트하여, 유일하지도않고 임시적이기까지했다. 따라서 알림을 받은 에피소드 모델로 돌아갈 방법이 없게 되버린다.

이것이 API에서 좀 이상한 부분인데, 문서에 명확하게 명시되있지 않았다. 그러나 내가 찾은 해결책은 각 다운로드마다 유일한 세션 id를 부여하고 모델에 저장해 놓는다. 그러면 어떤 다운로드가 알림이 왔는지 쉽게 찾아낼 수 있게 된다.

좋다. 이상적인 상황에서는 확실하다. 그러나 이상적이지 않은 상황은 어떨까? 만약 다운로드가 실패한다면? 셀룰러 접근을 해제하고 다운로드가 백그라운드에서 일어나는데 Wi-Fi 존을 벗어나면 어떻게 될까?

마지막 경우는 어느정도 방법을 알고 있다. 만약 보통 세션 구성으로 Wi-Fi 에서 다운받기 시작한 뒤, Wi-Fi를 끈다면 여러분은 셀룰러 다운로드가 허용되지 않았다고 즉시 에러를 받을 것이다. 그러나 백그란운드 세션을 사용하고 있다면 시스템은 똑똑하게 Wi-Fi존에 다시 들어올때까지 기다리다가 그때 요청을 다시 시도한다.

다른 에러로 나타날 수도 있다. 실제로 내 로컬 서버가 동작하지 않을때 커넥션 에러가 나는데, 다운로드는 계속 진행되는것 같지만 계속해서 0%를 가리켰다. 로컬 서버를 켜니 마치 아무 문제 없던것처럼 다운로드를 시작했다.

요청을 재시작할때까지 얼마나 기다려야하는지에대한 이야기가 문서에 명확하게 나와있지 않았다. 사실 사용자가 앱을 다시 실행하면 다운로드는 어떤 상태이야야할까? 우리는 어떻게 알 수 있을까? 개발을 하면서 가끔 주인없는 다운로드를 발견했다. 다운로드 정보의 상태는 .downloading인데 완료나 성공이나 다른 콜백을 받지 못했다. 그때 내가 한 것은 실패로 다시 표시하는 것이다. 그런데 사실 '언제' 그것을 해야할까? 다운로드는 시간이 좀 걸릴 수 있고 몇분 뒤에 다시 시작할 수도 있으므로 단지 x분 뒤에 실패로 표시하는것처럼 간단하지는 않ㄴ았다.

그런식으로 처리하긴 했지만 아마 옳은 방법은 아닌것 같다.



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

,



NSScreencast iOS 앱에서 비디오를 다운받아  오프라인에서도 사용할 수 있는 기능을 넣고 싶었다. 보통 비디오는 80에서 200MB 크기인데, 이것에는 실패시 다시 복구하는 다운로드 시스템을 만들기 위해 신경을 조금 쓸 필요가 있었다.



첫번재로 해야 할 일은 에피소드 화면에 진행상태가 보이는 다운로드 버튼을 넣는 것이었다. 여기서는 iTunes에서 음악을 다운 받는 것과 비슷하게 구현했다. 다운로드는 백그라운드에서 일어나며 진행 노티피케이션을 받아서 UI에 퍼센테이지로 나타낸다.




NSOperation 서브클래스를 통해 실제 다운로드가 끝난다. 이런 방식은 동시성을 제어하고, quality of service과 비행기 모드 다운로드에서 취소시키는 메커니즘을 쉽게 구현할 수 있게 해준다. 다운로드 진행은 에피소드 id를 가진 노티피케이션을 통해 보내지는데, 이 부분과 관련된 UI는 이것을 가져와서 갱신할 수 있다.


물론 다운로드 되는 동안 사용자가 화면을 응시하고 있을 필요는 없으므로, 앱을 둘러보든 기다리든 다운로드는 계속 진행될 것이다.

다은으로 어떤것이 다운중인지, 다운받은 비디오를 오프라인에서 보고, 저장공간을 확보하기 위해 다운로드 중인 것과 예전에 다운로드 받은 것들을 한 곳에서 볼 수 있게 하고 싶었다.

이 화면은 각 줄이 에피소드로서 테이블 뷰 안에 데이터를 보여준다. 현재 다운로드 받은 셀들은 진행 노티피케이션을 받아서 빨리 다시 불러와야 한다.

이 모든것들을 하기 위해 상태를 코어데이터 모델로 저장했다. 나의 DownloadInfo 모델은 아래와 같이 생겼다.


이것을 위해 코어데이터를 활용함으로서 그 라이프 사이클 내내 다운로드 상태를 추적할 수 있다. 이전에는 plist를 사용했었는데, 간단한 저장소로 plist보다 코어데이터가 더 간편했다.

모델에 다운로드 진행 상태 퍼센테이지를 저장하는 것을 볼 수 있지만, 이것을 반복적으로 코어데이터에 저장하지는 않았다. 빠른 커넥션에서 다운로드 진행 변화가 빠르게 일어날 것이고, 코어데이터를 매번 저장할 필요는 없었다. 요청이 취소되거나 다운로드가 더이상 일어나지 않음을 UI에 보여주고 싶을 때만 데이터를 저장했다.

코어데이터를 사용해서 얻은 또다른 장점에는 빠르게 DownloadsViewController를 구성하기위해 NSFetchedResultsController의 유용한 점을 사용할 수 있다는 것이다.

실패 처리하기
네트워크는 항상 뭔가 잘못될지도 모른다는 가능성을 가지고 있다. 이러한 가능성은 큰 파일 다운로드시 더 커진다. 사람들이 Wi-Fi 존을 벗어나거나, 터널로 들어간다던지, 비행기 모드를 켠다던지, 다른 다운로드들과 함께 한꺼번에 많이 다운받는다던지 할 수도 있다. 최고로 좋은 사용자 경험을 보장하기 위해서는 이것을 다루고 싶었고 사용자가 빨리 재시도를 할 수 있도록(때때로는 자동으로 재시도 하도록) 하고 싶었다.

실패가 일어나면 Download의 state 프로퍼티를 .failed로 바꾸고 UI를 적절하게 갱신한다. 그 셀을 다시 불러오고 사용자는 다운로드를 재시도 하기위해 탭할 수 있다.

일시정지와 재개
NSURLSession API가 시작되면, 요청을 취소하고 resume data라 불리는 애매한 오브젝트를 만들어내는 기능을 추가한다. 이것을 이용하면 끊긴 곳에서 요청을 시작할 수 있으며, 한가지 의문은 나중에 다시 시작할 수 있도록이 데이터를 유지하는 방법이다. NSScreencast 모델에 추가하는 것이 딱 알맞았다. 사용자가 진행중에 다운로드를 누르면 downloadTask.cancel(byProducing:)을 호출하고 나중에 사용하기위한 재개 데이터를 모델에 저장한다.

다운로드가 시작되고 모델 데이터가 재개 데이터라면 어느 시점에서 끊겼든지 그 곳에서 요청을 재개하는데 사용된다. 이 기능은 추가하기 쉬운데다, 큰 파일 다운로드시 굉장히 유용하다.


셀룰러 다루기
나는 다른 사람들의 데이터 요금을 불태우고 싶지 않았으므로 NSURLSessionConfigurationallowsCelluarAccessfalse로 설정해두었다. 그릭고 사용자가 원할때 셀룰러로 다운받을 수 있게 토글을 추가했다.




Fx Reachability를 사용하여 커넥션 상태를 모니터링했다. 사용자가 셀룰러일때 에피소드를 다운받으려 한다면 토글을 띄워 설정하고 다운로드 할 수 있게 하였다.

모델과 파일시스템의 동기화 유지하기(Keeping the Model and FileSystem in Sync)
파일에대한 메타데이터는 디스크에 저장하기 때문에 이것이 항상 동기화 되어있음을 보장해야한다. 에피소드를 하나 삭제하면 코어데이터 모델 뿐만 아니라 디스크에 파일도 삭제해주어야한다. 이 동기화를 보장해주기위해 나는 CleanupDownloadsOperation을 가지고 있는데, 이것은 앱이 켜질때 실행되며, 각 저장된 DownloadInfo가 디스크에 옳바른 파일을 가지고 있는지(혹은 삭제되었는지) 확인하고, 다운로드 폴더의 각 파일이 코어데이터에 저장되 있는지(혹은 삭제되었는지) 확인한다.

이렇게하여 뭔가 잘못되거나 두가지 상태(데이터페이스/디스크)가 싱크가 맞지 않을때의 대비책을 마련해 둔 것이다.

백그라운드 다운로드
겉으로 보기엔 간단해 보이지만 백그라운드 다운로드는 혼란과 복잡함의 근원으로 표현된다. 이 주제에 대해서는 나의 다음 포스팅에서 다루겠다.

그들이 말하는 그냥 오프라인 다운로드 추가하기
처음에 오프라인 다운로드를 앱에 넣어보기 전까지는 그냥 하루 이틀정도 더 걸리는 작업이라 생각했지만, 어마무시하게 복잡한 작업이 되버렸다.

역시 이런것이 소프트웨어라 생각된다.




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

,

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

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

,

안드로이드 프로젝트에서 MVC, MVP, MVVM을 위한 간략한 가이드

Model View Controller(MVC)
MVC 디자인 패턴은 세가지 양상으로 앱을 쪼갠다: Model, View, Controller. 일들을 강제로 분리시켜 도메인 model과 controller 로직을 유저 인터페이스(View)로부터 분리시켜준다. 결과적으로 앱을 유지보수하기 간편하고 테스트하기 쉽게 만들어준다.

Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Controller
Controller는 들어온 요청을 처리하는 역할을 한다. Vie를 통해 사용자의 입력을 받으면 Model의 도움으로 사용자의 데이터를 처리하고 다시 그 결과를 View에 보내준다. 보통은 View와 Model 사이에 중재자 역할을 한다.

Model View Presenter(MVP)
이 패턴은 Controller 대신 Presenter가 들어간 MVC와 유사한 형태의 패턴이다. 이 디자인 패턴은 세가지 주 양상으로 앱을 쪼갠다: Model, View, Presenter


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Presenter
Presenter는 View의 도움으로 모든 UI 이벤트를 다루는 역할이다. view를 통해 사용자로부터  입력을 받고, Model의 도움으로 사용자의 데이터를 처리한 뒤, 다시 View에 결과물을 돌려준다. View와 Controller에서와는 다르게 View와 Presenter는 서로 완전히 분리되있고 인터페이스에의해 서로 소통하는 방식이다.
또한 Presenter는 Controller처럼 들어오는 요청 트래픽을 관리하지 않는다.

MVP 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 Presenter는 one-to-one 관계를 가진다. 이 의미는 하나의 View는 하나의 Presenter에 맵핑된다.
  • View는 Presenter에 참조하고 있지만, Model에는 참조하지 않는다.
  • View와 Presenter 사이에 두 방향으로 소통할 수 있다.

Model View ViewModel(MVVM)
MVVM은 Model-View-ViewModel로 정의된다. 이 패턴은 View와 ViewModel 사이에서 두방향 데이터 바인딩을 지원한다. 이것은 ViewModel에서 View에게 자동으로 변화를 전달할 수 있다. 일반적으로 ViewModel에서 View로 변화를 알림받는 옵저서 패턴을 사용한다.


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

ViewModel
ViewModel은 View의 상태를 유지, View의 액션 결과로 Model을 다루기, View 자체에서 이벤트를 트리거하는 그런 메소드나 명령, 다른 프로퍼티들을 노출시키는 역할을 한다.

MVVM 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 ViewModel은 many-to-one 관계를 가진다. 그 의미는 여러 View는 하나의 ViewModel에 맴핑될 수 있다.
  • View는 ViewModel에 참조되지만 ViewModel은 View에대해 모른다.
  • View와 ViewModel의 사이에 두방향 데이터 바인딩을 제공한다.

안드로이드 구현




앞으로 "Controller"라는 용어를 앞에서 말한 Controller, Presenter, ViewModel과 같은 의미로 사용할 것이다.

일반적으로 안드로이드에서는 Activity 클래스가 Controller이고 Fragment 클래스가 View 영역이다. 그러나 이것은 코드의 재사용성을 줄인다. 또한 Fragment와 Activity는 제한적으로 화면 전환 애니메이션이 가능하다.


View 영역(layer)을 위한 UI 클래스(i.e. LinearLayout) 
View 영역은 LinearLayout이나 ViewGroup과 같은 View(UI) 엘리먼트를 상속하여 View를 구현할 수 있다.
  • Activity / 앱 플로우의 독립적인 기능을 재사용할 수 있다.
  • Activity 수를 줄인다.(앱 용량을 덜 잡아 먹는다)
  • Controller와 의존적인 부분을 줄인다.

(독립된) Controller 클래스
Controller 클래스는 어떤 안드로이드의 클래스를 상속받아서도 안된다. Activity와 Fragment로부터 독립적이게 해주어야 재사용이 가능할 것이다.
  • Controller를 가볍게 만들어라, View와 Model을 연결해주는 역할만 하면 된다.(단일 책임 원칙)
  • 이벤트를 다른 Controller에게 넘겨주어라(i.e. analytics)
  • 안드로이드 클래스로부터 분리해라 재사용을 위해

관련된 좋은 습관들
Activity 클래스의 의존성을 줄이기
  • Controller는 추상화에 의존한다(interface)
  • 시스템 구성과 의존성을 컨트롤하기위해 코드 중심에 위치시킨다.
  • 차후에 의존성 주입(dependency injection)을 옮길 수 있게 한다.

Analytics, A/B 테스트 등을 분리하기 위한 파사드(facade)
  • 우리는 여러 기록장치를 사용하기 때문에 파사드를 이용해 이 API들을 한데 모아둘 필요가 있다.
  • A/B 테스트나 다른 임시적인 기능들은 추상화돼있어야하고, 분리된 Controller나 파사드를 통해 접근가능해야한다.

이것들은 어떻게 생겼을까?
테스트에 용이
저렇게 잘 구현하여 Activity로부터 완전히 분리시킨다면, 테스트하기 아주 수훨해 질 것이다. 일반적으로 Activity는 수많은 것(디바이스의 시스템 구성, 네비게이션, 스타일, 액션바..)을 다루며 이것이 테스트의 범위를 너무 크게 만들어버린다.

위와같은 방법으로 Robelectric 테스트는 모든 의존성의 모의 객체(mock)를 만들어주고, 바깥에서 유닛 테스트 할 수 있게 해준다.

사용가능한 라이브러리&프레임워크
아래 안드로이드 프레임워크는 앞서 말한 것들을 구현할 수 있게 해준다. 우리는 지금 시점에서 저것들이 필요하지 않을 수 있으나, 나중에 코드베이스 전반에 걸쳐 적용시켜보려한다면 손쉽게 사용해볼 수 있을 것이다.

  • Square mortar: Activity 라이프 사이클의 행동으로부터 분리시켜, View를 가볍게 만들고 View를 Controller와 한 쌍으로 만들어주는 간편한 라이브러리
  • inloop AndroidViewModel: 엄청난 양의 코드 없이 Fragment나 Activity로부터 데이터와 상태를 분리시켜준다. 벙어리(dumb) View가 되는걸 줄인다.
  • sockeqwe mosby: 현대 안드로이드 앱을 위한 Model-View-Presenter 라이브러리.


'그 외' 카테고리의 다른 글

[번역] 리액티브 프로그래밍이란?  (0) 2017.03.03

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

,

옮긴이 : 종종 브라우저로 원문 링크에 들어가지지 않는 현상이 생깁니다. 이럴때는 Medium 앱으로 Architecture of Node.js Internal Codebase를 검색하여 들어가시면 됩니다.


첫째로 JavaScript라는 단어에 대해...

StackOverflow 공동 창시자인 Jeff Atwood가 Coding Horror 프로그래밍 블로그에 글을 올렸다.
"JavaScript로 작성될 수 있는 모든 앱들은 결국 JavaScript로 작성될 것이다."

JavaScript는 지난 몇년간 가장 인기있는 프로그래밍 언어 중 하나로 크게 확장되었다. 실제로 2016년 SO 개발자 조사에 따르면, StackOverflow에서 가장 인기있고 가장 기술력있는 언어로 1등을 달성하고, 다른 결과들도 좋은 기록을 세웠다.

Node.js는 바이너리 데이터 조작, 파일 시스템 I/O 오퍼레이션, 데이터베이스 접근, 컴퓨터 네트워킹 등과 같은 중요한 서버사이드 기능을 담은 기초(foundation)로서 JavaScript 환경의 서버사이드이다. 이것은 Django(Python), Laravel(PHP), RoR(Ruby) 등과 같은 현존하는 시도되고 테스트된 프레임워크 사이에서 특출난 특징을 가진다. Node.js는 PayPal, Tinder, Medium, LinkedIn, Netflix와 같은 기술 리더 회사들이 사용한다는 것이다. 몇몇은 Node.js가 1.0버전이 되기도 전에 이미 사용하고 있었다.

나는 최근에 StackOverflow에서 Node.js 내부 코드베이스의 아키텍처에 관한 질문에 답변을 달았었다. 그것이 이 글을 쓰도록 영감을 불어넣어 주었다.


사실 공식적인 문서에서는 Node.js가 무엇인지 설명하기에 크게 도움이 되지 않는다.


"JavaScript 런타임은 크롬의 V8 JavaScript 엔진으로 만들어졌다. Node.js는 event-driven, non-blocking I/O 모델이고..."

이 말을 이해하고 실제 숨어있는 힘을 이해하기 위해 Node.js 요소들을 쪼개어보고, 몇몇 중요 용어들을 상세히  설명 할 것이다. 다음 어떻게 다른 조각들이 서로 소통하여 Node.js를 강력하게 만들 수 있는지 설명할 것이다.

Node.js 아키텍처 (High-Level to Low-Level)

요소들/종속물(COMPONENTS/DEPENDENCIES)
V8 : 구글에의해 오픈소스화된 높은 퍼포먼스를 자랑하는 JavaScript 엔진이며 C++로 구현되어있다. 크롬에서 사용하는 엔진과 동일한 엔진이다. V8은 당신이 JavaScript로 작성한 코드를 받아서, 기계코드로 컴파일한 뒤(이렇기에 빠르다), 실행시킨다. 어떻게 V8이 그냥 빠를 수 있을까? StackOverflow의 답변을 확인해보아라.

libuv : 비동기적 기능을 제공하는 C 라이브러리이다. 이것은 다른 중요한 기능들 사이에서 이벤트 루프, 스레드 풀, 파일 시스템 I/O, DNS 기능, 네트워크 I/O를 관리한다.

다른 C/C++ 요소들/종속물 : c-ares, crypto(OpenSSL), http-parser, zlib. 이 요소들은 서버에서 네트워킹, 압축, 암호화와같은 중요한 기능을 수행하기 위해 low-level로 소통하는 방식을 제공한다. 

앱/모듈 : 이것은 JavaScript 코드가 존재하는 모든 곳이다. 당신의 앱 코드, Node.js의 코어 모듈, npm으로 설치한 모든 모듈, 당신이 작성한 모든 모듈까지 당신은 대부분의 시간을 여기서 보낼 것이다.

바인딩 : 당신은 아마 이 시간을 통해 Node.js가 JavaScript와 C/C++로 작성되었다는 것을 알게 되었을 것이다. 여기엔 수많은 C/C++ 라이브러리가 있는데 그 이유는 간단하다: 빠르기 때문이다. 그러나 어떻게 당신이 JavaScript로 짠 코드가 C/C++로 짠 코드와 자연스럽게 소통할까? 그것은 세개의 서로 다른 언어이지 않는가? 그렇다. 다른 언어로 짜여진 코드는 보통 서로 서통할 수 없다. 바인딩 없이는 안된다. 이름에서 이야기하듯 바인딩은 한 언어를 다른 언어에 '묶어(bind)' 코드를 접합시킨다. 그러면 서로 의사소통을 할 수 있게 된다. 이 경우(Node.js) 바인딩은 단순히 C/C++로 작성된(c-ares, zlib, OpenSSL, http-parser 등) Node.js 내부 코어 라이브러리들을 JavaScript에 연결한다. 바인딩을 작성하는데 첫번째 이유는 재사용이다: 만약 필요한 기능들이 이미 구현되있다면, 그것을 가져다 사용하면 된다. 단지 언어가 다르다는 이유로 그것을 다시 짤 필요는 없다. 왜 그냥 연결시켜 사용하지 않겠는가? 두번째 이유는 퍼포먼스이다: C/C++ 같은 시스템 프로그래밍 언어들은 일반적으로 고수준언어(Python, JavaScript, Ruby등) 보다 빠르다. 그러므로 CPU-집약 오퍼레이션 코드는 C/C++로 작성하는 것이 현명할 것이다.

C/C++ Addon : 바인딩은 단지 zlib, OpenSSL, c-ares, http-parser와 같은 Node.js 내부 라이브러리를 접합하는 기능만 한다. 만일 당신이 써드파티의 C/C++ 라이브러리(외부 C/C++ 라이브러리)를 넣고 싶으면, 스스로 라이브러리를 접합시켜야한다. 당신의 코드를 접합해주는 코드가 addon이라 불리는 것이다. 바인딩과 addon은 당신의 JavaScript 코드와 Node.js의 C/C++ 코드를 연결하는 다리라고 생각하면 편할 것이다.

용어들(TERMINOLOGIES)
I/O : Input/Output의 약자이다. 이것은 기본적으로 시스템 I/O의 하위 시스템에의해 주로 다뤄지는 모든 컴퓨터 오퍼레이션을 나타낸다. I/O 바운드( https://en.wikipedia.org/wiki/I/O_bound)오퍼레이션은 디스크/드라이브와의 상호작용을 포함한다. 예를들어 데이터베이스 접근과 파일 시스템 오퍼레이션을 포함한다. 연관된 다른 개념에는 CPU 바운드, 메모리 바운드 등이 있다. 오퍼레이션이 I/O 바운드에 속하는지 CPU 바운드에 속하는지 아니면 다른데 속하는지 구별하는 좋은 방법은, 특정 오퍼레이션이 더 많은 퍼포먼스를 내고 리소스를 많이 잡아먹는지 체크하면 된다. 예를들어 한 오퍼레이션이 눈에 띄게 빨라지고 CPU 파워가 증가했다면 그것은 CPU 바운드 이다. 

Non-blocking/비동기적 : 보통 리퀘스트가 들어오면 앱은 그 리퀘스트를 다루고, 그 리퀘스트의 작업이 끝날때까지 다른 모든 오퍼레이션을 멈춘다. 다음은 실제 일어날 수 있는 문제이다: 만일 한꺼번에 수많은 리퀘스트가 들어오면, 각 리퀘스트는 이전 리퀘스트의 작업이 끝날때까지 기다려야한다. 다른 말로는, 이전 오퍼레이션이 다음 것을 블럭(block)시킬 것이다. 최악의 상황은,  이전 리퀘스트가 긴 응답시간(eg. 먼저 1000개의 계산 후, DB로부터 3GB 데이터를 읽는 경우) 이라면, 다른 리퀘스트들은 긴 시간동안 멈춤/블럭이 되 있을 것이다. 이 문제를 해결하기 위한 방법에는 각 리퀘스트에 대해 멀티 프로세싱 혹은/그리고 멀티 스레딩이 있을 수 있다. Node.js는 이것을 좀 다르게 다룬다. 모든 새 요청에 대해 새 스레드를 만드는 것 대신, 요청들은 한 메인스레드에서 다뤄지며, 그것이하는 일의 대부분이다.

리퀘스트들을 다루기 : 리퀘스트에 포함된 모든 오퍼레이션(eg. 파일 시스템 접근, 데이터베이스 읽기/쓰기)은 백그라운드에서 (위에서 언급한)libuv에의해 관리되는 일꾼 스레드로 보내진다. 즉, 리퀘스트 안에 있는 I/O 오퍼레이션들은 메이스레드가 아닌 곳에서 비동기적으로 다뤄진다. 이 방법으로 무거운 것을 다른곳으로 보냄으로써, 메인스레드가 블럭되지 않게 해준다. 당신의 앱 코드는 한 순간에 한가지 일만 하고 있고, 그것은 메인스레드 안에 있을 것이다.  libuv의 스레드풀에 있는 모든 일꾼 스레드는 당신이 접근할 수 없게 만들어 놓았다. 여러분은 그들에게 직접 접근할 수 없으므로 그것에대해 걱정할 필요도 없다. Node.js가 당신을 위해 관리해주기 때문이다. 이러한 아키텍처는 I/O오퍼레이션을 특히 효율적이게 만들어준다. 그러나 이것에 단점이 없는 것은 아니다. 오퍼레이션에는 I/O 바운드 하나만 있는게 아니라 CPU바운드, 메모리 바운드 등이 더 있다. 조금만 더 이야기해보자면 Node.js는 I/O 작업을 위한 비동기 기능만을 제공한다. CPU 집약 오퍼레이션을 수행하는 방법이 있지만, 이 글의 논지가 아니니 넘어가도록 하자.

이벤트기반(Event-Driven) : 일반적으로 현대의 대부분 시스템은 메인 앱이 꺼져야 들어오는 리퀘스트에의해 프로세스가 초기화된다. 그러나 그들이 다르게 흘러간다면? 전형적인 구현은 리퀘스트를 다음 순서처럼 다룬다: 리퀘스트를 위한 스레드를 하나 만들고, 다른 오퍼레이션이 끝난뒤 그 오퍼레이션을 수행하며, 오퍼레이션이 느려지면 그 스레드에 있는 모든 오퍼레이션이 멈추거나 느려진다. 모든 오퍼레이션이 완료되면 리스폰을 돌려준다. 그러나 Node.js에서는 메인 앱에의한 것이든 리퀘스트에의한 것이든 모든 오퍼레이션이 트리거(trigger)를 기다리는 Node.js 이벤트로 등록된다.

런타임(시스템) : Node.js 런타임은 고수준, 저수준 둘 다 Node.js 앱 실행을 같이 도와주는 전체 코드베이스(위에서 언급한 요소들)이다.

모든것을 섞어보자. (PUTTING EVERYTHING TOGETHER)
이제 Node.js 요소의 고수준 관점에서 보자. 우리는 그것의 아키텍처를 더 잘 알기위해 그것의 워크플로우(workflow)를 알아보고, 어떻게 다른 요소들이 서로 소통하는지도 알아볼 것이다.

Node.js 앱이 실행되면, V8엔진이 당신의 앱 코드를 실행시킨다. 당신 앱의 객체들은 옵저버(이벤트로 등록할 수 있는 기능)의 목록에 넣어둔다. 이 옵저버는 각 이벤트들이 발생할 때 알림을 받는다.

한 이벤트가 발생되면, 그것의 콜백 함수가 이벤트 큐(event-queue)에 들어간다. 큐에 이벤트가 한개라도 남아있으면 이벤트 루프(event loop)는 큐에서 이벤트를 빼어 콜스택(call stack)에 집어넣는다. 이전 이벤트가 처리가 되면 이벤트 루프는 다음 이벤트를 콜스택에 넣는다는 점만 유의하면 된다.

콜스텍에서 우연히 I/O 오퍼레이션을 만나면, 처리하기위해 libuv에게 넘어간다. 기본적으로, libuv는 4개의 일꾼 스레드를 유지하고 있다. 물론 더 많은 스레드를 만들 수도 있다. 만약 요청이 파일 시스템 I/O나 DNS 관련이면, 처리를 위해 스레드 풀에 할당될 것이다; 다른경우, 네트워킹과 같은 다른 리퀘스트들에서는 특정 플랫폼의 매커니즘이 그 리퀘스트들을 알아서 배치할 것이다.

스레드 풀을 사용하게 만드는 I/O 오퍼레이션들(파일 I/O, DNS 등)은 데이터베이스 트렌젝션이나 파일 시스템 접근과 같은 오퍼레이션을 수행하기 위해 Node.js의 저수준 라이브러리로 일꾼 스레드가 작용할 것이다. 처리가 끝나고 libuv는 이벤트가 다시 메인스레드에서 처리될 수 있게 이벤트 큐에 다시 집어넣는다. 이 동안에는 libuv가 비동기의 I/O 오퍼레이션을 다루고 있고, 메인스레드는 마냥 앉아서 처리 결과를 기다리는 것이 아니라 자신의 할 일을 하고 있는다. libuv에의해 돌려받는 이벤트는 이벤트 루프에의해 콜스택으로 돌아왔을때, 다시 메인스레드에서 처리될 수 있다. 이것이 Node.js에서 이벤트 생명주기이다.

mbg는 기가막히게 Node.js와 레스토랑을 비유해 놓았다. 나는 Node.js 사이클을 더 쉽게 설명하기 위해 그의 이야기를 빌려 설명하겠다.

Node.js 앱을 스타벅스 카페라 생각해보자. 높은 효율과 숙련이 잘 된 웨이터(한 명이고 메인스레드라 할 수 있다)가 주문을 받는다. 만약 같은 시간대에 많은 고객이 들이닥치면 웨이터의 서비스를 받기위해 줄을 서게 된다(이벤트 큐에 넣는다). 한 고객이 웨이터로부터 서비스를 받으면 웨이터는 메니저(libuv)에게 주문을 넘겨준다. 각 주문을 어떤 바리스타(일꾼 스레드 혹은 플랫폼-특정 매커니즘)에게 주문을 할당할지 정한다. 바리스타는 다른 다른 재료들과 기계를 써서(저수준 C/C++ 요소들) 고객이 요청한 것들의 음료를 만든다. 보통 4명의 바리스타가 특정 라떼(파일 I/O, DNS 등)를 돌아가면서 만든다. 그러나 주문이 피크치에 달할 때는 더 많은 바리스타를 일시적으로 고용할 수 있다(개업한 날의 이야기이지, 점심시간대의 이야기는 아니다). 한 번 웨이터가 메니저에게 주문을 넘기면, 웨이터는 다른 고객을 서브하기 위해 커피 만드는 것을 기다리지 않는다. 대신에 다음 고객을 부른다(이벤트 루프에의해 다음 이벤트를 큐에서 꺼네어 콜스택에 넣는다). 이벤트가 콜스택 안에 있는 것은, 고객이 카운터에서 서브를 받으며 있는 것과 비슷하다. 커피가 다 만들어지면, 커피는 고객 줄의 끝으로 보내진다. 커피가 카운터로 나오면 웨이터는 고객의 이름을 부를 것이고, 고객은 주문한 커피를 받게된다. (비유가 다소 현실세계에서는 이상할 수 있지만, 프로그램 관점에서 프로세스를 고려할 때는 딱 들어 맞을 것이다)


고수준 관점의 전반적인 Node.js 내부 코드 베이스와 그것의 일반적인 이벤트 생명주기에대해 설명이 끝났다. 그러나 이 총람은 굉장히 일반적인 상황의 이야기이고 많은 이슈와 세부적인 요소에는 들어맞지 않을 수 있다는 점을 유의하길 바란다. 예를들어 CPU 바운드 핸들링이나 Node.js 디자인 패턴 등등이 있다. 더 많은 다른 글들의 토픽들이 그 부분을 채워줄 것이다. 



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

,

Node.js 쓰다보면 메모리를 점점 잡아먹는 현상을 발견할 있다. 이것은 굉장히 일반적인 현상이고, Node.js에서 쓰는 V8엔진은 기본으로 1.4GB 메모리를 한계로 잡아놓고(기본적으로 64비트는 1.4GB, 32비트는 512MB 한계로 잡는다.) 안에서 메모리를 점점 사용하게된다. 나의 경우는 아마존 프리티어를 사용하기때문에 메모리가 1GB 밖에 되지않아서 메모리 제한을 낮추어야했다.


--max-old-space-size=512

메모리 사용을 512MB 제한하라는 옵션이다. 아래와같이 사용할 있다.

혹은 forever 모듈을 사용하고있다면 아래와같이 사용하면 된다.


--expose-gc

가비지 컬랙터를 수동으로 전환한다. 내가 자바스크립트 코드에서 gc(); 호출하면된다. 적어도 30초에 한번씩은 가비지 컬랙팅을 하기를 추천 한다고 한다.


--max-new-space-size=2048

만약 서버 멈춤이 짧은것이 중요하다면 --max-new-space-size=2048 플래그를 사용하면 된다. 이것은 피크 퍼포먼스를 줄여버리기도하지만, 100ms정도로 서버 멈춤이 짧아질 것이다


--noincremental-marking

반면 피크 퍼포먼스가 중요하고 서버 멈춤이 오래 걸려도 괜찮다면 --noincremental-marking 플래그를 사용하면 된다. 플래그를 사용하면 1GB 1 정도의 서버 멈춤을 예상할 있다. 따라서 주로 작은 (heaps)이나 일괄 처리 테스크(순차 처리 테스크) 유용하게 사용된다.



참고

  • StackOverFlow : NodeJS / ExpressJS Memory Leak
    http://stackoverflow.com/questions/22507667/nodejs-expressjs-memory-leak
  • StackOverFlow : Is anybody making a Node-optimized V8?
    http://stackoverflow.com/questions/8263210/is-anybody-making-a-node-optimized-v8
  • 600k concurrent websocket connections on AWS using Node.js
    http://www.jayway.com/2015/04/13/600k-concurrent-websocket-connections-on-aws-using-node-js/
  • How does Node.js do memory allocation?
    https://www.quora.com/How-does-Node-js-do-memory-allocation



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

,

포트폴리오 페이지를 제작하고있다. 보통 포트폴리오 제일 하단에 footer를 만들어서 자신의 소셜 링크를 달아놓는다. 그 부분을 쉽고 깔끔하게 구현하는 코드를 기록해놓았다.

결과는 아래와 같다.

참고 :  http://fortawesome.github.io/Font-Awesome/examples/#
더 많은 아이콘 : 
http://bootstrapheroes.com/semantriademo/downloader/light-blue/ui_icons.html


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

,