Coordinator 패턴 적용기

zoe·2023년 6월 20일
0
post-thumbnail

시작하기

프로젝트를 구현하면서 코디네이터 패턴에 대해 알게되었다.
MVVM구조로 역할분리를 하는데 성공했다고 생각했는데, 확장성과 프로젝트 규모의 증가 등의 측면에서 이점이 있을것같아 하이웨이 인포에 적용해보았다.

현재 구현의 문제

사실 코디네이터 패턴 없이도 앱은 잘 작동한다.
다만 몇가지 아쉬운점이 있다
1. 재사용성 문제
2. 뷰컨트롤러간의 강한 결합의 문제
3. Massive View Controller
등등...

MVC 패턴에서는 뷰 객체를 생성하고, 띄워주는 순서로 화면전환을 구현했다.
이게 간단한 프로젝트에서는 괜찮은데, 프로젝트 규모가 커질수록 VC의 코드가 길어지고 또 다양한 방식의 화면전환 플로우가 요구될 가능성도 있다.

코디네이터는 위의 문제점을 해결하기 위해 화면전환을 관리하는 독립적인 클래스를 이용하자는 아이디어에서 나왔다.
그럼 본격적으로 코디네이터 패턴에 대해 알아보자

코디네이터 패턴

코디네이터는 UIViewController를 보여주고 숨기는 역할을 한다.

  • 뷰 컨트롤러보다 한 층 위의 레이어에서 컨트롤러들을 관리하기 때문에 뷰 컨트롤러간의 강한 결합문제를 해결할 수 있다.
  • 또, 화면전환에 특화된 클래스 라는 점에서 화면전환 기능의 유지보수가 편리해진다는 장점이 있다.
  • 나의 경우 클린아키텍처와 함께 사용했기 때문에 의존성 주입도 관리해줄 수 있었다.

프로젝트 구조 살펴보기

프로젝트에 어떻게 적용했는지 살펴보기전 구조파악을 위해 앱 플로우를 가져와보았다.

  1. 첫번째 화면에서 검색탭을 누르면
  2. 장소검색뷰가 push되고, 검색어를 탭하면
  3. 맨 마지막 결과화면이 push되는 구조이다.

프로젝트에 적용하기

일단 코디네이터의 가장 기본적인 구조를 형성해주었다.
바로 코디네이터 프로토콜 정의하기 인데, 사람마다 프로토콜의 구성은 다양한것같다.

나의 경우 기본적으로 start() 메서드와 자식코디네이터 프로퍼티를 정의했고,
뷰가 사라졌을때 메모리관리를 해주기 위해 removeChildCoordinator()도 추가 구현해주었다.


AppCoordinator

이제 앱코디네이터를 만들 시간이다. 앱 코디네이터는 SceneDelegate에서 사용해줄것인데, window를 전달받아 앱의 윈도우에 보여지는 presentation을 세팅하게된다.

내 프로젝트는 화면이 탭바로 나누어져 있기때문에 rootViewController에 탭바컨트롤러를 지정해주었다.
start()는 모든 것들이 시작되는 곳이다.
특히 윈도우가 루트뷰에 나타나는 곳이므로, 눈에 보이기위해 필요한 작업들을 이 메서드 안에서 해주었다.

이렇게 정의하고 씬델리게이트에서 appCoordinator.start() 를 호출 해주면 된다.
아래는 씬델리데이트의 코드이다.

 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    self.window = UIWindow(windowScene: windowScene)
    guard let window = window else { return }

    appCoordinator = AppCoordinator(window: window)
    appCoordinator?.start()
 }

화면전환하기

RoadViewControlelr에서 서치바를 탭하면 SearchViewController로 화면전환을 해줘야 한다.

원래라면
인스턴스 생성, 뷰컨에 필요한 자원전달, 화면푸시 등으로 기본 4~5줄의 코드를 작성해야했지만,

코디네이터 패턴 적용 후 화면전환 코드는 모두 1줄이면 끝난다.
이렇게 뷰모델에게 다음뷰를 보여달라는 메서드를 호출하기만 하면 된다.


뷰모델의 구현코드도 심플하다.
코디네이터에게 필요한 위치정보를 전달하고 showSearchView()메서드를 호출하는게 끝이다.

final class RoadViewModel: ViewModelType {
    private let coordinator: RoadCoordinator?
    
 // ... 생략
    
    func showSearchView() {
        guard let currentLocation = currentLocation else { return }
        coordinator?.showSearchView(with: currentLocation)
    }
}

코디네이터의 실제 구현 모습

이제 RoadCoordinator가 어떻게 구현되어 있는지 살펴보자

  • 우선 RoadCoordinator 프로토콜을 채택했는데, RoadCoordinator는 Coordinator프로토콜을 채택한 프로토콜이다.

1. start() 메서드

  • start() 메서드의 코드를 보면 스토리보드에서 인스턴스를 이니셜라이징 해주는것을 알수있다.
  • 그후 MVVM패턴을 사용해주었기 때문에 ViewModel을 만들어서 뷰컨에 할당해주었다.
  • 그렇게 다 만들어진 뷰컨을 네비게이션 컨트롤러에 push 해주면 된다.

2. showService()메서드

다음 화면으로 전환하기 위해 showService()라는 메서드를 정의해주었는데, 이것 역시 start() 메서드와 동일하다

다음 뷰의 코디네이터 인스턴스를 만들어 화면시작 메서드를 호출해주면 된다.
조금 다른 부분이 있다면 방금 만든 코디네이터를 childCoordinator에 append 해주고 있는것을 볼 수 있다. 이건 왜 필요한걸까?


자식코디네이터는 왜 필요하지?

결론부터 말하면 메모리관리를 위해 필요하다.

이번 프로젝트를 하면서 제일 골치아팠던 문제이기도 한데
테이블뷰 셀을 누르면 결과화면이 여러번 푸시되는 문제가 있었다.

원인을 여러 방면으로 찾다가 이글을 읽고 해결할 수 있었다.


뷰를 푸시하고 pop해서 이전화면으로 돌아갔을 때
VC2가 deinit되고 메모리에서 내려가야하는데
화면전환을 여러번 하면 아래 사진 처럼 쓸모없는 코디네이터가 세개나 살아있는게 원인이었다.

이런 메모리 누수를 막기 위하여 우리는 하위뷰들이 pop되어 화면에서 사라질 때 하위뷰의 인스턴스를 제거해줘야 한다고 한다.


자식코디네이터 메모리에서 제거하기

코디네이터 프로토콜 기본구현에 정의해주었던 메서드를 사용해서

extension Coordinator {
    func removeChildCoordinator(_ child: Coordinator) {
        for (index, coordinator) in childCoordinators.enumerated() {
            if coordinator === child {
                childCoordinators.remove(at: index)
                break
            }
        }
    }
}

뷰컨트롤러가 할당해제 되는 시점에 호출해주는 방법으로 해결할 수 있었다.

final class SearchViewController: UIViewController {
    // 생략...
    deinit {
        viewModel.removeCoordinator()
    }
}

짜잔
이제는 쓸모없이 메모리에 남아있는 코디네이터가 없어진 모습을 볼수있다.

profile
개발하면서 마주친 문제들을 정리하는 공간입니다.

0개의 댓글