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끼리 상태를 공유해야 했다.
상태 관리의 상태
우리 앱은 상태 관리라는 곳에 상당한 노력을 쏟아붇고 있다. 앱에서의 갱신은 보통 아래의 순서를 따른다.
- 로컬 객체에서 상태를 갱신
- UI를 갱신
- 데이터베이스를 갱신
- 네트워크 연결이 가능해지면 서버로 보낼 그 변화를 큐에 넣기
- 다른 객체에 상태 변화를 알리기
나중에 또 한번 위의 과정을 담은 우리의 새 아키텍처에대해 포스팅을 할 예정이므로 오늘은 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를 보낸다. 그 view는 store에의해서만 호출되는 유일한 인터페이스를 통해 갱신되었다는 소식을 받는다.
상태 업데이트는 action을 통해 일어날 수 있다.
action은 상태 변화를 하게 해주는 트리거지만 스스로 상태변화를 구현해놓지는 않는다. 상태 변화를 원하는 모든 컴포넌트들이 글로벌 dispatcher에 action을 던진다. 이 store는 dispatcher와 함께 등록하고 그것들이 어디 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의 구현은 아래처럼 요약될 수 있다:
- 연관된 모든 action을 dispatcher와 함께 store를 등록한다. 이 예제에선 모든 AnnotationFilteringActions이 될 것이다.
- 각 action들별로 호출할 수 있는 핸들러를 만든다.
- 핸들러와 함께 필요한 비즈니스 로직을 동작하고 완성에 상태를 갱신한다.
구체적인 예제로서 AnnotationFilterStore가 toggleFilterAction을 어떻게 다루는지 확인할 수 있다:
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의 역할을 고수준에서 다시 한번 상기시켜보고자 한다:
- 필요로 하는 모든 action을 위해 dispatcher와 함께 store를 등록한다. 현재 예제에서는 모두 annotationFilteringActions이 되어야한다.
- 각 개별 action들을 위해 불릴 수 있는 핸들러를 구현한다.
- 핸들러 안에서 해당 비즈니스 로직을 실행하고 그 결과의 상태를 갱신한다.
다음으로 어떻게 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 컴포넌트들을 만들어 놓았다. 나중에 그것에 대해 가볍게 보여주겠다.
이제 주석 필터링 기능은 다시 접어두고 실제 세상의 코드를 보자(이번에는 약간 생략되었다. 이 코드는 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를 구현할 때 나는 일반적으로 아래 순서에 따라 작업을 쪼개어 한다:
- 상태 타입의 모양을 정의한다.
- action을 정의한다.
- 각 action들의 비즈니스 로직과 상태 변화를 구현한다. ― 이것은 store 안에 구현되있다.
- view를 표현하기 위해 상태를 맵핑하는 UI 바인딩을 구현한다.
이것은 우리가 얘기했던 세부적인 구현의 모든것들을 포괄한다.
이제 드디어 Flux에서 어떻게 테스트 할 지에대해 이야기해보자.
테스트 작성하기
Flux 아키텍처의 큰 장점중 하나는 일들을 엄격하게 분리한다는 점이다. 이것은 비즈니스 로직이나 UI 코드의 커다란 부분을 테스트하기 쉽게 해준다.
Flux에서는 테스트 해야하는 두가지 부분이 있다:
- store에서 비즈니스 로직
- 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와 함께 작업하고 싶거나 큰 산업을 위해 중요한 제품을 만드는데 도움을 주고 싶으면, 우리는 지금 고용중이다.
참고: