Conversation
There was a problem hiding this comment.
🤖 AI 코드 리뷰 - 아키텍처 관점
SceneDelegate에 인라인으로 작성되어 있던 화면 전환 로직 전체를 Coordinator 패턴으로 분리한 PR이다. ScreenFactory를 통해 DI를 일원화하고, View에서 직접 DIContainer를 참조하던 코드를 제거한 점은 의존성 방향 측면에서 개선이다. 다만 몇 가지 구조적 문제가 잔존한다. 첫째, Presentation 계층의 ViewController가 Coordinator의 구체 타입을 직접 참조하고 있어 계층 간 결합이 강하게 남아 있다. 둘째, HomeCoordinator가 UseCase를 직접 생성·실행하는 joinAppointment 메서드를 보유해 Coordinator의 책임 범위를 벗어난다. 셋째, ChatCoordinator.showAppointmentInfo가 ViewController를 반환하는 설계는 Coordinator 추상화와 일관성이 없다.
| final class AppointmentCreationViewController: UIViewController { | ||
|
|
||
| private let viewModel: AppointmentCreationViewModel | ||
| weak var coordinator: HomeCoordinator? |
There was a problem hiding this comment.
[HIGH] weak var coordinator: HomeCoordinator?처럼 Presentation 계층이 Coordinator 구체 타입을 직접 참조하고 있다. 동일한 패턴이 AppointmentListViewController, ChatViewController, HomeViewController 등 전체 ViewController에 반복된다. Coordinator를 프로토콜로 추상화하지 않으면 ViewController가 특정 Coordinator 구현에 강결합되어, 화면 흐름 변경 시 ViewController 코드도 함께 수정해야 하고 단독 테스트도 불가능하다. 각 ViewController가 필요로 하는 화면전환 인터페이스만 선언한 프로토콜(예: AppointmentCreationCoordinatorProtocol)을 정의하고, coordinator 프로퍼티를 해당 프로토콜 타입으로 선언해야 한다.
There was a problem hiding this comment.
weak var coordinator: HomeCoordinator? 처럼 구체 타입을 직접 참조하던 부분을
각 ViewController가 필요로 하는 화면전환 메서드만 선언한 프로토콜로 추상화했습니다.
프로토콜 단위는 실제로 그 프로토콜을 쓰는 ViewController 기준으로 나눴습니다.
예를 들어 ChatCoordinator 하나가 ChatViewController와 AppointmentInfoViewController 양쪽에 쓰이는데,
두 화면이 실제로 호출하는 메서드가 겹치지 않아서
ChatCoordinating / AppointmentInfoCoordinating 두 프로토콜로 분리하고,
ChatCoordinator가 둘 다 채택하도록 했습니다.
AppointmentRouteViewController의 RouteSearchCoordinator 직접 생성/소유 문제도 함께 정리했습니다. AppointmentRouteViewController는 이제 AppointmentRouteCoordinating 프로토콜의 showRouteSearch(from:destination:)만 호출하고, RouteSearchCoordinator의 생성과 소유는
이 화면을 실제로 present한 상위 Coordinator가 담당하도록 옮겼습니다.
|
|
||
| // MARK: - Join | ||
|
|
||
| func joinAppointment(code: String, completion: @escaping (Result<Void, Error>) -> Void) { |
There was a problem hiding this comment.
[HIGH] joinAppointment(code:completion:) 메서드 안에서 JoinAppointmentUseCase를 직접 생성하고 실행하고 있다. Coordinator의 역할은 화면 전환 조율이며, 비즈니스 로직 실행은 ViewModel 또는 UseCase 계층의 책임이다. 현재 구조에서는 screenFactory.container.resolve(...)를 통해 Repository를 꺼내 UseCase를 만드는 코드가 Coordinator에 존재한다. 이 로직은 HomeViewModel 혹은 별도의 ViewModel로 이동시키고, Coordinator는 결과에 따른 화면 전환만 담당해야 한다.
There was a problem hiding this comment.
로직을 HomeViewModel로 옮겼습니다.
같은 패턴이 MyPageCoordinator.start()에도 있어서(MyPageViewModel을 UseCase까지 직접 조립) 함께 정리했습니다.
|
|
||
| // MARK: - Appointment Info | ||
|
|
||
| func showAppointmentInfo(appointmentID: String) -> AppointmentInfoViewController { |
There was a problem hiding this comment.
[MEDIUM] showAppointmentInfo(appointmentID:) -> AppointmentInfoViewController가 ViewController 인스턴스를 반환한다. 호출부인 ChatViewController에서 _ = coordinator?.showAppointmentInfo(...)로 반환값을 버리고 있어, 반환 타입 자체가 현재 사용되지 않는다. Coordinator가 ViewController를 반환하는 설계는 Coordinator의 책임 범위(화면 전환)를 벗어나며, 나머지 show* 메서드가 모두 Void를 반환하는 것과 일관성이 없다. 반환값이 실제로 필요한 경우라면 콜백 패턴으로 대체하고, 그렇지 않으면 반환 타입을 Void로 변경해야 한다.
There was a problem hiding this comment.
showAppointmentInfo(appointmentID:)의 반환값(AppointmentInfoViewController)이
호출부(ChatViewController)에서 _ =로 버려지고 있던걸 Void를 반환하도록 수정했습니다.
| tabBarController.viewControllers = [homeNav, listNav, myPageNav] | ||
| tabBarController.tabBar.tintColor = .blue1 | ||
| return tabBarController | ||
| let coordinator = AppCoordinator(window: window!) |
There was a problem hiding this comment.
[MEDIUM] window! 강제 언래핑을 사용하고 있다. 바로 윗줄(17)에서 window = UIWindow(windowScene: windowScene)으로 할당했으므로 nil이 아님을 알 수 있지만, guard let으로 안전하게 꺼낸 뒤 사용하는 방식이 구조적으로 더 안전하다. guard let window 바인딩 이후 AppCoordinator(window: window)로 전달하면 강제 언래핑 없이 동일하게 동작한다.
|
|
||
| private let viewModel: AppointmentRouteViewModel | ||
| private var cancellables = Set<AnyCancellable>() | ||
| private var routeSearchCoordinator: RouteSearchCoordinator? |
There was a problem hiding this comment.
[MEDIUM] private var routeSearchCoordinator: RouteSearchCoordinator?를 ViewController가 직접 소유하고 있다. AppointmentRouteViewController는 ChatCoordinator 또는 HomeCoordinator에 의해 present되는 화면인데, 그 안에서 다시 하위 Coordinator를 생성·보유하는 구조가 된다. Coordinator 트리의 소유권이 Coordinator가 아닌 ViewController에 분산되어, 생명주기 관리가 불명확해진다. RouteSearchCoordinator의 생성과 소유는 AppointmentRouteViewController를 present한 상위 Coordinator가 담당하거나, 최소한 AppointmentRouteViewController에 coordinator 프로퍼티를 주입하는 방식으로 일관성을 맞춰야 한다.
There was a problem hiding this comment.
같이 처리했습니다.
sangYuLv
left a comment
There was a problem hiding this comment.
조립하는 ScreenFactory를 만든 점이 정말 좋은 것 같아요!
수고 많으셨습니다 🦦
중요한 작업인만큼 한 번 더 AI 리뷰를 받아보는 건 어떨까요?
재리뷰에서도 좋은 얘기를 해주기도 하더라구요!
이번 작업을 읽으면서 느낀 건데, 전체적으로 네이밍 수정/검토가 필요할 것 같아요.
단어 조합(e.g. PlaceSelection, PlaceSearch, RouteSearch)이 어떤 화면/기능을 의미하는지 잘 떠오르지 않거나 헷갈리기도 하네요.
큰 변화는 없을 것 같지만, 괜찮으시다면 나중에 한 번 작업을 진행해보겠습니다!
제 코멘트에 대한 답변이나 수정 작업이 모두 완료되면 리뷰 재요청 부탁드립니다. 파이팅 🫡
| chatCoordinator = coordinator | ||
| chatViewController.coordinator = coordinator | ||
| push(chatViewController) | ||
| } |
There was a problem hiding this comment.
제가 파악하기로는 이 coordinator에서 채팅 화면을 표시하는 로직이 3갈래 존재합니다.
func replaceAppointmentCreation(_ appointmentCreationViewController: UIViewController, withChatFor appointmentInfo: AppointmentInfo): 약속 생성 후 채팅 화면 이동showChat(appointmentInfo: AppointmentInfo): 약속 코드로 참여 후 채팅 화면 이동showChat(appointmentID: String): 이외 케이스
위 메소드 안에서 쓰이는 screenFactory의 메소드도 다른데, 약속 정보를 조회한 후 표시하는지와 가지고 있는 약속 정보를 기반으로 표시하는지의 차이점을 확인했습니다.
showChat(~)과 달리 replaceAppointmentCreation()은 push()대신 직접 viewControllers를 관리하는 로직을 포함하는데, 어떤 차이를 두고자 하셨는지 궁금합니다.
There was a problem hiding this comment.
showDatePicker(= showTimePicker)나 showPlaceSearch(=showSearchPlace)처럼 반복되는 coordinator 메소드 구현부는 한 곳으로 모으면 어떨까요?
최대로 반복되는 횟수가 3번이어서, 만약 바꾸지 않는 쪽을 선호한다면 메소드 이름이라도 통일하면 좋을 것 같습니다!
|
|
||
| init( | ||
| navigationController: UINavigationController, | ||
| screenFactory: ScreenFactory = ScreenFactory() |
There was a problem hiding this comment.
ScreenFactory를 필요로 하는 coordinator들은 초기화 메소드에서 기본값으로 새 인스턴스를 생성하고 있습니다.
전역에서 한 screenFactory만 가져도 괜찮지 않을까요?
더해서 각 coordinator마다 필요로 하는 스크린 생성 메소드가 다른데, 프로토콜로 분리해 접근을 제한하면 어떨까요?
구현부는 screenFactory의 extension으로 잘 분리되어있지만, 특정 coordinator가 필요한 메소드만 알고 있도록 해도 좋을 것 같아서 제안드려봅니다!
There was a problem hiding this comment.
이 로직이면 앱 실행마다 로그인을 요구할 것 같아서 일정 기간 로그인 상태를 유지하는 기능을 추가하면 좋을 것 같습니다!
어떻게 생각하시나요?
괜찮으시다면 백로그에 추가하겠습니다.
JIRA
📝 작업 내용
📌 요약
Coordinator(화면전환 전담)와ScreenFactory(조립 전담)로 분리했습니다.ChatViewModel조립,SearchPlaceCardViewController조립)을ScreenFactory쪽 공용 메서드 하나로 통합했습니다.🔍 상세
1. Coordinator / ScreenFactory 기반 구조 도입
"Repository resolve → UseCase 조립 → ViewModel 생성 → push/present"까지 전부 담당하고 있었습니다.
Coordinatorstart()만 갖는 최소 프로토콜입니다.AppCoordinator의 루트 화면 교체)도 채택할 수 있도록UINavigationController요구사항을 넣지 않았습니다.NavigationCoordinatorCoordinator를 상속하며navigationController: UINavigationController를 요구하는 프로토콜입니다.push,present,presentInNavigationController등 push/present 보일러플레이트를 extension으로 제공합니다.ScreenFactoryDIContainer를 들고 있으면서,화면별 조립 메서드(
makeChatViewController,makeAppointmentRouteViewController등)를화면 단위 extension 파일로 나눠 제공합니다.
2. 탭/화면별 Coordinator 도입
AppCoordinator/TabBarCoordinatorSceneDelegate가 직접 하던 로그인 화면 표시,로그인 성공 시 탭바 전환(cross-dissolve 애니메이션 포함),
3개 탭의
UINavigationController및 탭 Coordinator 생성을 이전했습니다.HomeCoordinator/AppointmentListCoordinator/MyPageCoordinatorAppointmentListCoordinator는AppointmentListViewController와PastAppointmentListViewController가같은 내비게이션 스택을 공유하므로 하나의 Coordinator로 통합했습니다.
ChatCoordinatorChatViewController가 모달로 띄우던 화면(약속 경로, 약속 정보, 내 위치 공유, 장소 검색/공유, 공유 장소 목록)과그 안의
AppointmentInfoViewController가 갖고 있던 하위 전환(날짜 선택, 장소 검색, 장소 지도 선택)까지 함께 담당합니다.RouteSearchCoordinatorRouteSearchViewController가 항상 자체UINavigationController로 감싸져 모달로 뜨는 독립 흐름이라,그 내비게이션 컨트롤러를 직접 생성해 소유하는 전용 Coordinator로 분리했습니다.
💬 리뷰 노트
1. Coordinator 단독이 아닌 Coordinator + Factory 조합을 선택한 이유
결국 Coordinator 자체가 UseCase 조립까지 떠안게 되어 "조립" 문제는 해결되지 않습니다.
두 문제를 함께 해결할 수 있다고 판단했습니다.
2. Coordinator 프로토콜에서 UINavigationController를 필수로 두지 않은 이유
Coordinator프로토콜 자체에navigationController: UINavigationController를 요구사항으로 뒀는데,AppCoordinator처럼UIWindow.rootViewController를 직접 교체하는 화면전환에는 내비게이션 스택 자체가 필요 없었습니다.Coordinator(최소 프로토콜)와NavigationCoordinator(내비게이션 스택이 필요한 경우에만 채택)로 분리해,push 기반 전환과 루트 화면 교체 양쪽을 모두 자연스럽게 표현할 수 있게 했습니다.
3. ChatCoordinator를 각 탭 Coordinator가 어떻게 공유하는지
그래서
ChatCoordinator를 미리 만들어두지 않고,HomeCoordinator/AppointmentListCoordinator가 채팅 화면을 push하는 시점에자신의
navigationController를 넘겨ChatCoordinator를 생성하고,private var chatCoordinator: ChatCoordinator?로 강하게 소유하는 방식을 택했습니다.