[CS - iOS] Coordinator Pattern

2024. 10. 12. 18:50·CS

다음주에 코디네이터 패턴을 활용할 일이 생길 예정인데 내가 너무 코디네이터 패턴에 대해 무지한 것 같아서 이렇게 정리해보는 글을 써보며 예습하는 시간을 가져보려한다.

코디네이터 패턴의 시작

코디네이터 패턴은 Soroush Khanlou 라는 분의 한 아티클에서 시작했다고 한다.

Khanlou 선생님..ㅎㅎ

해당 글을 읽어보면 알 수 있는데, 간단하게 요약해보자면 다음과 같다.

  1. UIViewController도 하나의 View 객체이다.
  2. 이러한 View 객체가 사용자의 흐름(다음 화면으로 화면전환을 하는 일)을 처리하는 것은 View의 역할 범위를 넘어섰다 !
  3. 따라서 ViewController 자체를 하나의 고수준 객체(coordinators or directors)로 보고 이를 총괄하는 하나의 객체를 가지면 역할 분리에 있어 큰 이점을 가지게 된다 !

Khanlou 가 생각한 코디네이터 패턴의 컨셉은 아래와 같다.

AppDelegate(SceneDelegate)는 AppCoordinator를 유지하며, 모든 Coordinator에는 일련의 하위 Coordinator가 있다.

그리고 이 컨셉이 지금 우리가 사용하는 Coordinator pattern이다 !

결론은 Coordinate 패턴이란 View 간의 화면 전환을 총괄해주는 또 하나의 Layer(?)라고 할 수 있을 것 같다.
(모듈화를 하게되면 Coordinator Layer를 따로 만들어 관리하게되지 않을까??)

코디네이터 패턴 예습

갓 zeddios 선생님 글을 보고 클론 코딩 느낌으로다가 한 번 실습을 해봤다.

화면은 딱 두 개, 로그인 화면과 메인 화면으로 구성되어있고 구조는 아래와 같다.

AppCoordinator가 앱이 시작할 때 맨 처음 실행될 최상위 Coordinator이다.

위에서 Khanlou 선생님이 말씀하신대로 하나의 VC당 하나의 Coordinator 객체를 만들어줬다. 그리고 AppCoordinator에서 childCoordinator 배열을 가지고 있고, 이 배열을 활용하여 각각의 VC을 관리하게된다.

앱의 flow를 설명해보자면, 다음과 같다.

처음 앱에 진입하면 LoginCoordinator가 생성되고 생성된 LoginCoordinator는 childCoordinators에 추가된다.

final class AppCoordinator: Coordinator {

    var childCoordinators: [any Coordinator] = []
    private var navigationController: UINavigationController

    var isLoggIn: Bool = false

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

    func start() {
        if isLoggIn {
            showMainViewController()
        } else {
            showLoginViewController()
        }
    }

    private func showLoginViewController() {
        let coordinator = LoginCoordinator(navigationController: navigationController)
        coordinator.delegate = self
        coordinator.start()
        /// 새로 추가될 coordinator를 childCoordinator에 추가 ~
        childCoordinators.append(coordinator)
    }
    ...
}

이후 LoginCoordinator에서는 start 메서드에 의해 LoginViewController가 생성되고 이를 화면에 보여준다.

final class LoginCoordinator: Coordinator {

    var childCoordinators: [Coordinator] = []
    weak var delegate: LoginCoordinatorDelegate?

    private let navigationController: UINavigationController

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

    func start() {
        let viewController = LoginViewController()
        viewController.view.backgroundColor = .brown
        viewController.delegate = self

        navigationController.viewControllers = [viewController]
    }
}

마지막으로 LoginViewController에서 LoginButton이 클릭되면 MainViewController로 화면이 변환되는 flow이다.

final class LoginViewController: UIViewController {

    private let loginButton: UIButton = { ... }()

    weak var delegate: LoginViewControllerDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()

        setUpUI()
        loginButton.addAction(loginButtonTapped() , for: .touchUpInside)
    }

    private func setUpUI() { ... }

    private func loginButtonTapped() -> UIAction {
        UIAction { [weak self] _ in
            guard let self else { return }
            self.delegate?.loginButtonDidTapped()
        }
    }
}

