코디네이터 패턴(MVVM-C) - 코디네이터는 네비게이션의 확장인가?

bono·2023년 7월 4일
9
post-thumbnail

본 포스팅은 코디네이터 패턴에 대한 개인적인 이해로 진행되며 오개념을 담고 있을 수 있습니다.

디자인 패턴은 개발자의 미적 감각에 따라 다양하게 해석되어 활용 되므로, 참고할 만한 무수한 자료 중 하나가 되기를 바랍니다!

(추가_ 24.03) 과거 포스팅 당시엔 네비게이션 분류에 대한 이해가 없어서 코디네이터의 역할이 1. 화면 책임, 2. Flow 책임으로 분류 될 수 있다 언급하였습니다. 하지만 이는 Tree-based / Stack-based 라는 보다 명확한 명칭이 있었음을 알게 되었습니다. 🙇‍
더불어 본 포스팅의 중반부터 설명하는 Coordinator는 그 중에서도 Tree-based Navigaiton 역할을 수행하는 Coordinator를 다루고 있습니다.
더 나아가, 본 포스팅이 다루는 Tree-based Navigation 역할을 수행하는 Coordinator는 부모가 자식 Coordiantor를 Stack으로 저장, 관리하는 이유를 이해하기 어렵게 한다 생각합니다. 그러니 Coordinator에 대한 이해를 위해서라면 본 포스팅 외에도 Coordinator가 Stack-based Navigation의 역할을 수행하는 경우의 코드로 확인해 보시면 좋을 것 같습니다🙇‍

들어가며, 코디네이터 패턴은 왜 쓰는 걸까?

사이드 프로젝트의 시작을 앞두고, 코디네이터 패턴을 알게 되었다.
그에 대해 접하며 들은 말들은 아래와 같다.

  • VIPER 패턴에서 화면 전환 역할인 Router를 추출하여 MVVM에 도입한 형태
  • 이미 비대한 ViewController에서 화면 전환 역할을 추출해 Coordinator에 전임
    : ViewController 역할 축소
  • 화면의 모듈화
  • 의존성 주입 역할
  • 재사용성 증진

간략한 조사 뒤 팀원과 프로젝트에 도입해 보자는 결론을 내렸다. 개인적으로는 협업 시 흩어지기 쉬운 화면 전환 코드를 공통된 양식으로 묶는 것만으로도 도입에 의미가 있다고 생각했다.

그러나 개발을 진행하면 진행할수록 코디네이터의 역할에 대한 의문이 들었다. 사용 이점 중 하나라는 화면 전환 ‘재사용’ 이점을 전혀 누릴 수 없었기 때문이다.

틀만 잡으면 사용이 용이할 것이라 생각했지만 다루기도 까다로웠다. 쓰면 쓸수록 왜 쓰는 지 모르겠다는 생각이 커져만 갔다.

결국 잠시 개발을 중단하고 코디네이터 패턴의 등장 배경과 정의를 돌아보는 시간을 가졌다.

그 과정에서 코디네이터 패턴에 대해 오해한 부분이 있었음을 알게 되었다.

뒤늦은 깨달음과 함께 코디네이터 패턴이란 무엇인지, Coordinator, ViewController, NavigationController 간의 관계에 초점을 맞추어 다시금 정리하고자 포스팅을 발행한다. 코디네이터 패턴에 관심있는 누군가에게 도움이 되길 바란다.

개인적으로 코디네이터 패턴을 풀어내며 담고 싶은 내용은 아래와 같다.

  • Coordinator는 NavigationController의 상위 개념이 아니다.

더불어 개인적인 이해를 풀어낸 포스팅이기에 오개념에 주의하며, 입맛에 맞게 선택적으로 정보를 수용하기를 권하고 싶다.

코디네이터 패턴에서 등장하는 키워드

  • ChildCoordinator ✨
  • ParentsCoordinator ✨
  • NavigationController
  • ViewController

