Project/TalkTalk

[iOS-1] Coordinator 패턴을 사용한 화면 전환 관리

생각 깎는 아이 2025. 3. 28. 22:03

Coordinator 패턴이란?

Coordinator 패턴은 화면 전환 로직을 ViewController로부터 분리하여 별도의 객체(Coordinator)에게 위임하는 아키텍처 패턴입니다. 이 패턴을 통해 ViewController는 UI 표시와 사용자 입력 처리에만 집중할 수 있고, 복잡한 화면 전환과 앱의 전체 흐름은 Coordinator가 관리하게 됩니다.

Coordinator 패턴 구현 

Coordinator 프로토콜을 정의하여 각 화면을 관리하는 Coordinator들이 반드시 포함해야할 구현을 강제합니다. 

/// Coordinator 패턴을 구현하기 위한 기본 프로토콜
/// 앱의 화면 흐름을 관리하고 뷰 컨트롤러 간 전환 로직을 캡슐화합니다.
protocol Coordinator: AnyObject {
    /// 현재 Coordinator가 관리하는 자식 Coordinator들의 배열
    /// 계층적 흐름 관리와 메모리 관리를 위해 사용됩니다.
    var childCoordinators: [Coordinator] { get set }
    
    /// Coordinator가 화면 전환에 사용하는 네비게이션 컨트롤러
    /// 뷰 컨트롤러 푸시, 팝 등의 전환을 담당합니다.
    var navigationController: UINavigationController { get set }
    
    /// Coordinator의 화면 흐름을 시작하는 메서드
    /// 일반적으로 첫 번째 뷰 컨트롤러를 네비게이션 스택에 추가합니다.
    func start()
}

 

각 계층의 화면 Flow를 관리하는 Coordinator들은 위에서 정의한 프로토콜을 채택하고 구현함으로써 화면 계층과 화면간 전환에 대한 실제 방법을 구현합니다. 

/// 앱의 최상위 Coordinator
/// 각 화면은 담당 Coordinator들이 관리하고 여기서는 Coordinator들을 관리
class AppCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    private let window: UIWindow
    
    func start() {
        window.rootViewController = navigationController
        window.makeKeyAndVisible()
        
        // 로그인 상태에 따라 적절한 flow 보여주기
        if isUserLoggedIn() {
            showMainFlow()
        } else {
            showAuthFlow()
        }
    }
	...
}

/// 인증 흐름 관리
class AuthCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    weak var delegate: AuthCoordinatorDelegate?
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        let loginVC = LoginViewController()
        loginVC.coordinator = self
        // 애니메이션과 함께 stack 교체
        navigationController.setViewControllers([loginVC], animated: true)
    }
    
    func goToRegister() {
        let registerVC = RegisterViewController()
        registerVC.coordinator = self
        navigationController.pushViewController(registerVC, animated: true)
    }
    
    func didFinishLogin() {
        delegate?.didFinishAuth(self)
    }
    
    func didFinishRegister() {
        delegate?.didFinishAuth(self)
    }
}

 

Coordinator 패턴에서는 ViewController는 화면 전환에 대한 책임을 모두 Coordinator에게 전가합니다. 따라서 CoordinatedViewController라는 프로토콜을 정의하고 내부 프로퍼티로 coordinator를 두어서 모든 ViewController들이 coordinator를 통해 화면 전환을 하도록 강제합니다. 

// 모든 화면에 공통으로 적용되는 기본 프로토콜
protocol CoordinatedViewController: UIViewController {
    associatedtype CoordinatorType: Coordinator
    var coordinator: CoordinatorType? { get set }
}

 

각 ViewController에 요구되는 화면 전환 종류를 명시하는 프로토콜을 정의하고 여기에 CoordinatedViewController를 채택과 동시에 where절을 통해 사용되어야할 구체 coordinator 타입을 명시합니다. 그리고 ViewController에게 이 프로토콜을 채택 시킴으로써 화면 전환을 모두 coordinator에게 넘기도록 합니다. 

// 인증 화면 관련 프로토콜
protocol LoginViewControllerProtocol: CoordinatedViewController where CoordinatorType == AuthCoordinator {
    func didTapLoginButton()
    func didTapRegisterButton()
}

class LoginViewController: UIViewController, LoginViewControllerProtocol {
    weak var coordinator: AuthCoordinator?

 

화면 전환 방식

TalkTalk 앱의 Coordinator 계층 구조와 화면 전환 방식을 시각화한 구조도입니다.

1. 부모-자식 관계 (Parent-Child):  상위 Coordinator가 하위 Coordinator를 생성하고 관리하는 계층적 구조입니다.

AppCoordinator (최상위)
├── AuthCoordinator
└── MainCoordinator
    ├── ChatCoordinator
    └── ProfileCoordinator
/// 앱의 최상위 Coordinator
/// 각 화면은 담당 Coordinator들이 관리하고 여기서는 Coordinator들을 관리
class AppCoordinator: Coordinator {
	...
    func start() {
        window.rootViewController = navigationController
        window.makeKeyAndVisible()
        
        // 로그인 상태에 따라 적절한 flow 보여주기
        if isUserLoggedIn() {
            showMainFlow()
        } else {
            showAuthFlow()
        }
    }
	...
    private func showAuthFlow() {
        let authCoordinator = AuthCoordinator(navigationController: navigationController)
        authCoordinator.delegate = self
        childCoordinators.append(authCoordinator)
        authCoordinator.start()
    }
    
