본문 바로가기

iOS/PROJECT

[TUDY] Coordinator 패턴 적용기 !!

프로젝트가 거의 끝나가면서 TUDY 프로젝트에 담당한 코디네이터 패턴을 적용했던 경험을 적어보려 합니다!

TUDY 프로젝트는 MVC 입니다.

 

Coordinator Pattern 접하기

우선 코디네이터 패턴을 처음 접하게 된 것은 부스트캠프 프로젝트를 구경하다가 코디네이터 패턴이라는 것을 쓰고 있길래

어떨때 사용하는 것이지? 하고 찾아보았습니다.

https://github.com/boostcampwm-2021

 

코디네이터 패턴에 대한 자세한 설명은 생략하지만

결론은 화면전환의 부담을 뷰컨트롤러에서 코디네이터로 모두 위임하겠다는 것이 코디네이터 패턴의 주 목적이었습니다.

 

 

Coordinator Pattern 사용 이유

1

저희 TUDY 프로젝트에서는 로그인 -> 회원가입의 절차에 대략 5개의 뷰가 있었고 

여러 뷰에서 각각 뷰를 생성하는 것보다 코디네이터에서 한번에 관리하는 것이 좋겠다는 생각이 들었습니다.

 

2

개인채팅방 (초대) -> 단체채팅방 (이동)과 게시글 (채팅보내기) -> 개인채팅방 (이동) 의 기능 이 있는데

이를 뷰컨트롤러에서 구현하게 되면 각 뷰컨의 연결이 너무 긴밀하게 연결 될 것 같았고,

코디네이터에서 관리하게 된다면 뷰컨에서 서로의 뷰를 신경쓸 필요가 없다고 생각했습니다.

 

3

새로운 프로젝트에서 새로운 도전을 해보기 위해서!

 

그래서 팀원분들에게 전달했었던 메세지..ㅎㅎ

정말 다들 흔쾌히 좋다고 해주셔서 이틀동안 공부하고 바로 도입해보았습니다.

 

 

구성도를 그려보자!

처음 그렸던 코디네이터 구성도

탭바, 로그인, 회원가입, 마이페이지가 앱 코디네이터 아래에 존재하고,

홈과 채팅 코디네이터가 탭바 아래에 존재하는 구성도였습니다.

 

이 구성도는 개발의 막바지인 지금 이렇게 바뀌었습니다.

 

 

(마이페이지가 빠져있습니다.)

로그인 코디네이터와 회원가입 코디네이터를 합치고

탭바 코디네이터 아래에 모든 코디네이터가 존재하는 구성도로 바뀌었습니다..!

 

이렇게 바꾸게 된 이유는..

앱을 시작하자마자 회원가입 창을 보여주는 것보다 앱을 탐색하다 필요할 때 로그인, 회원가입 창을 보여주는 것이 사용자 경험에 더 좋다고 생각했기 때문입니다.

 

그래서 홈 코디네이터와 채팅 코디네이터에서 로그인 코디네이터를 생성해서 보여주도록 구성도를 바꾸었습니다.

 

Coordinator를 구현해보자

https://somevitalyz123.medium.com/coordinator-pattern-with-tab-bar-controller-33e08d39d7d

 

Coordinator pattern with Tab Bar Controller

What is Coordinator pattern?

somevitalyz123.medium.com

기본 Coordinator와 Tab Bar를 코디네이터로 적용시키는 방안은 이 블로그를 많이 참고하였습니다.

사실 구현 방법도 처음인지라 거의 비슷하게 가져갔던 것 같습니다.

 

이 블로그에선 로그인, 탭 코디네이터가 같은 depth?지만

TUDY에서는 Tab Coordinator 아래에 Login Coordinator가 존재하도록 구현하였습니다.

 

모든 코디네이터가 채택하는 기본적인 코디네이터 프로토콜입니다.

import UIKit

protocol Coordinator: AnyObject {
    
    /// 부모 코디네이터가 자식이 finish 됐을 때 알 수 있도록 돕는 delegate 프로토콜
    var finishDelegate: CoordinatorFinishDelegate? { get set }
    /// 각각의 코디네이터는 하나의 네비게이션 컨트롤러를 가지고 있습니다.
    var navigationController: UINavigationController { get set }
    /// 모든 하위 코디네이터를 가지고 추적하는 배열, 대부분의 경우 이 배열에는 하위 코디네이터가 하나만 포함됩니다.
    var childCoordinators: [Coordinator] { get set }
    /// 코디네이터 타입
    var type: CoordinatorType { get }
    