위의 두 개는 코디네이터 패턴에서 새로 정의된 키워드이다.
이에 대해서는 아래에서 자세히 설명할 예정이다.

먼저 코디네이터 패턴의 등장 배경에 대해 알아보자.

코디네이터 패턴 등장 배경

서론_개발 입문자 시절 생각한 앱 화면 전환 방식

iOS 개발에 입문한지 얼마 되지 않았던 때의 나는 A에서 B 화면으로의 push 이동이 위의 그림처럼 화면 전환자에게 요청하는 방식으로 진행되리라 추측했다.
(화면 전환자라는 용어는 내가 상상해 만들어 낸 단어이다)

하지만 실제 개발을 배워보니 내가 요청하면 들어주는 화면 전환자라는 건 없었다.

앱 개발에서 화면 전환은 NavigationController에 의해 이루어졌다.

NavigationController(이하 Navi)을 이용한 화면 이동 코드는 위와 같았다.

또한 화면 이동을 주도하는 것은 Navi가 아닌 AViewController(이하 AVC)였다.

A -> B 화면 이동
버튼 눌리면 '(AVC)내가' 다음 화면인 BVC를 만들어 Navi에 쌓을게

A -> B 화면으로의 push 이동은 온전히 AVC의 주도 하에 이루어졌다.

AVC가 화면 BVC의 인스턴스를 직접 생성하여 자신이 소속된 Navi의 메서드를 사용하여 직접 넣어주는 방식이었다.

VC 아닌 다른 객체에게 화면 전환 업무를 요청할 수는 없을까?
그게 가능하다면, 코디네이터가 그것일까?

그에 대한 나름의 답을 본문에 담았다.

일단은 코디네이터가 등장한 배경에 대해 더 알아보자.

한 화면이 가지는 다양한 화면 전환

앱 개발에서 한 VC가 여러 화면 전환을 수행하는 것은 일반적인 상황이다.

현재 화면 : AViewController
이동 가능 화면 : B / C / D ViewController

AVC의 화면이 위와 같이 구성되었더라면 AVC는 특정 액션에 따라 사용자가 이동할 특정 화면(B / C / DVC)을 직접 생성하고 네비게이션 컨트롤러에 push 해야 한다.

그렇게 이동한 화면 (B / C / DVC) 위에는 또 다른 화면 전환을 이루는 요소들 (버튼 등등)이 존재할 수 있다.

안 그래도 할 일이 많은 ViewController는 화면전환 책임까지 가져야 한다. 버튼을 눌리면 보여줘야 할 ViewController가 B인지 C인지 D인지를 알아야 한다.

ViewController로 부터 이러한 화면전환 업무를 분리해 낼 수 없을까?

그 생각이 코디네이터 패턴의 등장 배경이다.

코디네이터 패턴은 화면전환을 비즈니스 로직의 일부라고 여긴다.
즉, 화면전환이 앱의 정체성에 기여하는 요소라고 본다.

MVVM이건, 클린 아키텍쳐건 그 중요도에 따라 구조를 나누고 계층화한다. 여기서 화면전환은 비즈니스 로직이므로, View와는 다른 계층에(내부) 존재해야 한다. 이를 구현하기 위한 도구가 Coordinator 라고 할 수 있을 것 같다.

VC가 직접 화면전환하는 일에 대해 불편함이나 이질감을 느끼지 못한 사람이라면 이 대목을 이해하기 어려울 것 같다. (그게 왜 비즈니스 로직? 이라는 생각이 든다)

하지만 당장은 이를 받아 들여야 코디네이터 패턴의 사용 이유를 이해할 수 있다.

화면 전환 로직을 ViewController로부터 분리해 낼 수 있다면 VC는 화면 위에 게시할 정보와 액션에 더 집중할 수 있을 것이다.

Coordinator의 필요를 느끼는 대목은 이뿐만이 아니다.

화면 전환 코드의 중복

TabBar 사용 시 상황

위 그림은 탭바 아이템에 해당하는 각각의 Navi와 각각의 RootViewController들 즉 VC들을 표현한 그림이다.