    private func showMainFlow() {
        let mainCoordinator = MainCoordinator(navigationController: navigationController)
        childCoordinators.append(mainCoordinator)
        mainCoordinator.start()
    }
}

AppCoordinator는 로그인 상태에 맞는 하위 coordinator를 생성하고 start( ) 메서드를 호출하면서 적절한 화면으로 이동합니다. 

 

2. 위임 (Delegation): 주로 하위 Coordinator가 이벤트를 상위 Coordinator에게 알릴 때 사용합니다.

class AppCoordinator: Coordinator {
	...
    init(window: UIWindow) {
        self.window = window
        self.navigationController = UINavigationController()
    }
    
	...
    
    private func showMainFlow() {
        let mainCoordinator = MainCoordinator(navigationController: navigationController)
        childCoordinators.append(mainCoordinator)
        mainCoordinator.start()
    }
}

/// AuthCoordinatorDelegate 채택 및 구현으로 AuthCoordinator에서 이벤트 발생시 AppCoordinator에서 동작가능
extension AppCoordinator: AuthCoordinatorDelegate {
    func didFinishAuth(_ coordinator: AuthCoordinator) {
        // Child coordinator에서 AuthCoordinator 제거 후 메인 화면으로 이동
        childCoordinators = childCoordinators.filter { $0 !== coordinator }
        showMainFlow()
    }
}

/// Delegate 패턴을 활용하여 상위, 하위 컴포넌트간의 이벤트 발신, 수신 
protocol AuthCoordinatorDelegate: AnyObject {
    func didFinishAuth(_ coordinator: AuthCoordinator)
}

/// AuthCoordinator에서 이벤트 감지시 delegate(AppCoordinator)를 이용한 화면 전환 
class AuthCoordinator: Coordinator {
	...
    weak var delegate: AuthCoordinatorDelegate?

	...
    
    func didFinishLogin() {
        delegate?.didFinishAuth(self)
    }
    
    func didFinishRegister() {
        delegate?.didFinishAuth(self)
    }
}

Delegate 패턴은 동작을 수신하는 컴포넌트와 실제 동작을 수행해야하는 컴포넌트가 분리되어 있을 때 사용하는 통신 방식입니다.

위의 경우 회원 가입 완료 버튼을 관리하는 것은 AuthCoordinator인데, 회원 가입 완료 버튼이 눌렸을 때 메인 흐름으로 전환시키 위해서는 AppCoordinator가 동작해야합니다. 따라서 AuthCoordinatorDelegate를 정의한 다음 이를 AppCoordinator에 채택시킵니다. 이후 AppCoordinator에서 AuthCoordinator를 생성할 때 자기 자신을 delegate(대리자)로 설정함으로써 AuthCoordinator에서 동작이 일어났을 때 AppCoordinator의 자원(프로퍼티, 메서드)를 활용하여 이에 대한 구현을 제공할 수 있습니다. 

 

3. 직접 참조 (Direct Reference): ViewController가 자신의 Coordinator 메서드를 직접 호출하는 방식입니다.

class ProfileViewController: UIViewController, ProfileViewControllerProtocol {
    weak var coordinator: ProfileCoordinator?
	...
    private let editProfileButton = UIButton(type: .system)
	...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        ...
        // 버튼 설정
        editProfileButton.addTarget(self, action: #selector(editProfileButtonTapped), for: .touchUpInside)
    }
    
    @objc private func editProfileButtonTapped() {
        didTapEditProfileButton()
    }
    ...
    
    func didTapEditProfileButton() {
        coordinator?.showEditProfile()
    }
	...
}

editProfileButton에 대해 사용자 액션이 발생하면 addTarget을 통해 매핑된 @objc 메서드 내부에서 자신이 weak 참조하는 coordinator를 사용해 화면을 전환합니다. 

 

4. 알림 (Notification):여러 컴포넌트에 동시에 알림이 필요한 전역 이벤트에 사용됩니다.

// ProfileViewController에서 로그아웃 요청
func didTapLogoutButton() {
    // 알림창 표시 후
    coordinator?.logout()
}

// ProfileCoordinator에서 로그아웃 처리
func logout() {
    NotificationCenter.default.post(name: NSNotification.Name("LogoutNotification"), object: nil)
}

// SceneDelegate에서 로그아웃 알림 관찰
private func setupLogoutObserver() {
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(handleLogout),
        name: NSNotification.Name("LogoutNotification"),
        object: nil
    )
}

@objc private func handleLogout() {
    if let window = self.window {
        appCoordinator = nil
        appCoordinator = AppCoordinator(window: window)
        appCoordinator?.start()
    }
}

로그아웃은 앱 전체에 영향을 미치는 이벤트이므로 NotificationCenter를 통해 처리됩니다. LogoutNotification은 프로필 화면에서 로그아웃 시 발생하며, SceneDelegate가 이를 감지하여 AppCoordinator를 재설정합니다.

TalkTalk 앱의 전체 화면 흐름

  1. 앱 시작 시 SceneDelegate가 AppCoordinator를 생성하고 시작합니다.
  2. AppCoordinator는 로그인 상태에 따라 인증 흐름 또는 메인 앱 흐름을 시작합니다.
  3. 인증 흐름에서 사용자가 로그인 또는 회원가입을 완료하면 AuthCoordinator는 delegate를 통해 AppCoordinator에게 알립니다.
  4. AppCoordinator는 메인 앱 흐름을 시작하고, MainCoordinator가 탭 바 컨트롤러를 설정합니다.
  5. 메인 앱에서는 ChatCoordinator와 ProfileCoordinator가 각 탭의 화면 흐름을 관리합니다.
  6. 사용자가 로그아웃하면 알림이 발생하고, SceneDelegate가 이를 감지하여 AppCoordinator를 재설정합니다.