MVVM 아키텍처 도입, 나의 ViewModel 사용법 (= Input-Output 패턴)

2024. 11. 30. 22:31·📱 iOS/Swift

문제 상황

MVC 패턴의 코드 길어짐과 수많은 의존성을 컨트롤러가 갖는 문제,

또한 View와 비즈니스 로직 분리 등을 위해 MVVM 도입했다.

우리 팀에서 MVVM의 ViewModel을 어떻게 사용하기로 정의했는지 설명하겠다.

 


문제 해결

MVVM 도입 결정

  • MVC에선 Controller가 View와 Model 일을,
    MVP에선 Presenter와 View가 서로 일 주고받음
    MVVM에서 ViewModel은 Model하고만 소통함
    즉, 관심사 분리를 잘 해낼 수 있음
  • 위 특징 때문에 테스트 가능한 구조가 되어 테스팅도 가능
  • MVC 패턴의 고질적인 컨트롤러에 많은 의존성이 쌓이는 문제를 덜어낼 수 있음

 

MVVM을 위한 Input-Output 패턴 도입

우리팀은 Combine을 사용하여 프로젝트를 진행하고 있다.

그리고 View와 ViewModel에 대한 단방향 데이터 플로우를 위해 Input-Output 패턴으로 적용하여 양방향 스트림을 진행하려 한다.

View가 갖고 있는 Subject로 Input을 넣으면,
ViewModel은 View의 스트림을 구독을 하고 있다가, 데이터를 가공한 후에 자신의 Output 스트림으로 전달한다.

그러면 View가 구독중인 ViewModel의 output 스트림에 의해 화면이 다시 그려지게 되는 것이다.

 

ViewModelType 프로토콜

이를 위해 다음과 같은 프로토콜을 만들어주었다.

protocol ViewModelType {
    associatedtype Input
    associatedtype Output

    func transform(input: Input) -> Output
}

 

ViewModel 클래스

위 프로토콜은 모든 뷰모델이 채택하여 다음과 같이 사용된다.

public final class RegisterViewModel: ViewModelType {
    enum Input {
        case registerTextFieldEdited(text: String?)
        case registerButtonTapped(text: String)
    }

    enum Output {
        case registerButtonEnabled(isEnabled: Bool)
        case moveToHome(destination: String)
    }

    private let output = PassthroughSubject<Output, Never>()
    private var cancellables = Set<AnyCancellable>()

    public init() { }

    func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
        input.sink { [weak self] event in
            switch event {
            case .registerTextFieldEdited(let text):
                self?.validateTextField(text: text)
            case .registerButtonTapped(let text):
                self?.registerButtonTapped(text: text)
            }
        }.store(in: &cancellables)

        return output.eraseToAnyPublisher()
    }
    ...
}

 

View 클래스

이렇게 함으로써 뷰모델은 뷰로부터 오는 input 스트림을 구독하고, 자신의 output 스트림을 리턴해준다.

그러면 리턴 값을 아래 뷰가 다음과 같이 사용한다.

public final class RegisterViewController: UIViewController {
    // MARK: - Property
    private var viewModel = RegisterViewModel()
    private let input = PassthroughSubject<RegisterViewModel.Input, Never>()
    private var cancellables = Set<AnyCancellable>()
    ...
    private func bind() {
        let output = viewModel.transform(input: input.eraseToAnyPublisher())

        output.sink { [weak self] event in
            switch event {
            case .registerButtonEnabled(let isEnabled):
                self?.registerButton.isEnabled = isEnabled
            case .moveToHome(let houseName):
                do {
                    let homeViewModelFactory = try DIContainer.shared.resolve(HomeViewModelFactory.self)
                    let homeViewModel = homeViewModelFactory.make()
                    let homeViewController = HomeViewController(viewModel: homeViewModel)
                    self?.navigationController?.pushViewController(homeViewController, animated: false)
                    self?.navigationController?.viewControllers.removeFirst()
                } catch {
                    MHLogger.error(error.localizedDescription)
                }
            }
        }.store(in: &cancellables)
    }
}

