---
# System prepended metadata

title: Rx OpenMarket 찰리 PR

---

# Rx OpenMarket 찰리 PR

찰리 안녕하세요. 
이번에 Rx+MVVM 적용하고, CollectionView의 DiffableDataSource 사용해서 오픈마켓 리팩토링을 해봤습니다.
현업에 계시고, 담당하고 있는 다른 리뷰이들도 있어서.. 바쁘신데도..코드리뷰 해주신다고 해서 정말 감사드립니다. 
큰 도움이 될 것 같아요. 🙏
그리고 아무리 디버깅해도 풀리지 않는 부분이 있었어요 🤯

원래는 이 레포로 진행해서 혹시 커밋 히스토리를 확인하시려면 참고해주세요.
기존 작업 레포: [링크](https://github.com/yanghojoon/E-Commerce)

## 프로젝트 개요
기존 오픈마켓 프로젝트를 MVVM 구조와 RxSwift를 적용하여 리팩토링한 프로젝트입니다. 
(추가적으로 DiffableDataSource와 Compositional Layout 또한 적용했습니다)

ProductPage를 서버에 request하여 상품목록을 받아 `ProductListView`를 만들고,
DetailViewProduct를 서버에 request하여 상품 상세정보를 받아 `ProductDetailView`를 구현했습니다.

MVVM-C 패턴으로 Flow Coordinator 타입에서 ViewController 및 ViewModel을 초기화하여 의존성 주입을 관리하고, 화면전환을 담당하도록 했습니다.

MenuSegmentedControl을 구현하여 View 상단에 Custom 메뉴버튼을 만들었습니다.
저번에 여쭤봤던 내용을 토대로 SegmentedControll 보단 Button으로 구현하는 것이 좀 더 커스텀에 용이하다고 판단하여 UIButton을 활용해 커스텀을 했습니다. 

## 조언을 요청드리는 부분
전체적인 코드 구조, 타입 간 역할 분리, Rx+MVVM 적용이 잘 되었는지 궁금합니다.

## 🧐 의문점
1. Rx의 debug() 메서드
Observable을 subscribe하는 부분에서 debug()가 있어야 정상작동하는 문제가 있었습니다.
debug 메서드는 event 흐름을 콘솔에 나타내는 기능을 한다고 파악했는데, debug를 호출하지 않으면 products가 nil로 나와서 데이터 전달이 되지 않는 것으로 확인했습니다.

```swift
    func fetchData<T: Codable>(api: Gettable, decodingType: T.Type) -> Observable<T> {
        return Observable.create { emitter in
            _ = request(api: api)
                .debug()  // FIXME: 없으면 products가 nil이 됨
                .subscribe { event in
                switch event {
                case .next(let data):
                    guard let decodedData = JSONParser<T>().decode(from: data) else {
                        emitter.onError(JSONParserError.decodingFail)
                        return
                    }
                    emitter.onNext(decodedData)
                case .error(let error):
                    emitter.onError(error)
                case .completed:
                    emitter.onCompleted()
                }
                emitter.onCompleted()
            }
            .disposed(by: disposeBag)
            
            return Disposables.create()
        }
    }
```

2. map을 사용했을 때 데이터를 전달하지 못하는 문제
subscribe 대신 map을 사용해서 중간에 스트림을 끊지 않고 유지해주려 했으나, map을 사용하여 바로 viewController로 전달할 경우 내부 코드블록이 실행되지 않고 넘어가는 문제가 있었습니다. 
```swift 
private func configureViewDidLoadObserver(by inputObserver: Observable<Void>,
                                          productsOutput: PublishSubject<([UniqueProduct], [UniqueProduct])>) {
    inputObserver
        .subscribe(onNext: { [weak self] _ in
            guard let self = self else { return }
            _ = self.fetchProducts(at: 1, with: 20).subscribe(onNext: { productPage in  // FIXME: map은 안되고, subscribe은 됨(?)
                let uniqueListProducts = self.makeHashable(from: productPage.products)
                let recentBargainProducts = productPage.products.filter { product in
                    product.discountedPrice != 0
                }
                let bannerProducts = Array(recentBargainProducts[0..<Content.bannerCount])
                let uniqueBannerProducts = self.makeHashable(from: bannerProducts)

                productsOutput.onNext((uniqueListProducts, uniqueBannerProducts))
                guard let firstProductID = productPage.products.first?.id else { return }
                self.latestProductID = firstProductID
            })
        })
        .disposed(by: disposeBag)
}
```



3. Layout을 잡지 못하는 문제
```swift
private let selectorView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 2).isActive = true
    //...
    return view
}()

private func configureUI() {
    //...
    NSLayoutConstraint.activate([
        selectorView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -7),
        selectorView.widthAnchor.constraint(equalTo: gridButton.widthAnchor)
    ])
}

private func changeSelectedUI(sender: MenuSegmentedControlViewModel.MenuButton) {
        switch sender {
        case .grid:
            gridButton.setTitleColor(CustomColor.backgroundColor, for: .normal)
            gridButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 23)
            tableButton.setTitleColor(.systemGray, for: .normal)
            tableButton.titleLabel?.font = UIFont.systemFont(ofSize: 23)

            UIView.animate(withDuration: 0.2) { [weak self] in
                guard let self = self else { return }
                // FIXME: origin과 widthAnchor를 설정했는데, horizontal position을 못잡는 문제
                self.selectorView.frame.origin.x = self.gridButton.frame.origin.x
            }
        case .table:
            tableButton.setTitleColor(CustomColor.backgroundColor, for: .normal)
            tableButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 23)
            gridButton.setTitleColor(.systemGray, for: .normal)
            gridButton.titleLabel?.font = UIFont.systemFont(ofSize: 23)
            
            UIView.animate(withDuration: 0.2) { [weak self] in
                guard let self = self else { return }
                self.selectorView.frame.origin.x = self.tableButton.frame.origin.x
            }
        }
    }
```

현재 origin.x와 widthAnchor를 잡아두었으나 layout error가 발생하는 문제가 있습니다. 
이를 개선해보고자 레이아웃을 잡고 안쓰는 레이아웃은 deactivate를 해주는 방식을 적용해보려 했으나 레이아웃이 deactivate되지 않는 문제가 있었습니다. 

```swift
NSLayoutConstraint.deactivate([
    selectorView.leadingAnchor.constraint(equalTo: tableButton.leadingAnchor),
    selectorView.trailingAnchor.constraint(equalTo: tableButton.trailingAnchor)
])
NSLayoutConstraint.activate([
    selectorView.leadingAnchor.constraint(equalTo: gridButton.leadingAnchor),
    selectorView.trailingAnchor.constraint(equalTo: gridButton.trailingAnchor)
])
```
혹시 레이아웃이 변경되지 않아 발생하는 문제인가 해서 layoutIfNeeded나 layoutSubview 같은 메서드를 호출해보았으나 제대로 deactivate되지 않고 레이아웃이 충돌했는데 혹시 레이아웃을 deactivate시킬 수는 없는 걸까요?