    init(_ navigationController: UINavigationController)
    
    func start()
    func finish()
}

extension Coordinator {
    
    func finish() {
        childCoordinators.removeAll()
        finishDelegate?.coordinatorDidFinish(childCoordinator: self)
    }
}

 

 

코디네이터를 구현할때 이 프로토콜을 채택한 각자의 할일을 적은 코디네이터 프로토콜을 채택하여 만들었습니다.

 

Coordinator 프로토콜을 채택한 HomeCoordinator 프로토콜입니다.

홈과 게시글, 검색화면에서 일어나는 일들이 존재합니다. (push와 show는 통일을 해야..)

import Foundation
import UIKit

protocol HomeCoordinatorProtocol: Coordinator {
    
    var homeViewController: HomeViewController { get set }
    
    func pushSearchViewController()
    func pushFastSearchViewController(work: String)
    func showProjectWriteViewController()
    func registerProject(viewController: UIViewController)
    func updateProject(viewController: UIViewController)
    func showModifyProject(project: Project)
    func pushProjectDetailViewController(project: Project)
    func showLogin()
}



// HomeCoordinator

final class HomeCoordinator: HomeCoordinatorProtocol { ... }

 

 

Coordinator Pattern 구현에서 어려웠던 점 - 1

그래서 코디네이터와 뷰컨트롤러는 어떻게 소통하는 것이지?

 

고려했었던 점은 코디네이터는 뷰컨트롤러를 알지만, 뷰컨트롤러는 코디네이터를 알 수 없다는 점이었습니다.

 

뷰 컨트롤러에 클로저를 선언하고, 코디네이터에서 이 클로저를 구현해줍니다.

그리고 화면 전환이 필요할 때 뷰컨트롤러에서 이 클로저를 호출해주는 방식으로 구현하였습니다. (참조 블로그)

 

예를들어 홈 뷰컨트롤러에서 didSendEventClosure 클로저 변수를 선언해두고

enum Event {
    case showSearch
    case showProjectWrite
    case showProjectDetail(project: Project)
    case showLogin
    case showFastSearch(work: String)
}

var didSendEventClosure: ((Event) -> Void)?

홈 코디네이터 start() 함수에서 홈 뷰컨틀롤러를 푸시할 때 Event마다 어떤 일을 할지 설정해둡니다.

func start() {
    homeViewController.didSendEventClosure = { [weak self] event in
        switch event {
        case .showSearch:
            self?.pushSearchViewController()
        case .showProjectWrite:
            self?.showProjectWriteViewController()
        case .showProjectDetail(let project):
            self?.pushProjectDetailViewController(project: project)
        case .showLogin:
            self?.showLogin()
        case .showFastSearch(let work):
            self?.pushFastSearchViewController(work: work)
        }
    }
    self.navigationController.pushViewController(self.homeViewController, animated: true)
}

그리고 화면 전환이 필요할 때 didSendEventClosure만 호출해주면 뷰컨트롤러에서는 간단하게 화면전환을 할 수 있게 됩니다.

@objc private func didTapProfile() {
    if isLogin() {
        //로그인 되어 있으면 마이페이지로
    }
    didSendEventClosure?(.showLogin)
}

@objc private func didTapFakeSearchBar() {
    didSendEventClosure?(.showSearch)
}

@objc private func didTapFloatingButton() {
    if isLogin() {
        didSendEventClosure?(.showProjectWrite)
    } else {
        didSendEventClosure?(.showLogin)
    }
}

 

 

Coordinator Pattern 구현에서 어려웠던 점 - 2

코디네이터 - 코디네이터 관계는 어떻게 소통을 하나?

 

TUDY에는 홈, 채팅화면에서 로그인이 되어있지 않다면 로그인 화면으로 이동하는 기능과

게시글 상세 화면에서 채팅보내기 버튼을 누르면 개인채팅 화면으로 이동하는 기능이 있습니다.

위에서는 뷰컨트롤러 - 코디네이터 관계였다면

이 기능은 코디네이터 - 코디네이터 관계로 다른 코디네이터의 화면을 보여주어야 했습니다.

 

이 관계는 Delegate 패턴으로 해결할 수 있었습니다.

 

HomeCoordinatorDelegate가 할 일은 개인채팅 화면을 보여주는 것과 로그인화면을 보여주는 것입니다.

import Foundation

