SLP란 현재 싹에서 5주 동안 진행하였던 Service Level Project의 약자입니다.
본 글에 나오는 SLP라는 명칭은 앱 이름이라고 생각 해주시면 됩니다.
SLP앱은 위치기반 주변에 같은 취미를 가지고 있는 새싹들을 찾아 매칭하여 채팅할 수 있는 앱이며,
현업 서비스 수준의 기술을 담은 규모있는 프로젝트 입니다.
이번 포스팅은 SLP 앱을 개발하며 적용시킨 코디네이터 패턴에 대하여 정리한 페이지입니다...!
아래의 이미지들은 직접 구상한 이미지로 무단 도용을 금지합니다
코디네이터란?
아키텍처 이름에서 우리는 MVC-C, MVVM-C 이러한 표현을 종종 보게 되는데요. 여기서 마지막에 C가 붙는 의미가 Coordinator의 C를 이야기 합니다. 코디네이터 패턴을 사용하지 않는다면 화면 동작의 관한 코드를 모두 ViewController가 담당하게 됩니다. 코디네이터를 사용하면 얻을 수 있는 장점을 정리해보자면 이렇습니다.
- VC의 책임을 줄일 수 있다.
- 의존성을 외부에서 주입(DI)할 수 있습니다.
- ViewController의 계층, 화면 간의 연결 플로우를 모아 관리 할 수 있다.
코디네이터 패턴을 적용하게 된 계기
SLP에서는 화면전환의 로직이 한개의 ViewContoller에서 많은 편입니다. ViewController에서 비지니스 로직이나 View 로직을 Usecase나 Viewmodel로 뺏지만, 여전히 화면동작에 대한 코드 때문에 ViewController의 책임이 많아지게 되었습니다. 그리고 화면 전환에 관한 코드를 따로 모아서 볼 수 없었죠.
...작성중
코디네이터를 어떻게 구성할 것인지
코디네이터를 처음 적용해보면서 팀원과 가장 많은 시간과... 고민과.. 회의를 하였던 부분입니다. 코디네이터를 어떻게 구성할 것인가에 관한 문제였는데요. 코디네이터를 설계하기 위해서는 앱 기획의 화면 플로우를 이해하고 있어야 합니다. 참고로 화면 플로우에서 플로우라 하면 VC하나를 말하는 것이 아닙니다. 구체적으로 예를 들어 말하자면 <로그인 Flow>라고 한다면 그안에는 로그인을 하기 위한 여러 VC들이 존재할 수 있습니다.
코디네이터 패턴을 제대로 이해하지 못한 분들이 겪는 많은 오류가 Coordiantor와 ViewController를 무조건 1:1로 구성하는 것입니다. 저희 팀은 아니였지만, 다른 새싹 팀에서 코디네이터 패턴을 그렇게 이해하고 1:1로 짜신 분들이 있었습니다. (저 또한 극 초반에는 플로우라는 단어를 이해하지 못해 헷갈려했습니다.) 저희 팀은 코디네이터는 어느정도로 설계할지 결정하는 것이 코디네이터 코드를 작성하는 것보다 더 많은 시간이 걸렸습니다. 아래는 코디네이터를 설계하기 전에 먼저 SLP의 가장 큰 플로우들의 흐름을 그림으로 정리한 것입니다.
그림에서 보다시피, 처음 앱을 키면 나올 수 있는 화면 플로우의 경우의 수는 3가지 입니다.
- 앱을 처음 깐 유저라면 온보딩 플로우를,
- 첫 방문이 아니라면 로그인 플로우를,
- 자동로그인이 되어있는 상태라면 탭바 플로우를 보여주어야 합니다.
그리고 로그인 플로우를 완료하게 되면 비회원인 경우, 회원인 경우로 2가지 경우가 나뉩니다.
- 회원이라면 로그인 성공으로 탭바 플로우로 화면 전환하고,
- 비회원이라면 회원가입을 진행하게 됩니다.
회원가입 플로우에서
- 회원가입을 실패하면 다시 회원가입을 재시작하고,
- 회원가입을 성공하게 되면 탭바플로우를 실행합니다.
탭바플로우에서는
- 로그아웃 기능은 없습니다.
- 회원탈퇴 기능만 있기 때문에 회원탈퇴시 다시 온보딩 화면플로우부터 시작합니다.
코디네이터 설계도
위의 화면 플로우를 기반으로 전체 코디를 우선 2가지로 나누기로 하였습니다. 온보딩, 로그인, 회원가입 3개의 플로우를 묶은 AUTH 코디네이터와 탭바 플로우를 묶은 TAB코디네이터를 구성하였습니다.
전체 코디구성 설계도는 아래와 같습니다. 위의 구성은 아래의 그림에서 빨간색으로 표현된 코디들입니다.
전체 큰 코디는 2가지로 나누었고 그다음 각각의 탭바마다 코디를 둔 형태입니다. 만약에 마이페이지에서 로그아웃 기능이 있었다면, Auth Coordinator 하위에 로그인 Coordinator를 두려고 하였습니다. 이유는 로그아웃뒤에 로그인 플로우만 재사용되기 때문입니다. 그런데 SLP에서는 로그아웃은 없고 회원탈퇴만 있는 형태라서, 회원탈퇴가 이루어진 뒤 Auth 코디전체 흐름으로 돌아가면 되기 때문에 Auth코디네이터만 설계하게 되었습니다.
코디네이터 구현
코디네이터 기본 프로토콜
import UIKit
protocol Coordinator: AnyObject {
var delegate: CoordinatorDelegate? { get set }
var navigationController: UINavigationController { get set }
var childCoordinators: [Coordinator] { get set }
var type: CoordinatorStyleCase { get }
func start()
func finish()
init(_ navigationController: UINavigationController)
}
extension Coordinator {
func finish() {
childCoordinators.removeAll()
delegate?.didFinish(childCoordinator: self)
}
}
기본적인 코디네이터의 기본구조 입니다. 코디네이터 패턴에는 Router를 두어 구현하는 경우도 있고, 간단하게 childCoordinator없이 구현하는 방법도 있지만, 저희팀에서 사용한 코디네이터 패턴은 Naviagation을 기반으로 한 ChildCoordinator를 이용한 방법을 사용하였습니다. 그리고 finish 되었을 때 해줄일을 정하기 위해 Delegate를 이용하여 didFinish함수를 구현하였습니다.
import Foundation
protocol CoordinatorDelegate: AnyObject {
func didFinish(childCoordinator: Coordinator)
}
import UIKit
final class AppCoordinator: Coordinator {
weak var delegate: CoordinatorDelegate?
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
var type: CoordinatorStyleCase = .app
init(_ navigationController: UINavigationController) {
self.navigationController = navigationController
navigationController.setNavigationBarHidden(true, animated: false)
}
func start() {
if userDefaults.bool(forKey: UserDefaultKeyCase.isLoggedIn) {
connectTabBarFlow()
} else {
connectAuthFlow()
}
}
private func connectAuthFlow() {
let authCoordinator = AuthCoordinator(self.navigationController)
authCoordinator.delegate = self
authCoordinator.start()
childCoordinators.append(authCoordinator)
}
private func connectTabBarFlow() {
let tabBarCoordinator = TabBarCoordinator(self.navigationController)
tabBarCoordinator.delegate = self
tabBarCoordinator.start()
childCoordinators.append(tabBarCoordinator)
}
}
extension AppCoordinator: CoordinatorDelegate {
func didFinish(childCoordinator: Coordinator) {
self.childCoordinators = self.childCoordinators.filter({ $0.type != childCoordinator.type })
self.navigationController.viewControllers.removeAll()
switch childCoordinator.type {
case .auth:
self.connectTabBarFlow()
case .tab:
self.connectAuthFlow()
default:
break
}
}
}
Delegate Protocol 사이의 인스턴스 강한참조 순환을 막기위해 프로토콜을 weak로 선언해줍니다. 프로토콜 타입을 weak를 선언해야 인스턴스간의 강한참조순환을 막을수 있는데 이것에대한 자세한 이유는 https://github.com/Youngminah/TIL/issues/124 TIL에 정리한 적이 있습니다.
인스턴스간의 강한참조순환을 막기 위해 Weak로!
화면전환의 관한 코디를 가지고 있을 클래스는 ViewModel입니다. 따라서, Delegate Protocol 사이의 인스턴스 강한참조 순환을 막기위해 프로토콜을 weak로 선언하는것 말고도 코디네이터와 ViewModel간의 즉, 인스턴스간의 강한 참조순환이 발생할 수 있습니다.
final class CertificationViewModel: ViewModelType {
private weak var coordinator: AuthCoordinator?
private let certificationUseCase: CertificationUseCase
...
}
코디네이터선언을 weak로 선언하지 않는다면 ViewModel이 Coordinator 참조하게 됩니다.
TMI로 뷰모델은 해당 화면 플로우를 담당하는 AuthCoordinator가 관리하는 화면들만 띄울 수 있습니다. 이말은 CertificationViewModel에선 AuthCoorinator가 아닌 TabbarCoordinator가 관리하는 화면들은 띄울 수 없습니다. 이런 경우에는 코디를 종료하고 상위코디로 가서 Tabbar코디를 이용하는 방법이 있습니다.
func showCertifacationViewController(verifyID: String) {
let viewModel = CertificationViewModel(
verifyID: verifyID,
coordinator: self,
certificationUseCase: CertificationUseCase(
userRepository: UserRepository(),
fireBaseRepository: FirebaseRepository(),
sesacRepository: SesacRepository()
)
)
let vc = CertificationViewController(viewModel: viewModel)
navigationController.setNavigationBarHidden(false, animated: false)
navigationController.pushViewController(vc, animated: true)
}
Coordinator가 ViewModel 참조하고 있는 형태라서 순환참조가 발생할 수 있는 구조입니다. 따라서 ViewModel에서 코디를 선언할때 약한 참조로 선언해주었습니다.
회고
작성중...
'🍒 iOS 개발 > 기술 포스팅' 카테고리의 다른 글
[SLP] iOS Clean Architecture + MVVM-C with Rxswift 포스팅 (4) | 2022.02.24 |
---|---|
[새싹 커뮤니티 앱] Rxswift + MVVM 기술 포스팅 (6) | 2022.01.07 |