문제의 핵심은
“화면 전환이 UI 로직과 결합되어 있다”는 점
public protocol CoordinatorProtocol: AnyObject {
var childCoordinators: [CoordinatorProtocol] { get set }
var navigationController: UINavigationController { get set }
var parentCoordinator: CoordinatorProtocol? { get set }
func start()
func finish()
}
start(): 플로우 시작finish(): 플로우 종료childCoordinators: 현재 활성화된 하위 플로우들FirstCoordinator를 앱의 단일 진입점(router)으로 둠FirstCoordinator
├─ LoginCoordinator
├─ OnboardingCoordinator
└─ TabbarCoordinator
“앱에서 사용자가 어디로 가야 하는지”에 대한
단 하나의 Source of Truth
weak로 가지는가weak var coordinator: LoginCoordinator?
Coordinator는 navigationController를 통해 VC를 강하게 소유
VC가 coordinator를 strong으로 잡으면 순환 참조 발생
따라서:
UITableViewCell – delegate 패턴과 동일한 구조
func finish() {
childCoordinators.forEach { $0.finish() }
childCoordinators.removeAll()
parentCoordinator?.removeChild(self)
}
ViewController는 Coordinator를 강하게 참조하지 않습니다.
따라서 childCoordinators가 없다면, Coordinator는 생성 직후 바로 해제될 수 있습니다.
func showLoginFlow() {
let coordinator = LoginCoordinator(...) // 생성
coordinator.start()
// childCoordinators에 저장하지 않으면 → 여기서 바로 dealloc
}
childCoordinators 배열은 플로우가 유지되는 동안 Coordinator를 살아 있게 유지하는 역할을 합니다.
func clearChildCoordinators() {
childCoordinators.forEach { $0.finish() } // 재귀적으로 정리
childCoordinators.removeAll()
}
메인 → 로그인으로 전환할 때:
탭바 코디네이터 및 모든 하위 코디네이터가 정상적으로 종료됨
연결된 ViewController들이 해제(deinit)될 수 있음
고아(orphan) 코디네이터로 인한 메모리 누수 방지
이 구조를 통해 자식 코디네이터는 상위 코디네이터에게 상태를 전달할 수 있습니다.
(parentCoordinator as? FirstCoordinator)?.didLoggedIn()
즉, 플로우 단위의 의사결정은 항상 상위에서 이루어지게 됩니다.
각 Coordinator는 자기 바로 아래 단계만 관리합니다.
이 구조의 장점:
부모는 손자(grandchild)를 알 필요 없음
각 레벨이 자신의 책임 범위만 관리
전체 앱 플로우가 트리 구조로 명확하게 표현됨
Coordinator는 화면 이동을 분리하기 위한 도구가 아니라
앱의 흐름을 구조화하기 위한 아키텍처 요소
ViewController는 UI에 집중하고,
Coordinator는 “다음에 어디로 갈지”만 책임진다
이 구조를 통해: