iOS

[iOS] Coordinator 패턴 적용기

양밀루 2025. 7. 22. 20:25

📚 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)
    }
}

📊 실제 동작 흐름

앱 시작 → 로그인 → 메인 화면

  1. AppCoordinator 생성
  2. AuthCoordinator 생성 및 시작
  3. 로그인 완료 → delegate로 AppCoordinator에 알림
  4. AuthCoordinator 제거 + MainTabCoordinator 생성
  5. 각 탭별 Coordinator들 생성 (MovieList, Search, MyPage)

영화 목록 → 영화 상세 → 뒤로가기

  1. MovieListCoordinator.showMovieDetail() 호출
  2. MovieDetailCoordinator 생성 (자식으로 추가)
  3. 뒤로가기 → MovieDetailCoordinator.finish() 호출
  4. 부모(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를 만들어 보고싶다

 

 

참고 자료