Swift에서 Coordinator Pattern으로 UI 구성하는 방법 총정리

Coordinator Pattern이란?

Coordinator Pattern은 2015년 Soroush Khanlou가 고안한 디자인 패턴으로, UIViewController의 화면 전환 책임을 분리하여 앱의 네비게이션 흐름을 관리하는 패턴입니다[1][2]. 이 패턴은 MVC-C, MVVM-C 등 아키텍처 패턴 뒤에 붙는 ‘C’로 표현되며, ViewController 간의 결합도를 낮추고 코드의 재사용성을 높이는 것을 목표로 합니다[2][3].

왜 Coordinator Pattern을 사용하는가?

기존 방식의 문제점

기존의 ViewController에서 직접 화면 전환을 처리하는 방식은 여러 문제점을 가지고 있습니다[1][4]:

  • 중복 코드 발생: 동일한 ViewController로 전환하는 코드를 여러 곳에서 반복 작성해야 함
  • 강한 결합도: ViewController 간 서로 종속적인 관계로 인한 높은 결합도
  • 단일 책임 원칙 위반: ViewController가 UI 표시와 화면 전환이라는 두 가지 책임을 동시에 가짐
  • 테스트 어려움: 강한 결합으로 인해 단위 테스트 작성이 곤란함

Coordinator Pattern의 장점

Coordinator Pattern을 사용하면 다음과 같은 이점을 얻을 수 있습니다[2][5]:

  • 관심사 분리: ViewController는 UI 표시에만 집중하고, 화면 전환은 Coordinator가 담당
  • 느슨한 결합: ViewController 간 직접적인 의존성 제거
  • 재사용성 향상: 화면 전환 로직의 중앙 집중화로 재사용 용이
  • 테스트 용이성: 각 구성 요소를 독립적으로 테스트 가능
  • 유지보수성: 화면 전환 로직 변경 시 Coordinator만 수정하면 됨

기본 구조 및 구현 방법

1. Coordinator 프로토콜 정의

먼저 모든 Coordinator가 준수해야 할 기본 프로토콜을 정의합니다[6][7]:

protocol Coordinator: AnyObject {
    var navigationController: UINavigationController { get set }
    var childCoordinators: [Coordinator] { get set }

    init(navigationController: UINavigationController)
    func start()
}

주요 구성 요소:

  • navigationController: 화면 전환에 사용할 UINavigationController
  • childCoordinators: 하위 Coordinator들을 관리하는 배열
  • start(): Coordinator 시작 시점의 로직을 정의하는 메서드

2. 구체적인 Coordinator 구현

final class MainCoordinator: Coordinator {
    var navigationController: UINavigationController
    var childCoordinators = [Coordinator]()

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let viewController = ViewController.instantiate()
        viewController.coordinator = self
        navigationController.pushViewController(viewController, animated: false)
    }

    func showDetailView() {
        let detailCoordinator = DetailCoordinator(navigationController: navigationController)
        childCoordinators.append(detailCoordinator)
        detailCoordinator.start()
    }
}

3. Storyboard 인스턴스화 지원

ViewController를 쉽게 생성하기 위한 프로토콜을 정의합니다[8][4]:

protocol Storyboarded {
    static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        let fullName = NSStringFromClass(self)
        let className = fullName.components(separatedBy: ".")[1]
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        return storyboard.instantiateViewController(withIdentifier: className) as! Self
    }
}

4. AppDelegate/SceneDelegate 설정

앱의 진입점에서 최상위 Coordinator를 설정합니다[7][9]:

// AppDelegate에서 설정
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    var coordinator: MainCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let navController = UINavigationController()
        coordinator = MainCoordinator(navigationController: navController)
        coordinator?.start()

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = navController
        window?.makeKeyAndVisible()

        return true
    }
}

고급 구현 패턴

1. 부모-자식 Coordinator 관계

복잡한 앱에서는 여러 Coordinator를 계층적으로 구성할 수 있습니다[10][11]:

protocol CoordinatorFinishDelegate: AnyObject {
    func coordinatorDidFinish(childCoordinator: Coordinator)
}