각각의 RootVC 위에는 앱의 기획 의도에 맞는 화면이 쌓일 수 있다.

서로 다른 VC에서 같은 화면 전환 발생

이해를 돕기 위해 지금 진행중인 사이드 프로젝트에서 벌어지는 화면 전환을 그려보았다.

위 그림이 묘사하는 상황은 아래와 같다.

1. HomeVC -> AddDiaryVC

홈 탭HomvVC에는 메인 펫 이미지가 있다. 해당 이미지 하단 버튼을 누르면 메인 펫에게 오늘자 다이어리를 추가할 수 있는 AddDiaryVC 화면으로 이동할 수 있다

2. DiaryVC -> PetDiaryVC -> AddDiaryVC

다이어리 탭DiaryVC에서 내가 추가한 모든 펫들을 확인할 수 있다. 특정 펫을 클릭하면 해당 펫의 다이어리 모음인 PetDiaryVC로 이동한다. 이동한 페이지에서 플러스 버튼을 눌러 해당 펫에게 다이어리를 추가하는 AddDiaryVC 페이지로 이동할 수 있다.

즉, HomeVCPetDiaryVC 모두 보여주는 화면은 다르지만 AddDiaryVC으로의 이동을 이루어야 한다.

다른 예시로는 쇼핑몰 앱을 들 수 있다.

내 장바구니에 담긴 옷을 터치하면 옷의 판매 페이지로 이동할 수 있고, 쇼핑 페이지에서 옷을 터치해도 옷의 판매 페이지로 이동할 수 있다. 유입된 경로는 다르지만 전부 같은 화면을 노출한다. (AddDiaryVC)

이전(Before) 화면이 곧 이동(after) 화면의 생산 주체

이렇듯 하나의 앱에는 접근 경로는 다르지만 같은 화면을 목표하는 흐름이 존재할 수 있다.

그러니 AddDiaryVC를 생성하고 push하는 코드가 각각의 VC에서 중복적으로 사용된다.

만일 앞서 상상했던 화면 전환자가 존재한다면 어떨까.

HomeVC가 직접 AddDiaryVC를 생성하는 것이 아니라, 화면 전환자에게 다이어리 추가 화면으로의 이동을 요청하는 것이다.

그렇다면 HomeVC는 더 이상 다이어리 추가에 맞는 화면을 구성하는 것이 AddDiaryVC인지 iOSHaeunVC인지 알 필요가 없다. 그저 요청하기만 하면 된다.

그렇게 시작된 코디네이터

Coordinator는 이제껏 AVC가 했었던 화면 삽입 업무를 가져간다.

Coordinator의 도움을 받으니 HomeVC도, PetDiaryVC도, 이제는 다이어리 추가 화면으로의 이동하기 위해서 AddDiaryVC를 직접 생성할 일이 사라질 것이다.

이제 VC들은 새로운 화면 삽입이 어떻게 이루어지는 지 몰라도 된다. 그저 Coordinator에게 요청할 뿐이다.

어딘가 익숙한 설명같지 않은가 !

개인적인 생각이지만 Coordinator는 delegate와 설명이 흡사하다.
그래서 나는 아래와 같이 정리하였다.

Coordinator는 화면 전환이라는 역할에 맞는 이름을 부여받은 Delegate이다.

HomeVC와 PetDiaryVC 더 이상 AddDiaryVC를 직접 생성하기 위한 코드를 작성하지 않아도 된다.

원하는 화면 이동만 알고 있을 뿐, 그 화면 이동 내용이 구현된 이동 지점(VC)에 대해서는 몰라도 된다.

(추가) Coordinator의 역할범주

Coordinator의 역할은 화면으로 하기도 하고, 흐름 자체를 묶어 (A -> B -> C) Flow로 구성하기도 한다.

