[Refactor] 메인화면의 프레젠테이션 레이어를 리팩토링합니다. #106
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
배경
기존 메인화면의 코드들에는 다음과 같은 문제점들이 있었습니다.
따라서 중복된 코드들을 제거하고, 로직을 개선하였습니다.
작업 내용
1. 오류 수정
(1) 메인 화면 데이터 fetch 후 화면 갱신이 되지 않는 문제 해결
MainSceneViewController의 bind 메서드에서 화면을 갱신하는 publisher인
fetchPopupDataPublisher에 바인딩하지 않고,fetchPopupImagesErrorPublisher에 두 번 바인딩 했습니다.저는 바보입니다.따라서 원래 의도대로
fetchPopupDataPublisher에 바인딩을 하고, 메인 스레드에서reloadData()를 호출하도록 변경했습니다.(2) 메인화면에서 캐러셀 이미지를 잘못 불러오던 오류 해결
MainSceneViewModel의
provideCarouselImageUrl메서드에서mainSceneDataSource.item()를 호출하여 캐러셀 이미지 url을 받아왔습니다.그러나 해당 메서드는 오늘의 추천 팝업(캐러셀 이미지) 데이터를 반환하는 메서드가 아니라, 찜 목록, 관심사, 지금 놓치면 안 될 팝업스토어의 데이터를 반환하는 함수입니다.
저는 바보입니다.따라서
MainSceneDataSource에 캐러셀 이미지를 반환하는getCarouselImageUrl()메서드를 추가하여 해결했습니다.2. MainSceneSection 열거형 도입
(1) 도입 이유
기존에는 섹션 번호(
indexPath.section)를 기반으로 직접 분기하여 데이터를 처리하고 있었기 때문에 각 섹션이 어떤 역할을 하는지 명확히 표현하기 어려웠습니다.따라서 찜 목록, 관심사, 지금 놓치면 안 될 팝업스토어 섹션을 명시적으로 표현할 수 있는
MainSceneSection열거형을 도입하였습니다.아래와 같이 도메인 모델에
PopupSectionCategory라는 열거형이 있습니다.이 열거형을 사용할까 고민했지만, 현재 구조에서는
userInterest의 연관값으로 인덱스가 들어가는 것이 컬렉션 뷰 데이터 소스 로직 처리에 편리하기에 프레젠테이션 모델 느낌으로 새로 추가했습니다.또한
MainSceneSection에는todayRecommend가 필요없기에 현재 구조에서는 분리하는게 적절하다고 판단했습니다.(2) 변경된 부분
1) sections 배열과 buildSections, updateSections
sections 배열은 오늘의 추천, 찜 목록, 관심사, 지금 놓치면 안될 팝업 섹션들 중 현재 화면에 표시될 섹션들을 담는 상태 변수입니다.
뷰 모델에서 네트워킹 작업(
fetchPopupList()) 이후 섹션 상태를 갱신할 수 있도록updateSections()메서드를 호출합니다.이 과정에서 책임이 분리된 구조로 설계하였습니다.
섹션 계산:
buildSections()상태 변경:
updateSections()네트워킹:
fetchPopupList()buildSections()는 순수하게 섹션 구성을 계산만 하므로 테스트가 용이합니다.updateSections()는 상태를 변경하는 책임만 가집니다.fetchPopupList()는 데이터 fetch 이후updateSections()메서드에 상태 변경을 위임합니다.또한
MainSceneViewController에서는sections라는 상태 변수를 유일하게 신뢰하게 되므로, MVVM의 "뷰는 뷰모델의 상태만 바라봐야 한다"는 원칙에 부합하도록 설계하였습니다.3. MainSceneDataSource 구조 개선
MainSceneDataSource가 원래의 역할인 데이터를 저장하는 역할보다 더 많은 역할을 하게 되었습니다.도메인 모델을 받아 뷰모델로 변경하는 역할을 하고 있었기에 코드가 복잡해졌었습니다.
따라서
MainSceneDataSource는 도메인 모델 타입으로 데이터를 저장하도록 하고, 데이터를 저장만 하도록 구조를 변경했습니다.이에 따라
MainSceneViewModel이 도메인 모델에서 뷰 모델로 가공하도록 하고, 뷰에게 데이터를 전달하는 역할도 수행하도록 변경했습니다.기존에는 뷰 컨트롤러에서
MainSceneViewModel.getDataSource()를 호출하여 뷰 컨트롤러가 데이터 소스에 직접 접근해 데이터를 받아왔습니다.이 구조에서는 뷰 컨트롤러는
MainSceneViewModel도 알아야하고,MainSceneDataSource도 알아야하기 때문에 의존성을 줄일 필요가 있었습니다.따라서 뷰 컨트롤러가 데이터 소스에 접근하기 위해서는
MainSceneViewModel에 접근해MainSceneDataSource에 간접 접근하도록 구조를 변경했습니다.이를 통해 뷰 컨트롤러는
MainSceneDataSource의 구현과 관계없이 데이터를 받아올 수 있게 되었습니다.3. UICollectionView DataSource 메서드 로직 개선
MainSceneViewController의UICollectionView DataSource메서드들은 중복된 코드도 많고, 길고 매우 복잡했습니다.이를 개선하기 위해 다음의 작업을 진행했습니다.
PopupPreviewViewData의convertToImage메서드를 추가해 개선PickOrInterestCell과ClosingSoonPopupCell의 공통 기능인configureContents를 추상화하는 프로토콜을 정의해cellForItemAt메서드 간소화(1) 중복된 이미지 로딩 로직 개선
기존에는
configurePickOrInterestCell,configureClosingSoonPopupCell등 여러 셀 구성 메서드에 이미지 로딩 로직이 중복되어 존재했습니다.따라서 각각의 메서드마다 fetchImage → Result 처리 → 이미지 fallback → main thread 업데이트 로직이 반복되어 가독성과 유지보수성 저하되었습니다.
이를 해결하기 위해
PopupPreviewViewData에convertToImage(from:)메서드를 추가하여 이미지 처리 로직을 ViewModelData 객체로 이동시켰습니다.ViewController에서는fetchImage호출 후convertToImage만사용하도록 하여 책임을 분리하고 로직을 간결하게 했습니다.convertToImage메서드는 이미지 fetch의result: <Data, ImageFetchError>를 파라미터로 받아 내부에서 성공, 실패를 처리합니다.PopupPreviewViewData의 익스텐션을MainSceneViewController에서 선언한 이유는 다음과 같습니다.UIImage를 반환하기 위해 UIKit이 필요 ->
PopupPreviewViewData가 선언된MainSceneViewModel에서 선언하면 뷰 모델이 UI 프레임워크에 의존하게 됨.-> 따라서 뷰 컨트롤러 파일 내부에서 선언해 해당 메서드는 UIKit 전용임을 명시하고자.. 했습니다!
// MainSceneViewController.swift private func configurePickOrInterestCell( _ collectionView: UICollectionView, for indexPath: IndexPath, _ popupData: PopupPreviewViewData ) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: PickOrInterestCell.reuseIdentifier, for: indexPath ) as? PickOrInterestCell else { return UICollectionViewCell() } mainViewModel.fetchImage(url: popupData.popupImageUrl) { result in let image = popupData.convertToImage(from: result) DispatchQueue.main.async { cell.configureContents(with: popupData, image: image) } } return cell } // MARK: - Extensions extension PopupPreviewViewData { func convertToImage(from result: Result<Data, ImageFetchError>) -> UIImage { switch result { case .success(let data): return UIImage(data: data) ?? UIImage(resource: .popupPreviewPlaceHolder) case .failure(let error): print("팝업 프리뷰 이미지 로딩 실패: \(error.localizedDescription)") return UIImage(resource: .popupPreviewPlaceHolder) } } }(2)
PickOrInterestCell과ClosingSoonPopupCell의 공통 기능인configureContents를 추상화하는 프로토콜을 정의해cellForItemAt메서드 간소화cellForItemAt에서 섹션에 따라PickOrInterestCell또는ClosingSoonPopupCell셀을 구성했습니다.이를 위해 switch 문으로 분기하였고, 중복된 코드가 많아 코드 길이를 늘린 주범이었습니다.
따라서 셀 구성 메서드인
configureContents를 추상화한 프로토콜인ConfigurablePopupPreviewCell프로토콜을 정의해PickOrInterestCell과ClosingSoonPopupCell에서 채택했습니다.뷰 컨트롤러에서는
configure메서드에popupData와 로딩한 이미지를 넘겨주고, 각 셀에서 필요한 데이터를 꺼내 구성하도록 변경했습니다.refactor: cellForItemAt 메서드 간소화-8993fc2커밋에서 코드가 엄청 줄었음을 확인하실 수 있습니다!!4. 모델 필드 이름 변경
타입 이름과 필드 이름에 중복이 있었습니다.
프레젠테이션 레이어는 아니지만, 생각난김에 이름을 간소화했습니다!
테스트 방법 및 리뷰 노트
리팩토링 할게 몇 개 더 남았습니다..ㅠ 대규모 공사라 여기저기 많이 건드려서 아직 화면이 정상 동작하지는 않아 테스트는 불가능합니다ㅠㅠ
다음 리팩토링은 아마 캐러셀 뷰 구조 변경일 것 같은데 이번 주 내로 끝내보겠습니다!