29기 SOPT 앱잼을 통해 개발했던 SeeMeet에 대한 후기를 이제서야 올린다. 개발은 올해 1월부터 했지만, 본인의 삽질에 대해 오랜 시간이 지나며 사이드 프로젝트화 되어 오랜 기간이 소요되었다 ㅠ
개발을 하면서 어려운 점에 대해 정리하여 앞으로 서술하고자 한다.
화면 전환의 분리 (코디네이터 패턴의 적용)
- 본래 본 프로젝트는 MVC 아키텍처로 시작되었다. (정확하게는 Apple MVC)
- Apple MVC 아키텍처는 ViewController가 데이터 가공과 뷰의 역할을 모두 짊어지는 아키텍처로 Massive View Controller의 문제가 발생한다.
- 그래서 엄청난 길이의 ViewController가 발생하기도 했다.
![](https://velog.velcdn.com/images/loinsir/post/35c63fdd-01bc-46d8-b4bc-38e931fadcc4/image.png)
엄청난 수의 ViewController...
- 결정적으로 코디네이터 패턴을 도입하기로 마음먹게 된 부분은 바로 이부분이다.
- 왼쪽 뷰의 친구 목록 각 셀의 메세지 버튼을 누르면 약속 신청 뷰가 켜짐과 동시에, 약속 신청할 친구에 추가되어야한다.
- 데모데이에는 구현이 안되어 있던 부분이기도 하고, delegate 패턴을 통해 구현을 할 수도 있었지만, 저 두 뷰들은 앱 뷰의 계층상 홈 뷰컨의 자식들로, 형제관계이다. 그래서 화면 전환하기가 좀 까다로웠다. (navigation pop, navigation push를 해야하는 상황) 화면 전환을 하면서 viewController를 해제하게 되면 해제 이후의 코드가 실행되지 않을 수 있기 때문이었다.
- 그래서 화면전환을 담당하는 Coordinator 패턴을 도입하여 ViewController로부터 화면전환 로직을 위임받고, Massive View Controller 문제도 약간은 해소함과 동시에 저 뷰컨트롤러 전환 간의 데이터 전달 등도 구현하고자 했다.
- 다음 자료를 참고했다.
- https://khanlou.com/2015/01/the-coordinator/ (코디네이터의 원조글)
코디네이터 패턴
- 코디네이터 패턴은 Soroush Khanlou 라는 분이 제안한 것으로 요약하자면 View Controller가 화면전환을 직접하는건 뷰로서의 역할을 벗어나는 행위라는 것이다.
- 그래서 화면전환을 담당하는 Coordinator 를 따로 정의하여 뷰 컨트롤러부터 화면전환의 역할을 분리하자는 것이다.
여러 자료를 탐독해 본 결과 구현은 여러가지 방법이 있는듯 했다. Rx를 이용해 하는 RxFlow도 있었고 delegate를 통해 상위 코디네이터에 화면 전환을 요청하는 방법등이 있었는데… 아직 이 프로젝트에 Rx를 도입할 생각까지는 없었었다…(근데 나중에 일부 화면에 도입하게 된다…ㅋㅋ)
결국 여러 블로그를 탐독해서 다음과 같이 구현하기로 했다.
- 코디네이터 프로토콜을 다음과 같이 정의
protocol Coordinator: AnyObject { var coordinators: [Coordinator] { get set } // 하위 코디네이터들을 관리하는 프로퍼티 func start() // 해당 코디네이터의 root VC를 띄우는 메서드를 지정한다. }
즉 코디네이터들은 계층 구조를 띄게 된다.
- 루트, 마스터 코디네이터인 AppCoordinator를 다음과 같이 정의. 이 앱 코디네이터는 탭바 코디네이터의 역할 또한 맡게 된다. 우리 앱은 시작하면 탭바로부터 시작되므로.
class AppCoordinator: Coordinator { var coordinators: [Coordinator] = [] let window: UIWindow? var navigationController = UINavigationController().then { $0.modalTransitionStyle = .crossDissolve $0.modalPresentationStyle = .overFullScreen } private let disposeBag = DisposeBag() init(_ window: UIWindow?) { // SceneDelegate에서 UIWindow의 의존성 주입받는다. self.window = window window?.makeKeyAndVisible() } //.... 생략
- 상위 Coordinator에서 정의한 navigationController는 새로운 자식 뷰 플로우가 시작될 때, 즉 새로운 코디네이터가 생성될 때 주입시킨다(탭바에 묶여있는 ViewController 2가지는 제외, 이들은 생성자에서 생성한다). 이렇게 해서 전체 앱에서 네비게이션 컨트롤러는 오직 1개만 존재시킬 수 있다. 굳이 이렇게 까지 할 필요가 있나 싶지만… 메모리를 약간이라도 아낄 수 있으면서 복잡한 계층을 회피할 수 있지 않을까 하는 생각에서였다.
class PlansCoordinator: Coordinator {
weak var parentCoordinator: Coordinator?
var coordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) { // HomeCoordinator로부터 의존성을 주입받는다.
self.navigationController = navigationController
}
// ...생략
- 여차저차 Coordinator를 각 뷰 Flow마다 생성하고 각 뷰에서 화면전환이 일어나는 delegate들을 구현한다.
extension RegisterCoordinator: EmailRegisterVCDelegate{
func backButtonDidTap() {
navigationController.popViewController(animated: true)
}
func closeButtonDidTap() {
self.navigationController.presentingViewController?.dismiss(animated: true)
self.navigationController.viewControllers.removeAll()
parentCoordinator?.start()
parentCoordinator?.coordinators.removeAll(where: { $0 === self })
}
func nextButtonDidTap(accessToken: String, refreshToken: String, email: String) {
startProfileRegisterVC(accessToken: accessToken, refreshToken: refreshToken, email: email)
}
}
이렇게 코디네이터 패턴을 도입했는데… 장점은 화면전환을 코디네이터 패턴에서 관리하게 되어 편하고 Massive View Controller를 어느 정도 해소했다는 점이었고, 단점은 Coordinator객체 또한 생성, 삭제를 관리해야 하기에 조금 더 앱 측면에서 보기에 복잡해졌다는 것이다. 일례로 잘못해서 뷰 흐름을 종료할때 부모 Coordinator에서 본인을 찾아 remove 해주어야 하는데 명확히 그렇지 못했다면 데이터가 그대로 남아서 다시 재호출 할때 문제가 되기도 했었고, 잘못된 remove시 크래시가 발생하기도 했었다. 여차저차 우여곡절이 많은 적용이었다.
![](https://velog.velcdn.com/images/loinsir/post/9fc58c51-619f-4cc5-9bd8-3db3545a4a4c/image.png)
그리고 Coordinator 계층 끼리의 소통도 delegate 패턴을 사용하게 되어 생각보다 코드의 양이 많많치 않았고, 실수로 부모, 자식 간 delegate 지정을 하지 못하였을 경우 작동하지 않아 디버깅 하기 까다로웠다. 이 부분은 비단 코디네이터 에서 뿐만 아니라 delegate 패턴 자체의 단점인듯. 만약 다음에 Coordinator 패턴을 또 사용하게 된다면 RxFlow라는, Rx를 도입한 Coordinator를 사용해볼것 같다. Rx는 잘만 사용한다면 코드의 양을 줄일 수 있으니….
Uploaded by N2T