Flow로 생성한다면 CViewController까지의 도달 순서가 확정적인 경우 유용할 것이다.
예를 들어, 환경설정의 -> 개인정보 -> 비밀번호 변경이라면 이 흐름을 벗어나서 비밀번호 변경 화면을 생성해야 할 일이 없으므로, Flow 단위로 묶을 수 있다.

똑같은 디자인 패턴도 사용 예제가 다른 것 처럼, 코디네이터 패턴도 이러한 관점에 따라 블로그마다 다른 예제를 보인다. 만일 코디네이터의 필요를 알았다면 원하는 형태로 쓰면된다.

본 포스팅에서는 ViewController 생성을 Coordinator 생성 기준으로 잡는다.

(추추가 24.03)

포스팅 당시엔 네비게이션 분류에 대한 이해가 없어서 설명하는 코디네이터의 종류를 1. 화면 책임, 2. Flow 책임으로 분리 설명하였다.
하지만 이는 Tree-based / Stack-based 라는 보다 명확한 명칭이 있었다.
그러니 본 포스팅의 하위 내용은 Tree-based Navigaiton 역할을 수행하는 Coordinator를 다룬다.


코디네이터가 없을 때의 화면 전환

Navi의 RootVC인 AVC 자신의 다음 화면에 대한 B, C, D VC를 직접 만들고 push 하는 일을 한다.

즉, AVC가 화면을 관리하기 위해 구체적으로 알아야 하는 다른 VC가 세 개나 된다.

코디네이터가 있을 때의 화면 전환

하지만 ViewController가 가지고 있던 다음 화면 생성의 책임을 Coordinator가 가져가면 어떻게 될까.

AVC는 더 이상 다음 화면에 어떤 VC가 필요한 지 구체적으로 알 필요가 없어졌다.

그저 원하는 화면 이동을 Coordinator에게 요청하면 된다.

그 이동으로 보여지는 게 BVC인지 CVC인지, DVC인지 구체적인 사항을 알 필요가 없다.

객체 지향적인 관점으로는 메세징에 해당한다.

영화관 티켓을 구입하기 위해 직원에게 요청하는 것처럼, 더 이상 VC는 직접 화면을 이동하지 않는다. 그건 VC가 할 수 있는 역할이 아니기 때문이다.

영화관 티켓 발급은 직원만이 할 수 있는 것 처럼, VC는 화면전환을 수행하는 Coordinator에게 화면전환을 요청할 것이다.

(내 생각)Coordinator는 마치 운전 기사 같다

Coordinator의 도입은 목적지에 가기 위해 내가 직접 차를 모는 지, 아니면 운전 기사가 대신 모는 지의 차이를 만든다 생각한다.

내가 직접 운전할 때에는 고양이 빌라에 도착하기 위해 어디서 언제 우회전하고 좌회전 하는 지 알아야 한다.

하지만 운전 기사가 있다면 도착지가 '고양이 빌라'라는 것만 알고 있어도 된다. 언제 어디서 우회전 해야 하는 지는 알 필요가 없다.

물론, 기사님은 여전히 길을 알아야 한다. 그게 Coordinator이다.

Coordinator는 ViewController가 처리하고 있던 사용자의 흐름 판단(비즈니스 로직)(Flow) 역할을 가져옴으로서 ViewController가 View에 관련된 활동만 수행하도록 돕는다 ⭐️

화면 전환을 직접 수행할 필요가 없어진 '나' VC는 화면관리에 더 집중할 수 있다.

또한 MVVM이 가진 장점이 그러했듯 코디네이터 패턴도 VC의 볼륨을 줄여 Massive VC를 피한다.

MVVM이 가진 ViewController의 책임을 Coordinator가 한층 덜어줄 수 있으니 MVVM과 Coordinator는 자주 결합되어 MVVM-C 라는 명칭으로 사용된다. (아키텍쳐 설계는 개발자 마음이라고 생각한다)

이렇게! 지금까지 코디네이터의 등장 배경에 대해 알아보았다.

이제는 코디네이터가 존재하는 위치와, 내가 했었던 오해를 토대로 Coordinator가 위치상 가지는 장점에 대해 말해보자.


