📚 Coordinator 패턴이란?
네비게이션 로직을 View Controller에서 분리하여 별도의 객체가 담당하도록 하는 아키텍처 패턴
기존 방식의 문제점
- View Controller가 다른 View Controller를 직접 생성하고 present/push
- 네비게이션 로직이 여러 곳에 분산되어 관리가 어려움
- View Controller 간의 강한 결합
Coordinator 패턴의 개념들
1. childCoordinators 배열의 역할
var childCoordinators = [Coordinator]() // 메모리 관리를 위한 배열
- 메모리 관리를 위한 것
- 배열에 추가 = 메모리에서 해제되지 않게 유지
- 배열에서 제거 = 메모리에서 해제
2. coordinator = self를 하는 이유
let homeVC = HomeViewController()
homeVC.coordinator = self // ViewController에게 "나(Coordinator)가 너의 관리자야!" 알려줌
- ViewController가 어떤 Coordinator에게 네비게이션을 요청해야 할지 알려주기 위함
- 안 하면 ViewController에서 네비게이션 요청 불가능
3. remove vs pop - 두 가지 다른 일
- pop: NavigationController가 화면을 stack에서 제거 (사용자에게 보이는 부분)
- remove: childCoordinators 배열에서 제거해서 메모리 해제 (개발자가 관리하는 부분)
- 둘 다 필요함! pop만 하면 메모리 누수, remove만 하면 화면이 안 사라짐
4. Parent-Child 관계 결정 기준
- 누가 누구를 생성하느냐에 따라 결정
- 누가 addChildCoordinator()를 호출하느냐 = Parent
- 누가 childCoordinators 배열에 저장되느냐 = Child
🏗️ 영화앱 프로젝트 적용 구조
1. Protocol + Base Class 구조
// 1️⃣ Protocol Layer (인터페이스)
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
func finish()
}
// 2️⃣ Base Layer (공통 기능)
class BaseCoordinator: Coordinator {
// 공통 기능들 구현 (addChildCoordinator, removeChildCoordinator, showErrorAlert 등)
}
// 3️⃣ Implementation Layer (실제 구현)
class AppCoordinator: BaseCoordinator { ... }
장점: 코드 재사용성, 일관성, 확장성
2. 계층 구조
AppCoordinator (Root - 최상위)
├── AuthCoordinator (로그인/회원가입)
└── MainTabCoordinator (탭바 + 각 탭 관리)
├── MovieListCoordinator (영화 목록)
│ └── MovieDetailCoordinator (영화 상세)
├── SearchCoordinator (검색)
│ └── MovieDetailCoordinator (영화 상세)
└── MyPageCoordinator (마이페이지)
3. Parent-Child 관계 예시
// AppCoordinator가 AuthCoordinator 생성
private func showAuth() {
let authCoordinator = AuthCoordinator(navigationController: navigationController)
authCoordinator.delegate = self
addChildCoordinator(authCoordinator) // Parent-Child 관계 성립
authCoordinator.start()
}
4. Delegate 패턴으로 통신
protocol AuthCoordinatorDelegate: AnyObject {
func didFinishLogin(_ coordinator: AuthCoordinator)
}
extension AppCoordinator: AuthCoordinatorDelegate {
func didFinishLogin(_ coordinator: AuthCoordinator) {
removeChildCoordinator(coordinator) // Auth 제거
showMainTab() // MainTab으로 전환
}
}
5. Repository 패턴과 결합
class MovieListCoordinator: BaseCoordinator {
private let movieRepository: MovieRepository
init(navigationController: UINavigationController, movieRepository: MovieRepository) {
self.movieRepository = movieRepository
super.init(navigationController: navigationController)
}
}
📊 실제 동작 흐름
앱 시작 → 로그인 → 메인 화면
- AppCoordinator 생성
- AuthCoordinator 생성 및 시작
- 로그인 완료 → delegate로 AppCoordinator에 알림
- AuthCoordinator 제거 + MainTabCoordinator 생성
- 각 탭별 Coordinator들 생성 (MovieList, Search, MyPage)
영화 목록 → 영화 상세 → 뒤로가기
- MovieListCoordinator.showMovieDetail() 호출
- MovieDetailCoordinator 생성 (자식으로 추가)
- 뒤로가기 → MovieDetailCoordinator.finish() 호출
- 부모(MovieListCoordinator)에서 자신을 제거
🚨 현재 구조의 한계점과 개선 방향
1. 일관성 없는 Child 제거 방식
문제:
- MovieDetailCoordinator는 finish()에서 부모에게 제거 요청
- AuthCoordinator는 delegate로 부모가 직접 제거
개선안:
// BaseCoordinator에 일관된 finish() 메서드 추가
override func finish() {
super.finish() // 자신의 children 정리
parentCoordinator?.removeChildCoordinator(self) // 일관된 제거 방식
}
2. 메모리 누수 위험
문제: delegate들이 제대로 nil 처리되지 않을 수 있음
개선안:
deinit {
// delegate nil 처리
if let authCoordinator = self as? AuthCoordinator {
authCoordinator.delegate = nil
}
print("🗑️ \(type(of: self)) 메모리에서 해제됨")
}
3. Delegate 체인의 복잡성
문제: 로그아웃 같은 간단한 동작이 3단계를 거쳐야 함
MyPageCoordinator → MainTabCoordinator → AppCoordinator
개선안: Event Bus 패턴이나 더 직접적인 통신 방식 고려
무엇보다.. 프로젝트 규모에 비해 너무 무거운 구조가 된것 같아 다음 번엔 규모에 맞는 조금 더 가벼운 구조로 coordinator를 만들어 보고싶다
참고 자료
'iOS' 카테고리의 다른 글
[iOS] RxSwift + Swift Testing (2) | 2025.07.31 |
---|---|
[iOS] Factory Pattern (3) | 2025.07.24 |
[트러블슈팅] UICollectionViewCell 그림자가 초기 렌더링 시 안 보이는 문제 (0) | 2025.07.18 |
[iOS] Coordinator 패턴으로 화면 전환 로직 분리하기 (0) | 2025.07.10 |
[iOS] 클로저에서 async/await로 리팩토링하기 (1) | 2025.07.08 |