-
[iOS-1] Coordinator 패턴을 사용한 화면 전환 관리Project/TalkTalk 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 앱의 전체 화면 흐름
- 앱 시작 시 SceneDelegate가 AppCoordinator를 생성하고 시작합니다.
- AppCoordinator는 로그인 상태에 따라 인증 흐름 또는 메인 앱 흐름을 시작합니다.
- 인증 흐름에서 사용자가 로그인 또는 회원가입을 완료하면 AuthCoordinator는 delegate를 통해 AppCoordinator에게 알립니다.
- AppCoordinator는 메인 앱 흐름을 시작하고, MainCoordinator가 탭 바 컨트롤러를 설정합니다.
- 메인 앱에서는 ChatCoordinator와 ProfileCoordinator가 각 탭의 화면 흐름을 관리합니다.
- 사용자가 로그아웃하면 알림이 발생하고, SceneDelegate가 이를 감지하여 AppCoordinator를 재설정합니다.
'Project > TalkTalk' 카테고리의 다른 글
[iOS-2] Dependency Container로 의존성 관리하기 (0) 2025.03.31