extension Coordinator {
    func finish() {
        childCoordinators.removeAll()
        finishDelegate?.coordinatorDidFinish(childCoordinator: self)
    }
}

2. 탭바 구성을 위한 Coordinator

탭바 기반 앱에서는 각 탭마다 별도의 Coordinator를 구성할 수 있습니다[12][13]:

final class TabBarCoordinator: Coordinator {
    var navigationController: UINavigationController
    var childCoordinators = [Coordinator]()

    func start() {
        let tabBarController = UITabBarController()

        // 각 탭에 대한 Coordinator 생성
        let homeCoordinator = HomeCoordinator(navigationController: UINavigationController())
        let profileCoordinator = ProfileCoordinator(navigationController: UINavigationController())

        childCoordinators.append(contentsOf: [homeCoordinator, profileCoordinator])

        tabBarController.viewControllers = [
            homeCoordinator.navigationController,
            profileCoordinator.navigationController
        ]

        navigationController.pushViewController(tabBarController, animated: false)
    }
}

3. MVVM과의 결합

Coordinator Pattern은 MVVM 아키텍처와 함께 사용될 때 더욱 효과적입니다[14][12]:

final class DetailCoordinator: Coordinator {
    var navigationController: UINavigationController
    var childCoordinators = [Coordinator]()

    func start() {
        let viewModel = DetailViewModel()
        let viewController = DetailViewController(viewModel: viewModel)
        viewController.coordinator = self
        navigationController.pushViewController(viewController, animated: true)
    }
}

데이터 전달 방법

1. 부모에서 자식으로 데이터 전달

초기화 시점에 필요한 데이터를 전달합니다[15]:

final class DetailCoordinator: Coordinator {
    private let itemId: String

    init(navigationController: UINavigationController, itemId: String) {
        self.navigationController = navigationController
        self.itemId = itemId
    }

    func start() {
        let viewModel = DetailViewModel(itemId: itemId)
        let viewController = DetailViewController(viewModel: viewModel)
        navigationController.pushViewController(viewController, animated: true)
    }
}

2. 자식에서 부모로 데이터 전달

Delegate 패턴이나 Closure를 활용하여 데이터를 전달합니다[15]:

protocol DetailCoordinatorDelegate: AnyObject {
    func detailDidComplete(with result: String)
}

final class DetailCoordinator: Coordinator {
    weak var delegate: DetailCoordinatorDelegate?

    func completeWithResult(_ result: String) {
        delegate?.detailDidComplete(with: result)
        finish()
    }
}

SwiftUI에서의 Coordinator Pattern

SwiftUI에서도 Coordinator Pattern을 적용할 수 있습니다[16]:

enum AuthenticationFlow: CaseIterable {
    case login, signup, forgotPassword
}

class AuthenticationCoordinator: ObservableObject {
    @Published var currentView: AuthenticationFlow = .login

    func showLogin() {
        currentView = .login
    }

    func showSignup() {
        currentView = .signup
    }

    func showForgotPassword() {
        currentView = .forgotPassword
    }
}

주의사항 및 모범 사례

메모리 관리

Child Coordinator를 적절히 관리하여 메모리 누수를 방지해야 합니다[6][10]:

func removeChildCoordinator(child: Coordinator) {
    childCoordinators.removeAll { $0 === child }
}

적절한 복잡도 유지

간단한 앱에서는 Coordinator Pattern이 오히려 복잡성을 증가시킬 수 있으므로, 앱의 복잡도에 따라 적절히 적용해야 합니다[2].

Navigation Controller Delegate 활용

복잡한 네비게이션 흐름에서는 UINavigationControllerDelegate를 활용하여 뒤로 가기 이벤트를 처리할 수 있습니다[8].

결론

Coordinator Pattern은 iOS 앱의 네비게이션 로직을 체계적으로 관리할 수 있는 강력한 패턴입니다[17]. 특히 복잡한 화면 전환 흐름을 가진 앱에서 ViewController 간의 결합도를 낮추고, 코드의 재사용성과 테스트 용이성을 크게 향상시킬 수 있습니다[2][5].

