ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MVVM패턴 적용 후기 - 2차 RxSwift를 사용한 신규화면 개발]
    취준(자소서,면접)/프로젝트 정리 2024. 7. 30. 13:48

    요약: 신규화면 개발시 RxSwift를 사용하여 MVVM패턴으로 구현. 기존 Objc의 MVVM한계를 벗어나고자 함.

     

     

    [개발 과정에서 수집한 정보들]

    신청 화면에서

    1. 종목정보, 목표금액, 기간 등… input정보를 Model로 정의 (구조체)
    2.   Subject / Observable로 등록 

     > ViewModel에서 감지 및 처리

     

    • didSet을 통해서 특정 값의 변화를 감지했을 때 해당 값을 바꿔줄 수도 있을 것인데, Observable은 뷰 컨트롤러 단에서 바로 bind 또는 subscribe로 연결할 수 있다는 점에서 RxCocoa와 연동이 간편한 것 같다.

     

    [MVVM의 단점]

    1. iOS의 MVVM패턴에는 표준이 없고 구현하는 사람마다 패턴이 조금씩 다르다.

    그 중에 Kickstarter에서 사용하는 Input과 Output Protocol을 사용하는 방식이 있다.

     

    *** 로그인 화면을 MVVM으로 구현한 예시

    https://duwjdtn11.tistory.com/611  >> 이해하기 용이함

    https://so-kyte.tistory.com/197

     

    2. RxSwift의 단점 - 디버깅

    운영업무는 이슈해결이 위주라, 디버깅 해서 원인을 역추적하는 경우가 많음

     

      1) 콜 스택 추적 어려움

    • RxSwift에서 데이터 스트림을 통해 이벤트가 전달될 때, 콜 스택이 전통적인 방식과 다르게 동작합니다. 이는 콜 스택을 통해 문제의 원인을 추적하는 것을 어렵게 만듭니다.
    • 오류가 발생했을 때, 이벤트 체인의 어느 부분에서 문제가 발생했는지 파악하기 어려울 수 있습니다.

      2) 비동기성

    • 비동기 작업이 많아지면 코드의 흐름을 이해하기 어려워질 수 있습니다. 특히, 여러 비동기 스트림이 동시에 동작할 때 문제를 추적하는 것이 복잡해질 수 있습니다.
    • 비동기 이벤트가 발생하는 순서를 파악하는 것이 어려워질 수 있습니다.

     

    >> 해결책 (디버그 연산자 사용, 라이브러리활용, 디버깅 전용 구독자 등)

     

     

    [기타정보]

    1. xib로 생성한 Custom View를 호출하는 방법 

    https://medium.com/a-day-of-a-programmer/xib%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-uiview-custom-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-348a9b789496

     

    2. as? is?  다운캐스팅/업캐스팅

    UIView를 as로 다운캐스팅 하여  직접 접근이 가능하다?

     

    3. TR 호출 시 @escaping 클로저의 의미
    https://jusung.github.io/Escaping-Closure/

    4. lazy var의 의미

    What is lazy var?

    • Lazy initialization is a concept where initialization (construction) of variable content is delayed until its first usage. First access to such variable triggers initialization. Since content is not created until variable is used (needed) using lazy initialized variables can save resources.
    • That is primary drive behind lazy initialization. You don't create something until you need it. That is also logic you will use when deciding whether something should be lazy var or not.
    • If you are dealing with views (or anything else) that are always visible (needed) there is little point in using lazy initialization. On the other hand when you are dealing with instances that are not always needed - then using lazy var is justified.
    • If your view is always visible in presented view controller, you will not accomplish much by making it lazy. If it is visible only under specific circumstances - for instance when user expands some collapsed panel - then making it lazy makes sense. It will make your view controller load faster and use less memory by default
      https://baked-corn.tistory.com/45

    5. 꼭 Observable로 방출해야하는 이유

       // 입력받는즉시 판단을 하는게 아니기때문에 굳이 observable을 쓸 필요가 없다...

        // + bind를 써서 바로 UI에 뿌릴게 아니기때문에...

        // >> 완전 잘못된 생각..

        //  due to the asynchronous nature of RxSwift, the return value from within a subscription cannot be directly passed out of the subscription.

        // >> 따라서 completion으로 전달하거나, Observable을 써야만한다

     

     

     

     

    [생각해볼점]

    1. BehaviorRelay를 썼는가?
    2. Rx의 함수들 (map, combineLates 등 외에도 더 다양한 함수를 활용해 볼 필요가 있음)
    3. RxDataSource는 적용 실패함.. section마다, row마다 데이터구조가 다른데 이걸 표현하는데 실패했다.
       >> SectionModel을 case별로 나누어서 고안하면 될거같긴 한데 시간관계상 패스…
    CollectionViewCell1 CollectionViewCell2
    CollectionViewCell3 CollectionViewCell4

    >> 이런 구조였다


    4. TR 호출 부분을 Service 분리할

     - Service로 분리하는 법

    더보기

    네, ViewModel에서 API를 호출하는 부분을 별도의 Service로 분리하는 것은 좋은 설계 방법입니다. 이렇게 하면 코드의 모듈성을 높이고, 테스트와 유지보수를 용이하게 할 수 있습니다. 

     

    ### 장점

     

    1. **모듈화**: API 호출 로직을 별도의 서비스 클래스로 분리하면, ViewModel은 UI 상태 관리에 집중할 수 있습니다.

    2. **재사용성**: 여러 ViewModel에서 같은 API 호출 로직을 재사용할 수 있습니다.

    3. **유지보수**: API 호출 로직이 한 곳에 집중되어 있어 변경 사항을 쉽게 관리할 수 있습니다.

    4. **테스트 용이성**: 서비스 클래스를 Mocking하여 단위 테스트를 작성하기 쉽습니다.

     

    ### 예제

     

    아래는 Swift와 RxSwift를 사용하여 API 호출을 Service로 분리하고 ViewModel에서 이를 사용하는 방법을 예시로 보여줍니다.

     

    #### 1. API Service 정의

     

    먼저, API 호출을 담당할 Service 클래스를 정의합니다.

     

    ```swift

    import RxSwift

     

    class APIService {

        // Singleton instance for the service

        static let shared = APIService()

        

        // Private init to enforce singleton pattern

        private init() {}

        

        // Method to fetch data from API

        func fetchData() -> Observable<[String]> {

            // Replace with your actual API call logic

            return Observable.create { observer in

                // Simulate a network call

                DispatchQueue.global().asyncAfter(deadline: .now() + 1) {

                    let data = ["Item 1", "Item 2", "Item 3"] // Replace with actual API response

                    observer.onNext(data)

                    observer.onCompleted()

                }

                return Disposables.create()

            }

        }

    }

    ```

     

    #### 2. ViewModel 정의

     

    ViewModel에서 Service 클래스를 사용하여 데이터를 가져옵니다.

     

    ```swift

    import RxSwift

    import RxCocoa

     

    class MyViewModel {

        // Dependency Injection of the APIService

        private let apiService: APIService

        private let disposeBag = DisposeBag()

        

        // Relay to hold the data

        let dataRelay = BehaviorRelay<[String]>(value: [])

        

        // Observable to expose the data

        var dataObservable: Observable<[String]> {

            return dataRelay.asObservable()

        }

        

        // Initializer with dependency injection

        init(apiService: APIService = .shared) {

            self.apiService = apiService

        }

        

        // Method to fetch data from the service

        func fetchData() {

            apiService.fetchData()

                .subscribe(onNext: { [weak self] data in

                    self?.dataRelay.accept(data)

                }, onError: { error in

                    print("Error fetching data: \(error)")

                })

                .disposed(by: disposeBag)

        }

    }

    ```

     

    #### 3. ViewController에서 ViewModel 사용

     

    ViewController에서 ViewModel을 사용하여 데이터를 바인딩합니다.

     

    ```swift

    import UIKit

    import RxSwift

    import RxCocoa

     

    class MyViewController: UIViewController {

        

        private let disposeBag = DisposeBag()

        private let viewModel = MyViewModel()

        

        // UI Elements

        @IBOutlet weak var tableView: UITableView!

        

        override func viewDidLoad() {

            super.viewDidLoad()

            

            // Fetch data

            viewModel.fetchData()

            

            // Bind the data to the table view

            viewModel.dataObservable

                .bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, item, cell in

                    cell.textLabel?.text = item

                }

                .disposed(by: disposeBag)

        }

    }

    ```

     

    ### 요약

     

    - **API Service 정의**: API 호출 로직을 별도의 Service 클래스로 분리합니다.

    - **ViewModel 정의**: Service 클래스를 ViewModel에 주입하고, 데이터를 가져와서 `BehaviorRelay`에 저장합니다.

    - **ViewController에서 ViewModel 사용**: ViewController에서 ViewModel을 사용하여 데이터를 바인딩하고 UI를 업데이트합니다.

     

    이와 같은 구조는 코드의 가독성을 높이고, 유지보수와 테스트를 용이하게 합니다.

     

     

     

    댓글

Designed by Tistory.