티스토리 뷰

iOS

[iOS] Coordinator Pattern

Esiwon 2023. 1. 9. 19:54

안녕하세요.
오늘은 Coordinator Pattern에 대해 알아보겠습니다.

먼저, Coordiantor가 무엇인지 알아보겠습니다.

Coordinator 란?

iOS를 공부하다 보면, 어느 순간 MVC, MVVM, MVP, VIPER 등 여러 Architecture를 접하게 됩니다. 여기서 MVC-C, MVVM-C 등 뒤에 C가 붙는 것을 보신 적이 있으시다면, C가 Coordinator를 의미하게 됩니다. 또한 Coordinator는 VIPER 패턴의 R(Router)과 같은 역할을 수행합니다.

즉, Coordinator는 화면 간 전환을 담당합니다.
좀 더 자세히는 해당 화면(ViewController)에 필요한 의존성을 주입하고, ViewController 객체를 생성해 화면을 전환하는 역할을 수행합니다.

개념 자체는 어렵지 않은 것 같습니다.

그럼, 왜 Coordinator Pattern을 사용할까요??

Coordinator Pattern을 사용하는 이유

객체지향 설계 원칙(SOLID) 중 단일 책임 원칙(Single Responsibility Principle)이 있습니다.

아키택쳐를 설계할 때 객체는 하나의 책임만을 가졌을 때 좋은 아키택쳐를 설계할 수 있습니다. Coordinator Pattern 또한 이러한 관점에서 탄생했습니다.

기존에는 화면전환을 ViewController가 담당했습니다.
때문에 부모 VC는 자식 VC를 알아야 했고, 자식 VC에 필요한 객체들 또한, 모두 부모 VC 내부에서 인스턴스를 생성해야 했습니다. 때문에 객체들 간 결합도가 높아질 수밖에 없고, 아키택쳐를 설계할 때 높은 결합 도는 유지 보수에 불리합니다.

여기서 Coordinator 패턴을 사용한다면, 더 이상 부모 VC는 자식 VC를 알 필요가 없습니다. 자식 VC의 의존성을 주입하고 객체를 생성해 화면을 전환하는 역할은 Coordinator가 하기 때문입니다. 이젠 VC는 자기 Coordinator에게 자식 VC로 화면전환을 요청하면 됩니다.

이로써 이제 ViewController 오직 데이터를 화면에 뿌려주는 역할만 집중할 수 있게 되고, 때문에 유지 보수가 용이해졌습니다.

Coordinator의 역할

위에서도 설명했지만, 다시 한번 Coordinator의 역할을 정리하자면,

  1. 자신이 담당하는 VC의 필요한 인스턴스를 생성해 주입합니다.
  2. 의존성이 주입된 VC 인스턴스를 화면에 표시합니다.

이렇게 두 가지의 역할을 수행합니다.

여기서 그럼 이런 생각이 들 수 있습니다.
"아니 SOLID 중 단일 책임 원칙에 의하면, 하나의 역할만 수행해야 하는데 2개의 역할을 수행하는 거 아니냐!"

2개의 역할이 맞습니다. 때문에 여기서 DIContainer객체 두어 Coordinator의 1번 역할을 DIContainer 객체가 수행하게 됩니다.

하지만, 이번 글의 주제는 Coordinator이기 때문에 따로 설명은 하지 않겠습니다. 궁금하신 분은 따로 구글링 해보시길 추천드립니다.

Coordinator Pattern 사용법

먼저,

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

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

위와 같이 Coordinator Protocol을 만들어 줍니다.

해당 프로토콜을 채택하여 Coordinator 객체를 만들어주면 됩니다.

기본적으로,

위 그림 처럼, Coordinator Pattern은 가장 상위에 AppCoordinator가 존재합니다.

final class AppCoordinator: Coordinator {
  let navigationController: UINavigationController
  weak var parentCoordinator: Coordinator?
  var childCoordinators: [Coordinator] = []

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

  func start() {
    let mainCoordinator = MainCoordinator(navigationController: navigationController)

    mainCoordinator.parentCoordinator = self
    childCoordinator.append(mainCoordinator)
    mainCoordinator.start()
  }
}

위와 같이 Coodinator 프로토콜을 채택 받은 AppCoordinator는 start 메서드 내부에서 rootVC로 보일 화면의 Coordinator의 start 메서드를 호출해 줍니다.

AppCoordinator의 start 메 서드는 AppDelegate 또는, SceneDelegate에서 호출되게 됩니다.

자 그럼 이번엔 Main 화면에서 검색을 위해 검색 화면으로 이동한다고 가정하면,

final class MainCoordinator: Coordinator {
  let navigationController: UINavigationController
  weak var parentCoordinator: Coordinator?
  var childCoordinators: [Coordinator] = []

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

  func start() {
    let viewModel = MainViewModel()
    let mainVC = MainViewController(viewModel: viewModel)
    mainVC.coordinator = self
    navigationController.pushViewController(mainVC, animated: true)
  }

  func showSearchView() {
    let searchCoordinator = SearchCoordinator(navigationController: navigationController)
    childCoordinator.append(searchCoordinator)
    searchCoordinator.parentCoordinator = self
    searchCoordinator.start()
  }
}

위와 같이 MainCoordinator 내부에는 start 메 서드와 다음 자식 화면으로 이동하는 메서드를 구현해 줍니다. childCoordinatorparentCoordinator 파라미터를 통해 계층 간 연결고리를 이어줍니다. 이는 화면을 사라질 때 해당 화면의 Coordinator 또한, 같이 deinit 되어야 하기 때문입니다.

이번엔 SearchCoordinator를 확인해 보겠습니다.

final class SearchCoordinator: Coordinator {
  let navigationController: UINavigationController
  weak var parentCoordinator: Coordinator?
  var childCoordinators: [Coordinator] = []

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

  func start(book: Book) {
    let viewModel = SearchViewModel()
    let searchVC = SearchViewController(viewModel: viewModel)
    searchVC.coordinator = self
    navigationController.pushViewController(searchVC, animated: true)
  }

  func removeCoordinator() {
    parentCoordinator?.removeChildCoordinator(child: self)
  }
}

SearchCoordinator 내부엔, 모든 Coordinator가 동일하게 start 메서드가 구현되어 있고,

화면이 사라지면, 같이 해당 Coordinator도 deinit 될 수 있도록, removeCoordinator 메서드를 구현해 주면 되겠습니다.

여기까지 간단하게 Coordinator Pattern 사용법에 대해 알아보았습니다.

마지막으로, Coordinator Pattern의 장단점을 정리하고 마무리하겠습니다.

Coordinator Pattern의 장단점

장점

ViewController에서 화면전환의 역할을 분리할 수 있다.
iOS 특성상 ViewController가 커지기가 참 쉬운 것 같습니다. 여러 기능을 구현하다 보면 VC가 너무 커져 특성 메서드 찾기가 힘들었던 경험이 있는데 MVVM, Coordinator Pattern 등을 사용하면, VC의 역할을 많이 분리할 수 있어 유지 보수에 용이해집니다.

단점

더 많은 코드를 작성해야 한다.
화면과 Coordinator가 1:1이라고 가정했을 때 화면 하나를 구현할 때마다 Coordinator도 같이 구현해 줘야 하기 때문에 더 많은 Class와 파일을 생성하야 합니다. 이는 작은 프로젝트에선 오버 엔지니어링이 될 수 있다고 생각합니다.
또한, 화면이 Disappear 되면, Coordinator도 같이 deinit되어 줘야 하는데 이를 따로 처리하지 않는다면, 메모리 누수가 발생할 수 있습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함