VC과 Coordinator 혹은 Coordinator끼리는 delegate 패턴을 사용하여 각자의 상태변화를 감지할 수 있도록 해주었다.

지금은 두 화면을 번갈아가며 보여주는 느낌이라 LoginCoordinator나 MainCoordinator의 자식 coordinator는 없지만, 위에 계속해서 쌓이는 형태가 되면, 자식 coordinator가 쌓이고 화면도 naviationController.push 이런식으로 화면 전환을 해야하지 않을까? 싶다.

코디네이터 패턴의 이점

아직 여러 화면이 존재하는 프로젝트에 적용해본 것은 아니라서 체감하지는 못했지만, VC의 역할이 줄어들면서 VC가 더 가벼워지는 것 같다는 생각이 들었다.

coordinator라는 객체로 VC를 관리하니 AppCoordinator라는 최상위 코디네이터 내부에서 여러 뷰들의 화면 전환이 더 자유롭게 이루어질 수 있을 것 같다.

그리고 coordinator 패턴을 사용함으로써 뷰간의 상속관계도 더 명확하게 알 수 있다는 장점도 있는 것 같다.

추가로.. Zeddios 센세에 의하면 저것때문에 RIBs가 나왔다..? 는 식으로 얘기하셨는데.. 난 RIBs도 뭔지 모르는 미물..

배움에는 끝이 없구나 😇

메모리 누수…왓..더..

근데 zeddios 선생님 코드를 내 입맛에 맞게(?) 처리해주는 과정에서 Coordinator 패턴에는 상관없는 메모리 관련 이슈가 발생했따…

zeddios 선생님 코드는 순환참조를 따로 고려하지 않은 코드인 것 같아 약한 참조를 활용하여 delegate를 구현해주었는데 왜인지.ᐟ.ᐟ LoginCoordinator가 계속해서 deinit되는 오류가… 발생.. 😇

AppCoordinator의 childCoordinators 배열이 약한 참조값도 아니고.. LoginCoordinator가 잘 append 되어있음에도 불구하고 걍 지혼자 deinit되어버린다;;

whyrano….whyrano…

LoginCoordinator의 delegate를 강한 참조 인스턴스(?)로 만들어줬더니 잘 되긴하는데… instruments를 확인해보니 아니나 다를까 바로 메모리 누수가 났다 😇

흐엉어엉어ㅓ어ㅓㅇㅇ

전체 코드는 GibHub에 업데이트 해뒀다. 혹시나.. 심심하신 분들은 어디가 문제일..지.. 봐주신다면 압도적 감사..

일단…. 이 글의 목표인 Coordinator 패턴에 대해선 잘.. 학습을 했으니… 여기서 얼레벌레 글을 마치도록…하겠ㄷ…ㅏ…

참고자료

  • 🔗 The Coordinator
  • 🔗 Zeddios - Coordinator Pattern
  • 🔗 [디자인 패턴][swift] Coordinator pattern
  • 🔗 SwiftUI NavigationView로 Coordinator Pattern 사용하기
저작자표시 (새창열림)

'CS' 카테고리의 다른 글

[CS - iOS] 동시성 프로그래밍 (feat. GCD, OperationQueue)  (0) 2024.08.08
[CS] 동시성 프로그래밍 vs 비동기 프로그래밍  (0) 2024.08.03
[CS - iOS] 프로세스와 스레드 관리  (0) 2024.05.02
'CS' 카테고리의 다른 글
  • [CS - iOS] 동시성 프로그래밍 (feat. GCD, OperationQueue)
  • [CS] 동시성 프로그래밍 vs 비동기 프로그래밍
  • [CS - iOS] 프로세스와 스레드 관리
00me
00me
얼렁뚱땅 방장이 운영하는 기술 블로그
  • 00me
    영미의 iOS 다이어리
    00me
  • 전체
    오늘
    어제
    • 프로그래밍 (29)
      • 📖 (0)
      • CS (4)
      • Python (5)
      • Swift (5)
      • iOS (3)
      • 코테 (3)
        • 자료구조 (0)
        • 알고리즘 (3)
      • 회고 (9)
  • 링크

    • 🍧 GitHub
  • 인기 글

  • hELLO· Designed By정상우.v4.10.3
00me
[CS - iOS] Coordinator Pattern
상단으로

티스토리툴바