코디네이터는 네비게이션 컨트롤러를 감싼 객체인가?

화면 전환 업무를 대신 수행하게된 코디네이터HomeVC로 부터 화면을 전환해 달라는 요청을 받고, AddDiaryVC를 생성하여 HomeVC가 소속된 Navi에 push 한다.

아래는 코디네이터를 구성하는 코드의 일부이다.

protocol Coordinator: AnyObject{
  ...
  var navigationController: UINavigationController {get set}
  ...
}

Coordinator는 NavigationController를 속성으로 가지고 자유롭게 접근할 수 있다.

아하. Coordinator는 Navi 소유하고, 그가 가진 기능을 편리하게 정의하여 사용할 수 있도록 가공하고 있네.
CoordinatorNavi의 기능을 확장시킨 객체로 볼 수 있는건가?

그렇지 않다

이전의 나는 그렇다고 생각했기 때문에 이 부분에서 코디네이터 패턴에 대한 이해를 완전히 놓치고 말았었다.

앞전의 운전 기사 비유를 이어나가자면 NavigationController자동차에 해당한다. 어디론가 이동할 때 필요하고, 편리하며, 동작 방법을 알아야 움직이게 할 수 있다. 그렇다고 자동차 내부의 동작 방식까지 알아야 하는 것은 아니다.

만약 코디네이터 패턴을 도입하며 운전기사에 해당하는 Coordinator의 역할을 자동차의 역할과 혼동하면 문제가 생긴다. 기사님을 모셔와서 좀 더 편하게 일하고 싶었을 뿐인데, 자칫 기사님을 자동차로 만드는 상황이 벌어지기 때문이다.

운전 기사를 고도로 발달된 자동차라고 보지 않듯이 Coordinator는 NavigationController의 역할을 대리하지 않는다.

코디네이터는 NavigationController의 역할을 대리하지 않는다.
(이 말을 쓰고 싶어서 글을 시작했다)

소소한 내용을 거창하게 풀어낸 것 같아 민망하다.

코디네이터 패턴 사용 시 이해하기 어려웠던 부분에 대한 내용을 하나 더 언급하고 포스팅을 마치려 한다.

코디네이터 도입과 사용

어디서 코드 재사용성을 느껴야 하나

앞선 설명대로라면 코디네이터를 통해 화면전환 코드의 재사용이 가능해야 한다.

어디서 재사용성이 확립되는 지 알아보자.

분홍색 박스로 표시된 두 Coordinator 모두 AddDiaryVC를 만들어 각각의 요청 받은 VC가 존재하는 NaviAddDiaryVCpush 하는 역할을 수행한다.

하지만 Coordinator의 이름이 HomePet으로 각기 다르다.

이는 각각이 아예 다른 Coordinator라는 것을 의미한다. 그렇다면 AddDiaryVC 생성 및 삽입 코드를 재사용하고 있다고 볼 수 없지 않은가?

(나는 학습 시 이 대목에서 코디네이터 패턴을 이해하기 어려웠다)

Coordinator가 가진 화면 전환 코드 재사용의 측면을 이해하려면 Coordinator의 구성 요소에 대한 이해가 필요하다.

Coordinator의 내부는 아래 그림과 같이 구성되어 있다.

부모 Coordinator와 자식 Coordinator의 이해

HomeCoordinator는 AddDiaryCoordinator를 자식으로 가진다.

PetCoordinator는 AddDiaryCoordinator를 자식으로 가진다.

자식이 무엇이냐는 얘기를 하기 앞서, Home / Pet / AddDiary 등등 코디네이터의 명칭을 결정하는 요인은 무엇일까 생각해 보자.

여기서부터는 이해를 돕기 위해 Coordinator 명칭에 대한 한 가지 규칙을 세우고자 한다.

Coordinator의 명칭은 Coordinator가 생성하는 ViewController가 무엇이냐는 것에 따라 결정된다.

