[Swift] Coordinator

팔랑이·2025년 4월 27일

iOS/Swift

목록 보기
71/84

기존 ViewController 중심 화면 전환의 한계

  • ViewController가 UI + 화면 전환을 동시에 담당
  • 다음 화면을 직접 생성해야 함
  • 로그인 / 온보딩 / 메인 같은 앱 진입 플로우 분기가 여러 VC에 흩어짐
  • 플로우 변경 시 여러 파일 수정 필요
  • 테스트가 어려움 (navigation stack이 VC 내부에 묶임)

문제의 핵심은
“화면 전환이 UI 로직과 결합되어 있다”는 점


Coordinator의 기본 개념

Coordinator의 역할

  • ViewController에서 화면 전환 책임을 분리
  • 화면 흐름(flow)을 하나의 객체에서 관리
  • ViewController는 “무슨 일이 일어났는지”만 알림

가장 중요한 원칙

  • ViewController는 다음 화면을 모른다
  • Coordinator는 UI를 그리지 않는다
  • Coordinator는 오직 navigation 결정만 담당

Coordinator 기본 구조

CoordinatorProtocol

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: 현재 활성화된 하위 플로우들

핵심 포인트

  • Coordinator는 트리 구조
  • 각 Coordinator는 자기 바로 아래 단계만 관리

App Entry Flow 설계

앱 실행 시 고려해야 할 상태

  • 로그인 안 됨 → Login Flow
  • 로그인 됐지만 첫 진입 → Onboarding Flow
  • 로그인 + 온보딩 완료 → Main(TabBar) Flow

이를 해결한 방식

  • FirstCoordinator앱의 단일 진입점(router)으로 둠
  • 모든 분기 로직을 한 곳에 집중
FirstCoordinator
 ├─ LoginCoordinator
 ├─ OnboardingCoordinator
 └─ TabbarCoordinator

“앱에서 사용자가 어디로 가야 하는지”에 대한
단 하나의 Source of Truth


상세내용

왜 ViewController는 coordinator를 weak로 가지는가

weak var coordinator: LoginCoordinator?
  • Coordinator는 navigationController를 통해 VC를 강하게 소유

  • VC가 coordinator를 strong으로 잡으면 순환 참조 발생

  • 따라서:

    • 부모 → 자식: strong
    • 자식 → 부모: weak

UITableViewCell – delegate 패턴과 동일한 구조


childCoordinators의 역할

  1. 부모 Coordinator가 childCoordinator를 배열에서 제거
  2. Coordinator에 대한 strong reference 사라짐
  3. navigation stack 교체로 VC가 제거됨
  4. ARC에 의해 Coordinator / VC / ViewModel 해제

finish()의 의미

func finish() {
    childCoordinators.forEach { $0.finish() }
    childCoordinators.removeAll()
    parentCoordinator?.removeChild(self)
}
  • “이 플로우는 더 이상 소유되지 않는다”는 선언
  • 메모리 누수를 구조적으로 방지

childCoordinators가 없을 때의 실제 문제

  1. 메모리 관리 (Retain Cycle 방지)
    weak var coordinator: LoginCoordinator? // ViewController는 coordinator를 weak 참조

ViewController는 Coordinator를 강하게 참조하지 않습니다.
따라서 childCoordinators가 없다면, Coordinator는 생성 직후 바로 해제될 수 있습니다.

func showLoginFlow() {
let coordinator = LoginCoordinator(...) // 생성
coordinator.start()
// childCoordinators에 저장하지 않으면 → 여기서 바로 dealloc
}

childCoordinators 배열은 플로우가 유지되는 동안 Coordinator를 살아 있게 유지하는 역할을 합니다.

  1. 플로우 전환 시 올바른 정리(Cleanup)
    func didLoggedOut() {
    clearChildCoordinators() // TabbarCoordinator와 그 하위 플로우 정리
    showLoginFlow()
    }

func clearChildCoordinators() {
childCoordinators.forEach { $0.finish() } // 재귀적으로 정리
childCoordinators.removeAll()
}

메인 → 로그인으로 전환할 때:

탭바 코디네이터 및 모든 하위 코디네이터가 정상적으로 종료됨

연결된 ViewController들이 해제(deinit)될 수 있음

고아(orphan) 코디네이터로 인한 메모리 누수 방지

  1. 부모–자식 관계 추적 및 상위로의 커뮤니케이션
    func addChild(_ coordinator: CoordinatorProtocol) {
    childCoordinators.append(coordinator)
    coordinator.parentCoordinator = self // 양방향 연결
    }

이 구조를 통해 자식 코디네이터는 상위 코디네이터에게 상태를 전달할 수 있습니다.

(parentCoordinator as? FirstCoordinator)?.didLoggedIn()

즉, 플로우 단위의 의사결정은 항상 상위에서 이루어지게 됩니다.

  1. 계층적인 플로우 관리 (Hierarchical Flow Control)
    FirstCoordinator.childCoordinators = [TabbarCoordinator]
    └── TabbarCoordinator.childCoordinators =
    [HomeCoordinator, ArchiveCoordinator, ...]

각 Coordinator는 자기 바로 아래 단계만 관리합니다.

이 구조의 장점:

부모는 손자(grandchild)를 알 필요 없음

각 레벨이 자신의 책임 범위만 관리

전체 앱 플로우가 트리 구조로 명확하게 표현됨


정리

  • Coordinator는 화면 이동을 분리하기 위한 도구가 아니라
    앱의 흐름을 구조화하기 위한 아키텍처 요소

  • ViewController는 UI에 집중하고,
    Coordinator는 “다음에 어디로 갈지”만 책임진다

  • 이 구조를 통해:

    • 플로우 변경이 쉬워지고
    • 테스트 가능성이 높아지고
    • 메모리 누수를 구조적으로 예방할 수 있다
profile
정체되지 않는 성장

0개의 댓글