Coordinator(코디네이터) — 화면 간 전환 로직을 ViewController에서 별도 클래스로 분리하는 아키텍처 네비게이션 패턴입니다. 이 패턴은 2015년 Soroush Khanlou가 제안했으며 iOS 커뮤니티에서 널리 채택되었습니다. Coordinator는 애플리케이션 흐름을 관리합니다: ViewController를 생성 및 표시하고, 화면 간 데이터를 전달하며, 흐름 완료를 처리합니다. 이 패턴은 컨트롤러에서 네비게이션을 분리하여 Massive View Controller 문제를 해결합니다. 자세한 내용은 Coordinator 원본 기사를 참조하세요.
핵심 요점
Coordinator — iOS 애플리케이션에서 네비게이션 책임을 담당하는 패턴입니다. 표준 UIKit에서는 ViewController 자체가 전환을 관리합니다: present, push, show segue — 모든 네비게이션 메서드가 UIViewController에서 호출됩니다. Coordinator는 이 로직을 분리합니다: ViewController가 이벤트를 보고하면(예: «사용자가 로그인 버튼을 클릭»), Coordinator가 다음에 표시할 화면을 결정합니다. ViewController는 UI 로직만 유지하고 네비게이션을 코디네이터에 위임합니다.
패턴 구조 — start() 및 finish() 메서드가 있는 CoordinatorProtocol. start() — 흐름의 시작: 첫 번째 ViewController 생성 및 표시. finish() — 상위 코디네이터에 알림과 함께 흐름 완료. Router — UINavigationController(또는 UISplitViewController)를 래핑하여 show, push, pop, dismiss 메서드를 제공합니다. 코디네이터는 Router를 통해서만 UINavigationController와 직접 작업하지 않습니다. 이를 통해 네비게이션 테스트가 가능하고 UI 프레임워크를 전환할 수 있습니다.
| 컴포넌트 | 역할 | 예시 |
|---|---|---|
| Coordinator | 네비게이션 흐름 관리 | AuthCoordinator, ProfileCoordinator |
| Router | UINavigationController 추상화 | push, present, pop, dismiss |
| ViewController | UI + Coordinator로 이벤트 위임 | LoginViewController.delegate |
Coordinator가 해결하는 문제 — Massive View Controller(네비게이션은 컨트롤러 비대화의 일반적인 원인). 표준 UIKit에서 ViewController는 prepareForSegue, 네비게이션 델리게이트, unwind segue 처리를 포함합니다. Coordinator는 이를 제거합니다. Storyboard의 Segue는 화면 간 정적 연결이지만, Coordinator는 조건부 동적 네비게이션을 제공합니다. Router 호출 순서를 확인하여 UI 없이 Coordinator를 테스트할 수 있습니다.
Swift의 기본 Coordinator — Router용 연관 타입과 start/finish 메서드가 있는 프로토콜. Router — UINavigationController를 추상화하는 프로토콜. Router의 구체적 구현은 UINavigationController를 래핑하고 메서드를 위임합니다. Coordinator는 init에서 Router를 받아 네비게이션에 사용합니다. 하위 코디네이터는 라이프사이클 관리를 위해 childCoordinators 배열에 저장됩니다.
// Router — 네비게이션 추상화
protocol RouterProtocol: AnyObject {
func push(_ viewController: UIViewController, animated: Bool)
func pop(animated: Bool)
func present(_ viewController: UIViewController, animated: Bool)
func dismiss(animated: Bool)
}
final class NavigationRouter: RouterProtocol {
private let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func push(_ vc: UIViewController, animated: Bool) {
navigationController.pushViewController(vc, animated: animated)
}
func pop(animated: Bool) {
navigationController.popViewController(animated: animated)
}
func present(_ vc: UIViewController, animated: Bool) {
navigationController.present(vc, animated: animated)
}
func dismiss(animated: Bool) {
navigationController.dismiss(animated: animated)
}
}
// Coordinator — 흐름 관리
protocol CoordinatorProtocol: AnyObject {
var childCoordinators: [CoordinatorProtocol] { get set }
var router: RouterProtocol { get }
func start()
func finish()
}
class AuthCoordinator: CoordinatorProtocol {
var childCoordinators: [CoordinatorProtocol] = []
let router: RouterProtocol
init(router: RouterProtocol) {
self.router = router
}
func start() {
let loginVC = LoginViewController()
loginVC.onLogin = { [weak self] in
self?.showHome()
}
router.push(loginVC, animated: true)
}
private func showHome() {
let homeCoordinator = HomeCoordinator(router: router)
childCoordinators.append(homeCoordinator)
homeCoordinator.start()
}
func finish() {
childCoordinators.removeAll()
router.pop(animated: true)
}
}
AppDelegate/SceneDelegate에서 Coordinator 생성 — AppDelegate 또는 SceneDelegate가 UINavigationController를 생성하고 NavigationRouter로 래핑한 후, 루트 Coordinator(AppCoordinator)를 생성하고 start()를 호출합니다. AppCoordinator는 애플리케이션 상태에 따라 온보딩, 로그인 또는 메인 화면을 표시할지 결정합니다. Coordinator는 네비게이션의 유일한 진입점이며, ViewController는 다른 화면에 대해 알지 못합니다.
코디네이터 계층 구조 — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. 하위 코디네이터는 상위에 의해 생성되어 childCoordinators 배열에 저장됩니다. 하위 코디네이터가 작업을 완료하면 상위의 finish()를 호출하고, 상위는 childCoordinators에서 제거합니다. 이는 메모리 누수를 방지합니다: Coordinator는 Router를 통해 ViewController에 강한 참조를 가지며, childCoordinators에서 제거되지 않으면 객체가 해제되지 않습니다.
// Coordinator -> Parent 통신용 Delegate
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — 상위
class AppCoordinator: AuthCoordinatorDelegate {
func startAuthFlow() {
let authCoordinator = AuthCoordinator(router: router)
authCoordinator.delegate = self
childCoordinators.append(authCoordinator)
authCoordinator.start()
}
func authCoordinatorFinished(_ coordinator: AuthCoordinator) {
childCoordinators.removeAll { $0 is AuthCoordinator }
startMainFlow()
}
}
childCoordinators 관리 — 배열에서 Coordinator를 제거하는 것이 해제 유일한 방법입니다. 완료된 Coordinator를 제거하지 않으면 ViewController와 함께 메모리에 남아 있습니다. 권장 접근법: 상위의 didMove(toParent:), 완료 시 callback, 또는 자동 제거를 위한 Combine publisher. Coordinator 패턴은 알림 메커니즘을 지정하지 않으며 — delegate, closure 또는 Combine — 선택은 개발자에게 달려 있습니다.
delegate를 통한 데이터 전달 — 하위 Coordinator가 delegate 프로토콜을 정의하고 메서드를 통해 결과를 전달합니다: func authCoordinator(_:didLoginWith user: User). 상위가 프로토콜을 구현하고 하위 흐름 완료 시 데이터를 받습니다. 타입 안전하고 명시적입니다. 단점: 각 하위 Coordinator마다 별도 프로토콜이 필요합니다. 10개 이상의 Coordinator가 있는 프로젝트에서는 파일 수가 증가합니다.
Result 타입을 통한 데이터 전달 — finish 메서드가 Result
| 방법 | 장점 | 단점 |
|---|---|---|
| Delegate | 타입 안전, 명시적, 별도 프로토콜 | 많은 프로토콜, 많은 보일러플레이트 |
| Closure | 간결, 파일 수 감소 | retain cycle 디버깅 어려움 |
| Combine/Rx | 반응형, 조합 용이 | 라이브러리 의존성, 디버깅 복잡 |
공유 데이터 계층 — Coordinator는 데이터를 직접 전달하지 않고 공유 서비스/리포지토리를 사용합니다. AuthCoordinator는 Keychain/UserDefaults에 토큰을 저장하고, ProfileCoordinator는 거기서 읽습니다. Coordinator는 직접 호출 대신 공유 상태(Dependency Injection 컨테이너)를 통해 통신합니다. 이는 Coordinator 간 결합도를 낮추지만 공유 상태에 대한 암시적 의존성을 만듭니다.
Coordinator vs Router — Router는 UINavigationController를 추상화하는 Coordinator의 컴포넌트입니다. Coordinator는 흐름(어떤 화면을 표시할지)을 담당하고, Router는 메커니즘(표시 방법: push/present)을 담당합니다. Router는 «방법», Coordinator는 «무엇»입니다. Router는 Coordinator 없이 사용할 수 있지만(예: Navigator 싱글톤), Coordinator를 Router 없이 사용하면 단순히 다른 추상화를 가진 ViewController입니다. 일반적으로 두 패턴을 함께 사용합니다.
Coordinator vs VIPER — VIPER에는 네비게이션을 담당하는 Wireframe 컴포넌트가 있으며 Coordinator와 유사합니다. VIPER에서 Wireframe은 모듈의 일부이지만, Coordinator는 모듈 위의 별도 계층입니다. VIPER 모듈(View-Interactor-Presenter-Entity-Router)은 네비게이션을 모듈의 일부로 포함합니다. Coordinator는 모듈 외부에 있으며 모듈을 생성하고 연결하지만 모듈의 일부는 아닙니다. Coordinator는 다른 흐름에서 화면 재사용에 더 유연합니다.
MVVM-C — Coordinator를 사용한 MVVM 확장. ViewModel은 Coordinator를 직접 알지 못합니다 — ViewController가 ViewModel을 통해 네비게이션을 위임하고, ViewModel이 프로토콜을 통해 coordinator를 호출합니다. MVVM-C는 SwiftUI를 사용하는 iOS 프로젝트의 표준 접근법입니다. Coordinator는 NavigationStack 또는 fullScreenCover를 관리하고, ViewModel은 상태를 게시하여 coordinator를 호출합니다. Apple은 SwiftUI에서 Coordinator를 권장하지 않습니다 — NavigationStack과 NavigationPath는 내장 네비게이션 메커니즘입니다.
// MVVM-C: ViewModel이 프로토콜을 통해 Coordinator 호출
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// 로직...
navigation?.showMainScreen()
}
}
자주 묻는 질문
SwiftUI의 경우 내장 네비게이션(NavigationStack, NavigationPath)이 종종 Coordinator를 대체합니다. Apple은 path-based 네비게이션을 권장합니다. Coordinator는 복잡한 조건이 있는 흐름(역할에 따른 온보딩-로그인-메인 화면)에 의미가 있습니다. 간단한 SwiftUI 애플리케이션에서는 Coordinator가 과도합니다 — NavigationPath를 사용하세요.
아니요, 다른 패턴입니다. Coordinator는 네비게이션 흐름을 관리합니다: 표시할 화면을 결정하고 ViewController를 생성 및 연결합니다. Router는 UINavigationController의 추상화입니다: push, present, pop, dismiss. Coordinator는 Router를 사용하여 네비게이션을 수행합니다. 일부 구현에서 Router가 Coordinator 로직을 포함하지만(Router-per-screen), 이는 원래 패턴에서 벗어납니다.
두 가지 주요 누수 원인: childCoordinators(상위가 하위를 유지하며 제거를 잊음)와 Router(UINavigationController가 ViewController를 유지). 해결책: finish() 시 하위 Coordinator를 항상 배열에서 제거. delegate에는 weak 참조 사용. Router의 경우 — UINavigationController가 이미 window 계층에 있으면 강한 참조를 유지하지 마세요. Coordinator의 deinit을 테스트하세요.
3-5개 화면 애플리케이션에서는 Coordinator가 과도합니다 — segue나 단순한 navigationController.pushViewController가 더 쉽습니다. NavigationStack을 사용하는 SwiftUI 애플리케이션에서도 과도합니다. Coordinator는 15개 이상의 화면, 복잡한 흐름(분기가 있는 온보딩, 비밀번호 복구가 있는 인증) 및 혼합 UIKit/SwiftUI 프로젝트에서 정당화됩니다.
Mock Router — 어떤 메서드가 어떤 매개변수로 호출되는지 확인. childCoordinators 확인: start() 후 배열이 비어 있지 않음, finish() 후 — 비어 있음. Coordinator는 UI 없이 테스트됩니다: Router는 프로토콜이며 mock은 UIKit이 필요 없습니다. 비동기 흐름에는 XCTestExpectation을 사용하세요. Android에서도 mock 네비게이션으로 NavigationController와 NavHost를 유사하게 테스트합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.