다만, 앱의 복잡도와 팀의 상황을 고려하여 적절히 적용하는 것이 중요하며, 초기 학습 비용과 추가적인 코드 작성이 필요하다는 점을 감안해야 합니다[2]. 올바르게 구현된 Coordinator Pattern은 유지보수가 용이하고 확장 가능한 iOS 앱 아키텍처의 기반이 될 수 있습니다.

출처
[1] 코디네이터 패턴(Coordinator Pattern) – 코딩하는 체대생 https://mini-min-dev.tistory.com/264
[2] [디자인 패턴] Coordinator Pattern – velog https://velog.io/@jeon0976/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Coordinator-Pattern
[3] [iOS] Coordinator Pattern – 시원한 코드 기록 – 티스토리 https://siwon-code.tistory.com/38
[4] [iOS] Coordinator Pattern – Clamp – 티스토리 https://clamp-coding.tistory.com/452
[5] Swift Coordinator Pattern: Step-by-Step Guide for iOS App … https://all-win-solutions.com/swift-coordinator-pattern-ios/
[6] [Swift] Coordinator Pattern https://jintaewoo.tistory.com/58
[7] iOS Coordinator 패턴 (개념 ~ 템플릿 적용 방법) – sookim-1 블로그 https://sookim-1.tistory.com/entry/iOS-Coordinator-Pattern
[8] hyun99999/CoordinatorTutorial-iOS: Coordinator 패턴의 유쾌한 반란 https://github.com/hyun99999/CoordinatorTutorial-iOS
[9] iOS) Coordinator pattern 적용해보자 – Basic – 코드너리 https://www.codenary.co.kr/discoveries/1810
[10] [새싹 iOS] 25주차_Coordinator Pattern 적용기 – velog https://velog.io/@s_sub/%EC%83%88%EC%8B%B9-iOS-25%EC%A3%BC%EC%B0%A8
[11] 간단한 예제로 살펴보는 iOS Design/Architecture Pattern: Coordinator https://lena-chamna.netlify.app/post/ios_design_pattern_coordinator_advanced/
[12] [Swift] Coordinator & Router & Factory Pattern을 사용한 리팩토링 (1) https://mini-min-dev.tistory.com/306
[13] [Swift] Coordinator & Router & Factory Pattern을 사용한 리팩토링 (2) https://mini-min-dev.tistory.com/307
[14] [Design Pattern] Coordinator 패턴 – seokyoung – 티스토리 https://seokyoungg.tistory.com/100
[15] GitHub – guoyingtao/CoordinatorPatternExample: My practice of applying coordinator pattern in an iOS project https://github.com/guoyingtao/CoordinatorPatternExample
[16] How to use Coordinator Pattern in SwiftUI – Swift Anytime https://www.swiftanytime.com/blog/coordinator-pattern-in-swiftui
[17] Coordinator Pattern for Navigation Management in Swift https://softwarepatternslexicon.com/patterns-swift/7/5/
[18] [iOS] Coordinator Pattern을 사용해보자 – @EddiOS – 티스토리 https://developer-eddy403.tistory.com/55
[19] Swift Tutorial: How to use Coordinator Pattern with MVVM – YouTube https://www.youtube.com/watch?v=wpw3l_jTuOo
[20] Coordinator Pattern – velog https://velog.io/@toma/Coordinator-Pattern
[21] SwiftUI 프로젝트에 AppDelegate, SceneDelegate 만들기 https://emptyhead.oopy.io/7aee0f49-0455-4270-8e1f-8d605d85f3c8
[22] Delegate in iOS https://intellipaat.com/blog/tutorial/ios-tutorial/delegate-ios/
[23] The basics of using the delegation pattern https://dev.to/bornfightcompany/the-basics-of-using-the-delegation-pattern-41gh
[24] Coordinator Pattern 도입 이유와 실제 도입기 (ft. 객체들의 책임과 unit … https://codingmon.tistory.com/64
[25] iOS) Coordinator pattern 적용해보자 – Basic – hyun9iOS – 티스토리 https://gyuios.tistory.com/84
[26] [iOS/Architecture] Coordinator Pattern – 제인로그 – 티스토리 https://janechoi.tistory.com/23

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다