(주의! 🔥 실제로는 편의에 따라 이름을 생성하면된다. 흐름에 따라 Coordinator를 생성한다면 흐름에 맞는 작명을 추천한다. 다만 나는 이러한 방식으로 이해하고부터 남의 코드를 읽을 수 있게 되었다)

하지만 그림 상에서 HomeCoordinator와 PetCoordinator가 AddDiary라는 VC를 생산하고 있다.

앞선 설명 대로라면 각각의 CoordinatorHomeVCPetVC를 생성해야 하기에 혼란스럽다.

한 번 꼬이면 당황스럽기 때문에 천천히 정리해 보자.

아래 설명은 A -> B 로의 화면 전환 상황을 가정한다.
(AVC와 BVC는 AViewController, BViewController를 의미)

  1. ACoordinator에게는 AVC를 생산하고 Navi에 push / present 할 책임이 있다.
  2. AVC는 자신이 관리하는 화면 위 버튼이 눌리면 BVC로의 전환을 이루어야 한다.
  3. 그러니 ACoordinatorAVC가 필요로 하는 다음 화면 생성자 = BCoordinator를 가지고, 버튼이 눌린 시점 AVC의 요청에 따라 자식 BCoordinator를 생성, 사용한다.
  4. ACoordinator에 의해 생성된 BCoordinatorBVC를 생성하고 Navipush한다.

여기서,
BCoordinator에게 ACoordinator란 부모이고
ACoordinator에게 BCoordinator란 자식이다

코디네이터 패턴에서 화면 전환의 재사용이 가능하고, 화면 모듈화가 가능하다는 대목이 바로 이러한 부분이다.

어떤 VC 건 간에 DiaryVC로의 화면 전환이 필요하다면 해당 화면을 생성하는 Coordinator를 자식으로 가지면 되기 때문이다.

급작스레 AVCF 화면으로의 이동이 필요한 버튼이 생긴다면 ACoordinatorFCoordinator를 생성하여 사용하는 메서드를 가지면 된다. 생성된 FCoordinator자식 Coordinator로서 부모로부터 배열 형태로 관리된다.

부모가 자식 코디네이터를 배열 형태로 관리하는 이유는 해당 childCoordinator가 할당 해제되는 일을 방지하기 위함이다.

AVC는 그저 자신을 생성한 Coordinator에게 F로 이동해달라 요청하기만 하면 된다. (Coordinating이라는 프로토콜을 통해 요청해도 된다. 참고)

이러한 구조가 Coordinator의 기본 뼈대이다.

각각의 Coordinatorstartfinish를 기본 메서드로 가진다.

Coordinator의 기본 틀은 아래 네 가지만 알고 있으면 된다.

protocol Coordinator: AnyObject{
  var navigationController: UINavigationController {get set}
  
  var childCoordinators: [Coordinator] {get set}
  var delegate: CoordinatorDelegate? {get set}
  
  func start()
  func finish()
  
}

본 포스팅은 코디네이터 패턴의 필요성을 다루기 위해 작성되었으므로 글을 마무리 할까 했지만, 예제 코드에 대한 내용이 있어야 할 것 같아 간략히 언급하고자 한다.

Coordinator 코드로 써보기

AppCoordinator를 통한 앱 최초 화면 설정

아래 코드는 AppDelegate에서 앱이 실행될 때 AppCoordinator를 통해 최초 화면에 해당하는 VC를 생성하는 모습이다.

@main
class AppDelegate: UIResponder, UIApplicationDelegate{
  var window: UIWindow?
  var appCoordinator: AppCoordinator?
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    self.window = UIWindow(frame: UIScreen.main.bounds)
    
    let navigationController = UINavigationController()
    
    appCoordinator = AppCoordinator(
      navigationController: navigationController
    )
    
    self.window?.rootViewController = navigationController
    self.window?.makeKeyAndVisible()
    
    appCoordinator?.start()
    
    return true
  } 
}

최초 화면이기에 NavigationController가 생성되어 window의 rootViewController로 설정되었다.

