https://www.youtube.com/watch?v=AVdfJVSum_Y&list=PLw-3TTKkn1fM0K30mImLB0JPF6aWBhSEM&index=12
위 영상을 보고 번역/정리한 글, 자세한 내용은 영상보시길츄천
발표자분의 상황은 지속적으로 변화하는 팀이고, 테스트가 잘 되는 구조를 원했습니다.
쉽게 배울수 있고 하나의 컴포넌트는 하나의 책임을 가지는 아키텍처를 요구함
또 뷰 모델은 어디에 둘지, 네트워킹은 어디에 둘지 명확해야함
발표자 분이 사용하는 구조에 대해서 알아보는 세션인듯
여기서 보여주는 구조의 흐름도는 다음과 같습니다.
이는 단순히 우리가 알고 있는 것을 표시할 수 있도록 합니다.
예제 코드
protocol TODOListView: class {
func showTODOs(todo: [TODOItem]) -> ()
}
class TODOListViewController: UIViewController, TODOListView {
func showTODOs(todo: [TODOItem]) {
// ...
}
}
이는 뷰 컨트롤러가 로드되거나 사용자가 버튼을 누를 때 사용자로 부터 UI 작업을 수신할 수 있습니다. 뷰 컨트롤러는 프레젠터에게 이 작업을 알리고, 프레젠터는 이 작업을 처리하는 로직을 구현하고 디스플레이 이벤트를 뷰로 보냅니다.
class TODOListPresenter {
weak var view: TODOListView?
func didLoad() {
let todos = self.todoListRepository.getAllTODO()
self.view?.showTODOs(todo: todos)
}
}
여기서 멋진 점은 레포지토리가 뷰의 인터페이스라서 테스트하기 더 쉬워진다는 점이라고 하네요
아래처럼 테스트용 레포지토리를 만들고 실제 프레젠터를 인스턴스화하여 테스트를 진행하면 됩니다.
class TODOListPresenterTest: XCTestCase {
var mockRepository: MockTodoRepo!
var presenter: TODOListPresenter!
override func setUp() {
self.mockRepository = MockTodoRepo()
self.presenter = TODOListPresenter(repository: mockRepository)
}
func testDidLoad(){
// verify는 메소드가 호출되었는지 확인하는 정도의 테스트라고 하네요
// 이 팀에서 만들어서 사용하는 방법이라고 함니다
XCTAssertFalse(verify(MockTodoRepo.getAllTODOs))
presenter.didLoad()
XCTAssertTrue(verify(MockTodoRepo.getAllTODOS))
}
}
위 코드에서 뷰도 인터페이스이기 때문에 모의 뷰도 포함하여 테스트를 하는 좀 더 현실적인 테스트 코드는 다음과 같습니다.
위 코드에서 아래 코드들이 추가되었다고 생각하시면 됩니다
// Property
var view: MockTodoView!
// setUp
self.view = MockTodoView()
self.presenter.view = self.view
// testDidLoad
XCTAssertTrue(verify(MockTodoView.showTODOs))
연결하는 친구라서 이름이 어셈블리인듯하네여
class TODOAssembly {
var navigationController: UINavigationController
var todoReposirtory: TODORepository
init(navigationController: UINavigationController,
todoRepository: TODORepository) {
...
}
var todoViewController: TODOListViewController {
let todoPresenter = TODOListPresenter(repository: todoRepository)
let vc = TODOListViewController(presenter: todoPresenter)
return vc
}
}
어셈블리 트리 구조는 다음과 같슴다.
간단한 예시 코드
class LoginAssembly {
// ...
var loginViewController: LoginViewController {
return LoginViewControlelr()
}
}
class TrafficAssembly {
// ...
var trafficViewController: TrafficViewController {
return UIViewController()
}
}
class CoreAssembly {
lazy var loginAssembly = LoginAssembly(self)
lazy var trafficAssembly = TrafficAssembly(self)
var rootViewController: UIViewController {
return trafficAssembly.trafficViewController
}
}
이를 AppDelegate
에서 초기 윈도우에 설정하는 코드는 다음과 같습니다.
let coreAssembly = CoreAssembly()
// 2018년 세션이라 @Main이 아닌가보네용
@UIApplicationMain
class AppDelgate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ...) -> Bool {
self.window?.rootViewController = coreAssembly.rootViewController
return true
}
}
위 코드처럼 앱이 실행되면 처음 하는 작업은 루트 뷰 컨트롤러를 코어 어셈블리의 루트뷰로 할당하는 것입니다.
코어 어셈블리 코드
final class CoreAssembly {
var naviagationController: UINavigationController
var appStateRepository: AppStateRepository
lazy var tourAssembly = TourAssembly(self.navigationController)
lazy var todoAssembly = TODOAssembly(self.navigaitionController)
init(...) {
// 맨 위 2개 프로퍼티에 할당하는거라 생략했스민다.
}
// 첫 실행이라면 tour로 아니면 todo로 return 하는 읽기속성
var rootViewController: UIViewController {
if appStateRepository.isNewInstall {
return tourAssembly.tourViewController
} else {
return todoAssembly.todoViewController
}
}
}
테스트를 하는 코드는 다음과 같습니다.
class RootAssemblyTest: XCTestCase {
func testRootViewControllerTour() {
let mockAppStateRepo = MockStateRepository(isNewInstall: true)
let assembly = RootAssembly(navigationController: UINavigationController(), appStateRepository: mockAppStateRepo)
XCTAssert(type(of: assembly.rootViewControole) == TourViewController.self)
}
// 아래는 TODO 테스트 하는 함수, 위 코드와 거의 동일
}
간단해보이지만 까다롭다고 하네요. 투두리스트에서 항목을 누른다고 가정하면서 설명합니다.
protocol TODOListNavigator {
func showTODODetail(id: Int) -> ()
}
네비게이터의 구현 코드는 다음과 같습니다.
어셈블리가 모든 뷰의 팩토리이므로, 네비게이터도 어셈블리에 의해 구현되어야 한다고 합니다.
class TODOAssembly {
var todoViewController: TODOListViewController {
// 위 투두어셈블리와 거의 동일한데,
//TODOListPresenter에 navigator: self 라는 인자를 전달합니다.
}
var todoDetailViewController: TODODetailViewController {
let presenter = TODODetailPresenter(repository: self.todoRepository)
let vc = TODODetailViewController(presenter: presenter)
return vc
}
}
extension TODOAssembly: TODOListNavigator {
func showTODODetail(id: Int) {
let vc = self.todoDetailViewController
vc.presenter.selectedTodoId = id
self.navigationController.pushViewController(vc, animated: true)
}
}
네비게이션 컨트롤러도 어셈블리의 일부라는 것을 명심해야 합니다
네비게이터의 테스트 코드는 다음과 같습니다.
// XCTestCase
var mockNavigator: MockNavigator!
var presenter: TODOListPresenter!
override func setUp() {
self.mockNavigator = MockNavigator()
self.presneter = TODOListPresenter(navigator: mockNavigator)
}
func testOnTodoTap() {
XCTAsserFalse(verify(MockNavigator.showTODODetail))
prsenter.onTodoTap(id: 4)
XCTAssertTrue(verify(MockNavigator.showTODODetail))
}
protocol TODODetailView: class {
func showTodo(todo: TODOItemDetail)
func showError(error: Error)
func showLoading()
}
class TODODetailViewContorller: UIViewController {
var isLoading: Bool = false
var error: Error? = nil
var data: TODOItemDetail?
//...
}
위 처럼 에러나 데이터를 로딩하는 상태를 들고 있을 때 테스트하기가 점점 힘들어집니다.
그래서 아래와 같이 상태를 열거형을 사용해서 표현한다면 테스트를 더 쉽게 할 수 있도록 해줍니다.
enum ViewState<T> {
case initial
case loading
case data(T)
case error(error)
}
protocol TODODetailStateView: class {
var state: ViewState<TODOItemDetail> { get set }
}
// 아래와 같이 상태라는 변수를 설정합니다
class TODODetailViewController: UIViewController {
var state: ViewState<TODOItemDetail> = .inital {
didSet {
// 변경사항은 여기서 처리합니다.
}
}
}
mvvm에서의 뷰 모델은 데이터를 표시하는 것 외에 다른 작업을 수행하지만, 여기서 설명하는 어셈블리 아키텍처에서는 로직이 없는 뷰 모델입니다.
struct TodoTavleViewCellViewModel {
var formattedDate: String
var content: String
}
class TodoTableViewCell: UITableViewCell {
var viewModel: TodoTavleViewCellViewModel? {
didSet {
guard let viewModel = viewModel else { return }
date.text = viewModel.formattedDate
content.text = viewModel.content
}
}
@IBOutlet wear var date: UILabel!
@IBOutlet wear var content: UILabel!
}
아키텍처에 대해 생각을 하게 돠는 영상이라 조큼 어려운거같습니다.
보면서 느낀점은 확실히 테스트하기 편해지는 아키텍처라고 생각이 들었습니다. 의존성 주입을 사용하기도 하고, 각자의 책임에 맞게 잘게 나눈게 제일 큰 영향이 있지 않나 싶습니다.
프레젠터는 MVC에서의 뷰 컨트롤러 부분을 닮았다고 생각했고, 어셈블리는 뭔가 코디네이터랑 비슷하다고 생각을 했습니다. 아직 잘 모르기는 합니다만 ㅋㅌㅋ
코디네이터가 라우터 같은 느낌으로 알고 있는데 여기서는 네비게이터가 그런 역할을 하지 싶은데 어셈블리에서 결국 진행을 하기 때문에 그런 생각을 했던거 같습니다. 비즈니스 로직은 프레젠터, 화면 전환은 어셈블리 & 네비게이터 ? 어렵네여 hmm,,
좀 더 아키텍처 관련 영상이나 글들을 보면서 지식을 넓혀봐야겠슴다..