// 홈 코디네이터 Delegate
protocol HomeCoordinatorDelegate: AnyObject {
    func showPersonalChat(with projectWriter: User)
}

// 로그인 Coordiantor Delegate
import Foundation

protocol LoginCheckDelegate: AnyObject {
    func prepareLoginCoordinator()
}

홈 코디네이터에서 HomeCoordinatorDelegate를 순환참조가 일어나지 않게 weak var로 생성해주고 

weak var loginDelegate: LoginCheckDelegate?
weak var homeDelegate: HomeCoordinatorDelegate?

이를 실행할 Delegate는 TabBarCoordinator로 설정해줍니다!

case .home:
    homeCoordinator.finishDelegate = self
    homeCoordinator.loginDelegate = self
    homeCoordinator.homeDelegate = self
    self.childCoordinators.append(homeCoordinator)
    homeCoordinator.start()

 

TabBarCoordinator인 이유는 홈과 로그인, 채팅 코디네이터의 상위가 모두 탭바 코디네이터이기 때문입니다.

 

Delegate 패턴을 사용해서 TabBar 코디네이터에게 일을 시킬 수 있는 것입니다.

func showLogin() {
    self.loginDelegate?.prepareLoginCoordinator()
}

 

FinishDelegate를 사용하지 않았던 이유는 FinishDelegate는 코디네이터가 완전히 종료되었을 때

코디네이터를 삭제할 때 호출하기 때문에 여기서는 홈을 삭제할 이유가 전혀 없어 새로운 Delegate를 생성하였습니다.

 

Coordinator Pattern 구현에서 혼동했던 점

네비게이션바 커스텀도 코디네이터에서 하는 것인가?

 

처음에 팀원들과 헷갈렸던 점이었습니다. 

"화면전환"의 역할을 네비게이션의 모든 역할이라고 착각하여 push할 때 마다 네비게이션 바 커스텀을 해주었지만

pop 할 때의 네비게이션 바 커스텀을 코디네이터가 인지할 수 없었습니다.

 

그래서 팀원들과 네비게이션 바는 View의 영역이기 때문에 ViewController에서 하는게 맞고, 

코디네이터는 "화면전환"의 역할만 담당한다.

라고 결론을 내어 pop 할 때의 커스텀은 ViewWillAppear() 함수에서 하기로 했습니다.

 

 

Coordinator Pattern 후기

처음 구현했을 때만해도 화면전환을 하는데 이렇게 많은 코드를 작성해야하나? 싶었는데

다양한 화면전환을 코디네이터에서만 구현해보니

뷰컨트롤러에서는 단지 didSendEventClosure만 호출하니까 확실히 뷰컨트롤러의 화면전환에 대한 부담을 줄일 수 있었습니다.

 

그리고 똑같은 뷰를 보여주는 코드를 같은 코디네이터에서 재사용할 수 있었기 때문에 재사용의 부분에도 유리한 부분이 있었습니다.

 

하지만 화면전환에 있어 복잡한 프로젝트가 아니고 간단한 프로젝트라면 투머치가 될 수 있다는 생각이 들었습니다.

 

새로운 도전으로 지식이 늘어난 기분을 느낄 수 있었습니다!!

 

 

 

 

 

참고

https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930

 

Coordinator Pattern

Coordinator의 시작부터 간단한 사용까지

zeddios.medium.com

https://jeonyeohun.tistory.com/310

 

[iOS] 메이트러너: 코디네이터 패턴 적용기

메이트 러너 앱 개발 과정을 공유하는 포스트입니다! GitHub - boostcampwm-2021/iOS06-MateRunner: 함께 달리는 즐거움, Mate Runner 🏃🏻‍♂️🏃🏻‍♀️ 함께 달리는 즐거움, Mate Runner 🏃🏻‍♂️🏃🏻

jeonyeohun.tistory.com

https://somevitalyz123.medium.com/coordinator-pattern-with-tab-bar-controller-33e08d39d7d

 

Coordinator pattern with Tab Bar Controller

What is Coordinator pattern?

somevitalyz123.medium.com

http://labs.brandi.co.kr/2020/06/16/kimjh.html

 

화면 전환을 해결해 준 Coordinator 패턴

리액티브 프로그래밍(Reactive Programming)을 사용하기 위해, VIPER 패턴으로 되어 있는 앱 구조를 MVVM 패턴으로 변환하는 작업을 시작했을 때였습니다. MVVM 패턴을 사용해 보지 않았지만, VIPER 패턴의 I

labs.brandi.co.kr