[iOS] Coordinator Pattern

나는 사과·2023년 3월 5일
0

TIL

목록 보기
17/17

클린 아키텍처 예제 소스를 통해 공부하면서 AppFlowCoordinator, OOFtorlowCoordinator 이라는 이름을 갖고 있는 파일들이 있어서 보니깐 앱에서 화면 이동 관련된 부분만 따로 모아뒀길래 나중에 화면이 많아지고 복잡해 질수록 해당 방식을 사용하면 관리하기도 편해지고 흐름에 대해서도 파악하기에 좋을 것 같다. 라는 생각을 갖고 있었다. 생각에 그치지 않고 이제 사용하기 위해서 흡수해보려고 한다.

등장 배경


Coordinator Pattern은 2015년 Soroush Khanlou 님의 “The Coordinator” 이라는 글로 소개되었다.

저자는 AppDelegate에서 집중되어 있고, ViewController가 하는 역할이 너무 많고 흐름 로직에 대해 문제라고 지적했다.

AppDelegate는 SceneDelegate로 분리가 되면서 그나마 역할적으로 많이 나눠졌다 생각하고,
ViewController 관련된 문제점을 보려고 한다.

단순히, TableView에서 Cell을 선택해서 화면을 넘어가는 로직을 보면 다음과 같다.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let data = self.dataSource[indexPath]
    let secondVC = SecondViewController(with: data)
    self.navigationController?.present(secondVC, animated: true)
}
  1. dataSource에서 선택한 셀에 대한 데이터를 가져오고
  2. 이동할 ViewController를 생성하고
  3. present를 통해서 이동할 ViewController를 보여주게 된다.

근데, 현재 ViewController가 이동하게 될 ViewController에 대해서 2번째 줄에서 알게 되고
3번째 줄에서는 self(ViewController, 자신)의 navigationController(부모) 에게 present(보여줘!) 라는 행동을 하고 있다.

그래서 알고 행동하는게 뭐가 문제인데?

저자는 ViewController가 View로써의 역할만 하길 원하지 앱에서의 해당 ViewController의 위치를 ViewController가 알거나 계층 구조에서 자식이 부모를 지배하면 안되고 존재 자체를 몰라야 한다. 그래서 ViewController는 ViewController의 역할만 하고 Flow Login은 한단계 위의 계층에서 다루는 것을 주장하면서 Coordinator에 대해 소개를 했다.

Coordinator 사용법


Coordinator는 하나 이상의 ViewController를 관리하는 객체다.

기존에는 AppDelegate(SceneDelegate)에서 최초 시작하는 ViewController를 window의 rootViewController로 설정했었지만 Coordinator 패턴을 사용히면 AppDelegate(SceneDelegate)에서는 AppCoordinator와 UINavigationController를 생성 후, AppCoordinator 파라미터로 UINavigationController를 넘겨주고 AppCoordinator의 start() 메서드를 호출해주면 된다.

말은 여기까지하고.. 코드로 보면 다음과 같다.

화면이 다양해질수록 Flow도 다양해지기 때문에 여러가지 Coordinator가 만들어질 수 있다. 그렇기 때문에 재사용성을 위해 protocol로 Coordinator를 생성해 준다.

1. Coordinator 프로토콜 생성

protocol Coordinator: AnyObject { 
	// 할당 해제되는 것을 방지하기 위해서 사용
	var childCoordinators : [Coordinator] { get set }
	// start 호출 시 어떻게 화면을 보여줄 것 인지에 해당하는 메서드
	func start()
}

2. App Delegate에 만들 최상위 AppCoordinator 생성

final class AppCoordinator: Coordinator {
	var childCoordinators : [Coordinator] = []
	private let navigationController: UINavigationController
	let isShowA: Bool = false 

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

	func start() {
		// 최초 실행 시 isShowA의 값에 따라 A or B Coordinator를 생성 후 start
		isShowA ? showA() : showB()
	}