이후 appCoordinator의 start()를 실행하면 Coordinator의 내부에서 해당 Coordinator가 담당하는 화면이 생성되어 navagtion에 push 되겠구나 유추할 수 있다.

SplashCoordinator의 내부 코드의 일부는 아래와 같다.

//  AppCoordinator.swift
init(
    navigationController: UINavigationController
  ) {
    self.navigationController = navigationController
  }
  
  ...
  
  func start() { 
	let splashViewController = SplashViewController(
      viewModel: SplashViewModel(
        coordinator: self
      )
    )
    self.pushViewController(viewController: splashViewController)
  }

AppCoordinator는 생성 시 화면 전환을 위해 자신이 접근해야 할 navigatinoController를 전달 받는다.

start() 메서드 내에는 자신이 생성할 책임이 있는 ViewController를 생성한다. AppCoordinator가 생성할 책임이 있는 ViewController는 SplashViewController이다.

(앞선 설명에서는 Coordinator가 생성하는 VCCoordinator명칭을 반드시 같게 설정해 주었다. 그러니 설명 대로라면 AppCoordinator가 아닌 SplashCoordinator라고 명명했어야 한다. 그러나 위 Coordiantor는 AppCoordinator가 더 걸맞은 명칭인 것 같아 SplashCoordinator라고 명명하지 않았다)

(마찬가지로 Coordinator는 반드시 한 화면에 대한 생성만을 책임지지 않아도 된다. 예를 들어 start메서드 내에서 로그인 여부에 따라 로그인 화면을 생성할 지, 홈 화면으로 이동할 지 선택할 수 있다. 앞선 Coordinator 이름 규칙은 단순히 이해를 위한 것일 뿐이다) (참고)

해당 코드는 MVVM과 결합된 형태로, Coordinator에게 화면 삽입 요청을 하는 위치가 ViewModel이기에 SplahViewModel에서 나(AppCoordinator)를 통해 다른 화면 삽입을 요청할 수 있도록 나(AppCoordinator = self)를 전달한다.

splashViewModel에서는 로그인 화면으로의 전환을 요청을 필요로 한다. 따라서 AppCoordinator는 이런 메서드를 만들 수 있다.


func moveAuth() {
    let authCoordinator = AuthCoordinator(
            navigationController: self.navigationController
          )
    authCoordinator.delegate = self //부모 코디네이터가 self임을 밝힘
    //authVC가 사라질 때 (화면이 pop) authCoordinator가 부모의 child에서 authCoordinator(자기 자신)를 지우기 위한 연결
    authCoordinator.start() //AuthVC 생성
    self.childCoordinators.append(authCoordinator) 
}

이제 SplashViewModelcoordinator.moveAuth()와 같은 접근을 통해 화면 전환을 이룰 수 있다.

Coordinator를 보고 화면 간의 관계(비즈니스 로직)를 이해할 수 있다. 더불어 ViewController와 ViewController 간의 이해 관계가 사라진다.

지금까지 코드로 Coordinator가 화면을 생성하는 일을 어떻게 대리하는 지 알아 보았다. 그렇다면 화면을 지워야 하는 경우에는 어떨까?

자식 Coordinator는 자신의 VC를 Navi에서 내릴 때(pop) 자신을 소유한 부모에게 finish 메서드를 전달하고, 소유 관계에서 순환참조 문제가 발생하지 않도록 부모는 자신의 자식 배열에서 해당 child를 삭제한다.


본 포스팅에서 이 이상으로 실제 Coordinator를 생성하고 적용한 코드를 다루진 않는다.

하지만 이전의 나와 같은 혼란을 겪고 있는 분들이 있다면, 포스팅이 도움이 되었길 바란다.

내가 참고했던 다양한 포스팅을 아래 남겨둔다.

더불어 현 프로젝트에서 사용하는 Coordinator Protocol에 대한 코드와 간략한 주석을 덧붙인다.


import UIKit
import RxSwift
import RxCocoa