그림으로 설명하면 아래와 같은 구조가 된다.


배운 점

  • MVVM 패턴을 적용하여 관심사 분리를 했다.
  • ViewModel에 Input-Output 패턴을 적용하여 플로우를 만들었다.

 


참조 링크

https://medium.com/myrealtrip-product/%EB%A7%88%EC%9D%B4%EB%A6%AC%EC%96%BC%ED%8A%B8%EB%A6%BD%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-ios-%EA%B0%9C%EB%B0%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-51048dca4626

 

마이리얼트립에서 사용하는 iOS 개발 아키텍처

MVVM을 사용하면서 데이터의 관리와 디자인요소를 어떻게 처리하는가

medium.com

 

https://medium.com/daily-monster/uikit-mvvm-with-combine-%EC%A0%81%EC%9A%A9%EA%B8%B0-ft-error-handling-a5f59389f8b7

 

UIKit + MVVM with Combine 적용기 (ft. Error Handling)

Hi I’m Wednesday Monster from Daily Monsters 🙌🫡

medium.com

 

https://youtu.be/KK6ryBmTKHg

 

https://medium.com/myrealtrip-product/%EB%8B%A8%EB%B0%A9%ED%96%A5-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%94%8C%EB%A1%9C%EC%9A%B0-unidirectial-data-flow-udf-ios-%EC%95%B1-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%A1%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-196a6c4f3b66

 

단방향 데이터 플로우(Unidirectial Data Flow, UDF) iOS 앱 아키텍처로 복잡한 상태 관리하기

시작하며

medium.com

 

저작자표시 (새창열림)

'📱 iOS > Swift' 카테고리의 다른 글

Unit Test 할 때 @testable의 역할이 뭘까 ?  (0) 2024.11.29
Swift의 전역 변수 초기화 시점  (0) 2024.11.23
디버깅할 때, print가 아닌 OSLog 사용을 위한 Logger 구현  (0) 2024.11.16
[iOS] SwiftLint 적용 & 겪은 에러 해결과정 기록  (2) 2024.10.19
[Swift] struct가 아닌, enum으로 네임스페이스 관리하기  (3) 2024.09.06
'📱 iOS/Swift' 카테고리의 다른 글
  • Unit Test 할 때 @testable의 역할이 뭘까 ?
  • Swift의 전역 변수 초기화 시점
  • 디버깅할 때, print가 아닌 OSLog 사용을 위한 Logger 구현
  • [iOS] SwiftLint 적용 & 겪은 에러 해결과정 기록
kyxxn
kyxxn
컴퓨터공학을 좋아하는 대학생의 공부 일기
  • kyxxn
    컴공 학부생의 공부 일기
    kyxxn
  • 전체
    오늘
    어제
    • 분류 전체보기 (156)
      • 📱 iOS (64)
        • Xcode (10)
        • Swift (17)
        • Swift Concurrency (12)
        • UIKit (21)
        • SwiftUI (0)
      • 🖥️ Computer Science (57)
        • 🏛️ Software Architecture Pa.. (2)
        • 👨🏻‍🎨 Design Pattern (3)
        • Data Structure (4)
        • Algorithm (10)
        • Computer Architecture (4)
        • Operating System (19)
        • Network (15)
      • ✍🏻 회고록 (9)
      • 🎸 기타 (25)
        • 해커톤 (1)
        • git (6)
        • 세미나 (1)
        • 책을 읽고 (1)
        • AOS, Kotlin (6)
        • Reinforcement Learning (9)
  • 블로그 메뉴

    • 링크

      • 깃허브
      • 일상 블로그
    • 공지사항

    • 인기 글

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    kyxxn
    MVVM 아키텍처 도입, 나의 ViewModel 사용법 (= Input-Output 패턴)
    상단으로

    티스토리툴바