	private func showA() {
		let aCoordinator = ACoordinator(navigationController: navigationController)
		aCoordinator.start()
		childCoordinators.append(aCoordinator)
	}

	private func showB() {
		let bCoordinator = BCoordinator(navigationController: navigationController)
		bCoordinator.start()
		childCoordinators.append(bCoordinator)
	}
}

3. AppCoordinator에서 이동하는 A, B Coordinator 생성

final class ACoordinator: Coordinator {
	var childCoordinators : [Coordinator] = []
	private let navigationController: UINavigationController

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

	func start() {
		// AViewController를 보여줌
		let aVC = AViewController()
		navigationController.viewControllers = [aVC]
	}
}
final class BCoordinator: Coordinator {
	var childCoordinators : [Coordinator] = []
	private let navigationController: UINavigationController

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

	func start() {
		// BViewController를 보여줌
		let bVC = BViewController()
		navigationController.viewControllers = [bVC]
	}
}

여기까지 작성하시면 다음과 같은 구조를 갖게 된다.

하지만 실제로는 이렇게 한 줄기로 뻗어 나가는 것이 아니라 A VC에서의 어떤 동작으로 인해 B VC로 이동해야하는 경우도 발생하게 되는데 이러한 경우에는 Delegate Pattern 등을 사용해서 화면을 이동 시켜주면 됩니다.
→ 해당 부분은 아래 참고 자료의 zedd님 내용 참고하시면 자세하게 나와있습니다!

또한, 위에서는 navigationController.viewControllers = [ VC ] 이런 식으로 작업 했는데 만약에 A에서 다른 C로 이동하는 거면 navigationController.pushViewController(VC) 이런 식으로 사용해주시면 됩니다!!

Coodinator 장점


  1. 각 ViewController가 독립적
    ViewController는 데이터를 보여주기만 하면 됨
  2. ViewController 재사용 가능
    패드버전의 화면을 위해서는 Coordinator만 변경하면 됨
  3. 앱의 모든 작업들은 캡슐화 되는 방법을 갖음
    다수의 ViewContoller를 아우르는 작업들을 캡슐화 할 수 있다
  4. 사이드 이펙트로부터 display-binding을 분리
    화면 전환 시 ViewController에 데이터를 망치는 일을 방지
  5. 완벽하게 제어가 가능
    호출을 받는게 아니라 호출을 함 (viewDidLoad X → start O)

결론


화면 전환 로직은 ViewController에서 상당히 많은 부분을 차지한다고 생각했었다.

회사 서비스만 생각하더라도 프로젝트 화면에서 프로젝트 설정, 각 포스트 화면, 작성화면, 프로젝트 참여자, 초대, 채팅, 다른 프로젝트로 이동화면, 공유하기, 파일함, 사진함, 업무 리스트, 일정, 알림, 검색.. 여기까지만 해도 14개의 화면으로 단 1개의 화면에서 이동하게 된다. 처음 신입으로 왔을 때, 이런 흐름을 한번에 파악하는게 진짜 어려웠고.. 솔직히 나중에 QA 작업하다가 발견한 화면들도 있었다.

이렇게 Coordinator 패턴을 사용한다면 처음 오시는 분들도 Coordinator 부분만 따라가면 회사 서비스의 화면 흐름도 쉽게 파악이 가능하고 각 ViewController가 더 이상 화면 전환에 대한 로직을 갖고 있지 않아도 되니 ViewController도 더 가벼워 질 수 있을 것 같다.

점점 더 커져가는 서비스면서 화면 전환이 매우 복잡한 경우에는 90%는 해당 패턴을 도입하면 좋을 것 같다!! (10%는 각 팀의 상황에 따라 달라질 수도 있을 것 같아서..🥲)

참고 자료


https://khanlou.com/2015/01/the-coordinator/
https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930
https://labs.brandi.co.kr/2020/06/16/kimjh.html

0개의 댓글