protocol CoordinatorDelegate: AnyObject {
  func didFinish(childCoordinator: Coordinator) 
  //ChildCoordinator 쪽에서 ParentsCoordinator에게 ChildVC 소멸을 알리기 메서드
}

protocol Coordinator: AnyObject{
  var disposeBag: DisposeBag { get }//Rx 사용으로 인한 특이점 (선택사항)
  var navigationController: UINavigationController {get set}
  //Coordinator가 VC 생성 후 push할 Navigation
  
  var childCoordinators: [Coordinator] {get set}
  //Coordinator가 생산한 VC가 가진 화면 전환들
  var delegate: CoordinatorDelegate? {get set}
  //ChildCoordinator 소멸 시 부모 Coordinator에게 접근하기 위한 delegate
  
  func start() //Coordinator가 담당하는 VC 생성 
  func finish() //Coordinator 및 관련 모든 Coordinator 삭제
  
  //MARK: Navigation 동작
  func pushViewController(viewController vc: UIViewController )
  func popViewController()
  
  //MARK: Modal 동작
  func presentViewController(viewController vc: UIViewController )
  func dismissViewController()
  
}

extension Coordinator{
  
  /// finish가 호출되면 -> delegate를 self로 할당하면서 didFinish를 정의한놈의 child를 모두 지워줌
  func finish() {
    childCoordinators.removeAll()//내 자식들 지우고
    delegate?.didFinish(childCoordinator: self)//부모(delegate)에게 나(childeCoordinator 중 self)를 지우라는 메서드 
  }
  
  
  func pushViewController(viewController vc: UIViewController ){
    self.navigationController.setNavigationBarHidden(true, animated: false) /// push되는 네비바는 기본적으로 false -> 커스텀 하는걸로 하는건 어떤지
    self.navigationController.pushViewController(vc, animated: true)
  }
  
  func popViewController() {
    self.navigationController.popViewController(animated: true)
  }
  
  func presentViewController(viewController vc: UIViewController){
    self.navigationController.present(vc, animated: true)
  }
  
  func dismissViewController() {
    navigationController.dismiss(animated: true)
  }
  
}

마치며,

본 포스팅을 통해 Coordinator 패턴에 대해 이해하는 시간을 가졌습니다.

여러번 덧붙이고 삭제하며 글을 다듬었더니 매끄럽지 않은 문장들이 생겨 양해를 구합니다.

혹시 저와 같이 Coordinator가 가진 화면 전환 이라는 키워드에 매몰되어 Coordinator가 Navigation Controller를 대리하는 것이라는 오해를 가진 분들이 있다면 그에 대해 해소되는 시간이었길 바랍니다.

물론 Coordinator가 책임지는 화면 전환에 대한 범위는 팀원의 성향과 팀의 목적에 따라 달라질 수 있다고 생각합니다.

더불어 Coordinator 도입이 정말 가치 있는 일인가에 대한 것은 더 겪어봐야 알 수 있을 것 같습니다.

궁금한 점이나 더 개선할 점이 있다면 언제든 댓글을 부탁 드립니다.

감사합니다 ! 다들 즐거운 개발되세요 !

초기 코디네이터 이해에 많은 도움을 얻은 자료

  1. 코디네이터 사용 상황에 대한 이해 및 코드 사용법 상세
  2. 탭바 컨트롤러와 네비게이션 컨트롤러의 결합 이해
  3. 코디네이터 사용 코드 이해 (영문)
  4. 코디네이터 등장 배경에 대한 이해(영문)
  5. 앞선 화면 처리 방식에 대해 이해하기 위한 추가 자료(영문)
  6. 애플 공식 NavigationController
  7. Coordinator 사용 예제와 장점
profile
iOS 개발자 보노

1개의 댓글

comment-user-thumbnail
2023년 7월 4일

잘 읽고 갑니다. 개발에 대해 깊은 관심이 보이는 글이네요! 본 받아야겠습니다 화이팅 하세요!!!

답글 달기