티스토리 뷰

분리를 결심한 계기


현재 프로젝트에서 ViewModel은 Container View(페이지 단위)와 1:1로 사용하고 있었습니다. 그런데 한 컨테이너뷰에 기능이 많아질 때, 뷰는 컴포넌트를 분리해가며 크기를 유지시킬 수 있었는데, 뷰모델은 계속해서 커지는 문제가 발생했습니다. 현재 Combine을 이용해서 작업하고 있는데 뷰모델이 관리하는 상태가 너무 많아져 어떤 변수가 어디에 쓰이는지 구분하기가 헷갈려졌고, 바인드 코드 또한 관리하기가 어려워졌습니다. 이에 여러 방법을 생각해보다 뷰모델을 분리하기로 결심했습니다.

 

문제의 뷰모델


import Foundation
import Combine

class SignupViewModel: ObservableObject {
    // MARK: - Dependency
    let validationServcie: ValidationService
    let accountService: AccountServiceType
    
    // MARK: - Input State
    @Published var name: String = ""
    @Published var email: String = ""
    @Published var gender: Bool = false
    @Published var birth: String = ""
    @Published var password: String = ""
    @Published var passwordAgain: String = ""
    
    @Subject var tapEmailVerificationButton: Void = ()
    @Subject var tapSignupButton: Void = ()
    
    // MARK: - Ouput State
    /// 이름 형식 검증에 대한 에러 메세지입니다.
    @Published var nameErrorMessage: String = ""
    /// 이메일 형식 검증에 대한 에러 메세지입니다.
    @Published var emailErrorMessage: String = ""
    /// 이메일 인증에 대한 에러 메세지입니다.
    @Published var emailVerificationErrorMessage: String = ""
    // 생년월일 형식에 대한 에러 메세지입니다.
    @Published var birthErrorMessage: String = ""
    /// 비밀번호 형식에 대한 에러 메세지입니다.
    @Published var passwordErrorMessage: String = ""
    /// 비밀번호 확인에 대한 에러 메세지 입니다.
    @Published var passwordAgainErrorMessage: String = ""
    /// 회원가입 성공 여부에 대한 에러 메세지입니다.
    @Published var signupErrorMessage: String = ""
    /// 이메일 인증하기 버튼 비활성화 여부입니다.
    @Published var disabledEmailVerificationField: Bool = false
    /// 이메일 인증을 시도했는지 여부입니다.
    @Published var sendedEmailVerification: Bool = false
    /// 이메일 인증 성공여부입니다.
    @Published var successEmailVerification: Bool = true
    /// 이메일 인증 실패 메세지입니다.
    @Published var emailVerificationErrorMessage: Bool = false
    /// 회원가입 버튼 비활성화 여부입니다.
    @Published var disableSignupButton: Bool = true
    
    // MARK: - Cancellable Bag
    private var cancellables = Set<AnyCancellable>()
    
    // MARK: - Constructor
    init(validationServcie: ValidationService,
        accountService: AccountServiceType){
        self.accountService = accountService
        self.validationServcie = validationServcie
        self.bind()
    }
    
    // MARK: - Method
    private func bind(){
        
        /// 이름 형식을 검증합니다. 빈 값일 땐 검증하지 않습니다.
        $name.map {
                self.validationServcie.isValidNameFormat($0)
                || $0.isEmpty
            }.map {
                $0 ? "" : ValidationErrorMessage.invalidName.description
            }.receive(on: RunLoop.main)
            .assign(to: \.nameErrorMessage, on: self)
            .store(in: &cancellables)
        
        /// 이메일 형식을 검증합니다. 빈 값일 땐 검증하지 않습니다.
        $email.map {
                self.validationServcie.isValidEmailFormat($0)
                || $0.isEmpty
            }.map {
                $0 ? "" : ValidationErrorMessage.invalidEmail.description
            }.receive(on: RunLoop.main)
            .assign(to: \.emailErrorMessage, on: self)
            .store(in: &cancellables)
        
        /// 생년월일 형식을 검증합니다. 빈 값일 땐 검증하지 않습니다.
        $birth.map {
                self.validationServcie.isValidBirthFormat($0)
                || $0.isEmpty
            }.map {
                $0 ? "" : ValidationErrorMessage.invalidBirth.description
            }.receive(on: RunLoop.main)
            .assign(to: \.birthErrorMessage, on: self)
            .store(in: &cancellables)
        
        /// 비밀번호 형식을 검증합니다. 빈 값일 땐 검증하지 않습니다.
        $password.map {
                self.validationServcie.isValidPasswordFormat($0)
                || $0.isEmpty
            }.map {
                $0 ? "" : ValidationErrorMessage.invalidPassword.description
            }.receive(on: RunLoop.main)
            .assign(to: \.passwordErrorMessage, on: self)
            .store(in: &cancellables)
        
        /// 비밀번호와 다시쓴 비밀번호가 일치하는지 검사합니다. 빈 값일 땐 검증하지 않습니다.
        $passwordAgain.map {
                $0.isEmpty
            }.map {
                $0 ? "" : ValidationErrorMessage.invalidPasswordAgain.description
            }.receive(on: RunLoop.main)
            .assign(to: \.passwordAgainErrorMessage, on: self)
            .store(in: &cancellables)
        
        /// 이메일 인증하기 버튼을 누르면 이메일 인증 버튼의 문구를 변경할 수 있도록 sendedEmailCertification을 변경합니다.
        $tapEmailVerificationButton.map {
            true
        }
        .assign(to: \.sendedEmailVerification, on: self)
        .store(in: &cancellables)
        
        // signup button enable/disable
        $name.combineLatest($email, $password, $passwordAgain) {
            name, email, password, passwordAgain in
            return self.validationServcie.isValidNameFormat(name) ||
            self.validationServcie.isValidEmailFormat(email) ||
            self.validationServcie.isValidPasswordFormat(password) ||
            passwordAgain == password
        }.receive(on: RunLoop.main)
            .assign(to: \.disableSignupButton, on: self)
            .store(in: &cancellables)
        
        
        /// signupButton을 탭하면 signup serivce를 통해 회원가입 요청을 보냅니다.
        $tapSignupButton.sink { [weak self] in
            self?.signup(SignupModel(...))
        }
        .store(in: &cancellables)
        
    }
    
    /// AccountService를 통해 signup api를 실행시키고 결과값을 signupResult로 send함
    func signup(_ signupModel: SignupModel){
		// ... 생략
    }
}

문제의 뷰모델인데요, 회원가입 뷰에 사용되는 뷰모델입니다. 아직 기능을 다 작성한 것이 아닌데도 180줄 정도 되고 Combine(or Rx) 특성상 코드가 많아질 수록 어디가 어떻게 연결되는지 파악하기가 어려워졌습니다. 

 

어떻게 분리할까?


일단 뷰모델의 역할을 정리해보자

분리하고자 하는 Signup 뷰모델은 다음과 같은 역할을 맡고 있습니다.

1. 사용자의 Input값 저장(name, email, password, ...)
2. Input값의 유효성 검사(이메일 형식, 비밀번호 형식 등)
3. 유효성 검사에 따른 에러메세지 표시(Output state)
4. 이메일 인증 과정(인증번호 요청 - 서버)
5. 회원가입 요청(서버)

 

분리 기준

위 역할을 바탕으로 어떤 기준으로 나눌것인지 생각해보았습니다.

1. Input State와 Output State를 기준으로

단순히 Input 값과 Output 값을 기준으로 나누는 방법입니다. Input은 사용자가 입력하는 Text값 혹은 버튼 탭, onAppear와 같의 사용자가 발생시키는 이벤트입니다. Output은 Input의 결과로써 나타나는 값들입니다.

장점
구분 기준이 명확합니다. 따라서 프로젝트 전체(다른 뷰모델들)에 적용하기 쉽다.
단점
두 뷰모델간의 결합도가 매우 높아지게 된다. (모든 InputVM 상태를 OutputVM에 바인드 시켜줘야함)

 

2. Presentation State와 Business State를 기준으로

예를 들면 isButtonEnable과 같은 상태은 비즈니스 로직과는 관련없이 결과로써 나타나는 '뷰'에만 필요한 상태입니다. 이를 기준으로 나누는 것입니다. 따라서 Service 객체에 직접적으로 사용되는지, 아닌지에 따라 나눌 수 있습니다.

장점
Service의 의존하는, 즉 비즈니스 로직을 호출에 필요한 상태와 아닌 것을 확실히 구분할 수 있다.
1번과 마찬가지로 프로젝트 전체에 사용하는 기준이 될 수 있다.
1번과 달리 필요한 상태만 Bind하면되므로 결합도는 다소 떨어진다.
단점
해당 뷰모델의 특성상 BusinessVM이 너무 커지게 되므로 분리의 의미가 없어진다.

 

3. 기능 중심으로 분리

현재 분리하고자하는 뷰모델의 역할을 보았을 때 크게 1. 사용자가 입력하는 값을 검증 2. 서버 API 호출이 필요한 로직 으로 나누어볼 수 있습니다. 따라서 기능을 중심으로 두가지로 분류해볼 수 있습니다.

장점
비즈니스 도메인(Service)에 따라 분리되므로 테스트가 용이해진다. (ValidationVM 테스트는 ValidationServcie Mock만 주입하면됨)
2번과 마찬가지로 필요한 상태만 Bind하면 되므로 둘 간의 결합도는 1번에 비해 적다.
단점
구분 기준이 기능에 따라 다르므로 프로젝트 전체에 일관되게 적용할 수 없다. 어떤 뷰모델은 4개로, 어떤 뷰모델은 3개로 나뉘는 등 다 달라지게 되므로 전체적인 프로젝트를 복잡해질 수 있다.

 

결정

저는 3번 방식으로 뷰모델을 분리하기로 했습니다. 그 이유는 첫번째로 테스트 용이성을 크게 보았고, 현재 프로젝트 기획상 한 뷰에 기능이 그렇게 많지는 않기 때문에 뷰모델이 3개 이상으로 쪼개져 프로젝트가 혼잡해질 가능성이 적다고 보았기 때문입니다.

 

1번 방식 같은 경우는 프로젝트 전반으로 뷰모델을 분리하면서도 아키텍처를 일관되게 유지하고 싶은 경우에, 2번 방식 같은 경우는 뷰에서 표현해야하는 작업이 매우 많은 경우에 유용할 것 같습니다. (fade in/out 작업이나 pagenation등..)

 

분리한 뷰모델간의 소통


그동안 한 뷰모델로 작업해왔던 이유는 각 상태들이 연관성이 높기 때문입니다. 따라서 뷰모델을 분리한다고해도 완전 독립적으로 두 뷰모델을 분리할 수는 없습니다. 결국 어느정도는 소통이 필요합니다. 예를 들면 회원가입 요청을 위해서는 결국 유효성 검사 ViewModel에서 관리하고 있는 사용자 Input값이 필요한 경우입니다. 이를 위해 저는 delegate 패턴, 즉 프로토콜을 이용하기로 했습니다.

각각 VM의 상태를 정의한프로토콜을 이용해서 두 뷰모델이 서로를 알고있도록 해줄겁니다. RIBs에서도 이런식으로 Presenter와 Interactor를 연결하죠.

 

적용하기


1. 프로토콜 정의

우선 각각 필요한 프로토콜을 정의해주었습니다. 각 뷰모델이 관리할 상태가 정의되게 됩니다. 만약 Combine을 사용하지 않는다면 사용하는 메서드들도 정의해주어야합니다. 하지만 현재 Combine을 이용해서 이벤트들을 처리하고 있기 때문에 Published와 Subject만 정의해주면 됩니다.

protocol SignupValidationViewModelType {
    // Input State
    var name: String { get }
    var email: String { get }
    var gender: Bool { get }
    var birth: String { get }
    var password: String { get }
    var passwordAgain: String { get }
    
    // Output state
    /// 이름 형식 검증에 대한 에러 메세지입니다.
    var nameErrorMessage: String { get }
    /// 이메일 형식 검증에 대한 에러 메세지입니다.
    var emailErrorMessage: String { get }
    // 생년월일 형식에 대한 에러 메세지입니다.
    var birthErrorMessage: String { get }
    /// 비밀번호 형식에 대한 에러 메세지입니다.
    var passwordErrorMessage: String { get }
    /// 비밀번호 확인에 대한 에러 메세지 입니다.
    var passwordAgainErrorMessage: String { get }
    /// 회원가입 성공 여부에 대한 에러 메세지입니다.
    var signupErrorMessage: String { get }
    /// 회원가입 버튼 비활성화 여부입니다.
    var disableSignupButton: Bool { get }
}

사실 ErrorMessage 상태는 Reqeust뷰모델에서는 필요하지 않은데요, 그래도 주석도 정리할겸, 통일성을 위해 적어주었습니다.

protocol SignupRequestViewModelType {
    // Input state
    /// 이메일 인증 버튼 탭 이벤트입니다
    var tapEmailCertificationButton: Void { get }
    /// 회원가입 버튼 탭 이벤트입니다
    var tapSignupButton: Void { get }
    
    // Output state
    /// 회원가입 성공 여부에 대한 에러 메세지입니다.
    var signupErrorMessage: String { get }
    /// 이메일 인증하기 버튼 비활성화 여부입니다.
    var disabledEmailVerificationField: Bool { get }
    /// 이메일 인증을 시도했는지 여부입니다.
    var sendedEmailVerification: Bool { get }
    /// 이메일 인증 성공여부입니다.
    var successEmailVerification: Bool { get }
    /// 이메일 인증 실패 메세지입니다.
    var emailVerificationErrorMessage: Bool { get }
    /// 회원가입 버튼 비활성화 여부입니다.
    var disableSignupButton: Bool { get }
}

 

2. ViewModel 코드 분리

2-1. 각 뷰모델의 옵셔널 변수 정의

class SignupRequestViewModel: ObservableObject, SignupRequestViewModelType {
    // MARK: - Dependency
    let accountService: AccountServiceType
    var validationViewModel: SignupValidationViewModelType? = nil
// ...
class SignupValidationViewModel: ObservableObject, SignupValidationViewModelType {
    // MARK: - Dependency
    let validationServcie: ValidationService
    var requestViewModel: SignupRequestViewModelType? = nil
// ...

뷰모델 클래스를 만들어주고 프로토콜을 채택합니다. 그리고 의존성이 생겨야하는 다른 뷰모델에 대한 옵셔널 변수를 정의해줍니다. (사실 기능상 RequestViewModel만 ValidationViewModel을 알면되지만 양방향으로 알아야 하는 경우가 있을 수도 있어 해주었습니다)

 

2-2. bind 코드 분리

각 뷰모델의 역할에 맞추어 복잡했던 bind코드를 분리해줍니다. 사실 이게 궁극적인 목표었습니다.

// SignupValidationViewModel.swift
class SignupValidationViewModel: ObservableObject, SignupValidationViewModelType {
	// ...
    private func bind(){
        /// 이름 형식을 검증합니다. 빈 값일 땐 검증하지 않습니다.
        $name.map {
            self.validationServcie.isValidNameFormat($0)
            || $0.isEmpty
        }.map {
            $0 ? "" : ValidationErrorMessage.invalidName.description
        }.receive(on: RunLoop.main)
            .assign(to: \.nameErrorMessage, on: self)
            .store(in: &cancellables)
        
		/// ... 생략
        
        /// 비밀번호와 다시쓴 비밀번호가 일치하는지 검사합니다. 빈 값일 땐 검증하지 않습니다.
        $passwordAgain.map {
            $0.isEmpty
        }.map {
            $0 ? "" : ValidationErrorMessage.invalidPasswordAgain.description
        }.receive(on: RunLoop.main)
            .assign(to: \.passwordAgainErrorMessage, on: self)
            .store(in: &cancellables)
    }

ValidationViewModel에는 입력 유효성 검사에 대한 상태 바인딩만 코드만 넣어줍니다. 반복적인 코드라 일부 생략했습니다.

 

// SignupRequestViewModel.swift
class SignupRequestViewModel: ObservableObject, SignupRequestViewModelType {
    // ...
    private func bind() {
        
        /// 이메일 인증하기 버튼을 누르면 이메일 인증 버튼의 문구를 변경할 수 있도록 sendedEmailCertification을 변경합니다.
        $tapEmailVerificationButton.map { true }
        .assign(to: \.sendedEmailVerification, on: self)
        .store(in: &cancellables)
        
        /// signupButton을 탭하면 signup serivce를 통해 회원가입 요청을 보냅니다.
        $tapSignupButton.sink { [weak self] in
            self?.signup(SignupModel(email: self?.validationViewModel?.email ?? "",
                                     pw: self?.validationViewModel?.password ?? "",
                                     birthday: self?.validationViewModel?.birth ?? "",
                                     gender: self?.validationViewModel?.gender ?? true,
                                     name: self?.validationViewModel?.name ?? ""))
        }
        .store(in: &cancellables)
        
        // ... 생략
    }

분리 목적대로 이메일 인증과 회원가입 요청에 관련된 상태들을 바인딩해줍니다. 다른 뷰모델의 참조가 필요한 경우는 아까 만들어둔 참조 변수를 이용해 접근합니다.

 

3. View에서 의존성 설정 (중요)

struct SignupView: View {
    
    @ObservedObject var validationViewModel: SignupValidationViewModel
    @ObservedObject var requestViewModel: SignupRequestViewModel
    
    init() {
        // ViewModel DI
        self.validationViewModel = SignupValidationViewModel(
            validationServcie: ValidationService())
        self.requestViewModel = SignupRequestViewModel(
            accountService: AccountService())
        // 두 뷰모델간 의존성 연결
        self.validationViewModel.requestViewModel = self.requestViewModel
        self.requestViewModel.validationViewModel = self.validationViewModel
    }
    
// ...

이렇게 ViewModel을 인스턴스화 해주는 부분에서 두 뷰모델의 참조를 연결해줍니다. 이 과정이 없어도 컴파일 에러가 나지 않기 때문에 조금 위험하긴 하네요...

 

4. View에 바인딩

이제 각각 필요에 맞게 상태를 바인딩해주면 됩니다.

// validationViewModel 예시
TextInput(title: "이름",
          placeholder: "이름을 입력해주세요",
          text: $validationViewModel.name,
          errorMessage: $validationViewModel.nameErrorMessage)
TextInput(title: "이메일",
          placeholder: "예) junjongsul@gmail.com",
          text: $validationViewModel.email,
          errorMessage: $validationViewModel.emailErrorMessage)
              
// reuqestViewModel 예시
ThinLightButton(title: requestViewModel.sendedEmailVerification ? 
				"이메일 인증 다시 보내기" : "이메일 인증하기", 
                onTap: requestViewModel.$tapEmailVerificationButton, 
                disabled: $requestViewModel.disabledEmailVerificationField)

 

장단점 정리


위와 같이 뷰모델 분리 후에 느낀 장단점을 정리해보겠습니다.

✅ 장점
- 상태와 바인딩 로직을 적절히 분리할 수 있어 코드 관리가 쉬워졌다
- 뷰모델 테스트가 용이해졌다 : 테스트 타겟 로직에 따라 주입해야하는 MockService가 명확해졌고, 테스트할 변수(상태) 타겟도 명확해졌다.
❌ 단점
- 두 뷰모델간의 의존성을 관리하기가 어렵다 : 뷰모델을 생성할때 두 뷰모델의 참조를 잘 전달해주어야한다. 실수가 발생해도 컴파일 에러가 나지 않아 신중해야한다.
- 뷰에 상태를 바인딩할때 어떤 상태가 어떤 뷰모델에 있는지 예측하기 어렵다 : 해당 뷰모델에 없는 상태를 참조하려하면 컴파일 에러가 나므로 실수할 일은 없지만 코드 가독성이 떨어지고 계속 ViewModel을 참고하며 뷰 코드를 작성해야한다. 이는 뷰 로직의 복잡성을 올린다.
- 전체적인 프로젝트 구조가 복잡해진다 : 기존처럼 컨테이너뷰와 뷰모델이 1:1관계일때보다 프로젝트의 복잡성이 올라간다. 즉, 어떤 로직이 어떤 뷰모델에 있을지 예측하는 것이 비교적 어려워졌다.

이 외에 ViewModel의 단위테스트를 위해서 다른 뷰모델의 Mock까지 만들어야하는 귀찮음도 늘었지만, 이는 단점이라기보단 당연히 해야할일인 것 같습니다.

 

++ 업데이트


작성하다보니 프로토콜로 연결하는 방식이 생각보다 책임분리가 잘 되지 않아서 방법을 바꾸었습니다.

예를 들면

// SignupValidationViewModel.swift
if isNameValid || isEmailValid || isBirthValid || isPasswordValid || isPasswordAgainValid {
    // 형식 검사 모두 통과 시 이메일 인증 확인 후 회원가입 버튼 활성화
    if self.requestViewModel?.emailVerificationState.success {
        self.requestViewModel?.signupState.enable = true
    }
}

ValidationViewModel에서 형식검사를 마친 후에 이메일 인증이 되었는지 확인하고 SignupButton을 활성화 시켜주어야 하는 로직인데 Signup에 관련된 로직은 RequestViewModel로 위임하기로 했지만 위와 같은 경우 일부 ValidationViewModel에 작성할 수밖에 없었습니다.

 

이에 Combine의 Subject를 이용해 두 뷰모델간의 소통방식을 바꾸기로 했습니다.

class SignupValidationViewModel {
	var eventToRequestViewModel = PassthroughSubject<SignupValidationViewModelEvent, Never>()
	var eventFromRequestViewModel: PassthroughSubject<SignupRequestViewModelEvent, Never>? = nil
// ...
class SignupRequestViewModel: ObservableObject, SignupRequestViewModelType {
    var eventToValidationViewModel = PassthroughSubject<SignupRequestViewModelEvent, Never>()
    var eventFromValidationViewModel: PassthroughSubject<SignupValidationViewModelEvent, Never> = nil
// ...

이런식으로 각 뷰모델로 보낼/받을 Subject를 선언해주고

 

enum SignupValidationViewModelEvent {
    case allInputValid
}

enum SignupRequestViewModelEvent {
    case signup
}

이렇게 이벤트 타입을 정의해주었습니다! 정보를 전달해야한다면 case event(data: String) 이런식으로 정의해주면 되겠죠?

 

이제 만약 ValidationViewModel에서 모든 값의 형식을 확인했다면

// SignupValidationViewModel.swift
if isNameValid || isEmailValid || isBirthValid || isPasswordValid || isPasswordAgainValid {
    // 모든 입력값이 형식검사를 통과했음을 알리기
    self.eventToRequestViewModel.send(.allInputValid)
}

이렇게 완료되었음을 알리기만 하면됩니다! RequestViewModel의 내부 상태값을 직접 조정하지 않아도 돼요 👍

 

// SignupReqeustViewModel.swift
eventFromValidationViewModel?.sink { (event: ValidationViewModelEvent) in
    switch event {
    case .allInputValid:
        self.signupState.enable = self.emailVerificationState.sucess
    }
}

그럼 RequestViewModel에서는 이 이벤트 Subject를 구독하고 있다가 event에 따라서 내부 값을 이용해 처리해주면 됩니다.

 

// SignupView.swfit
init() {
    // ViewModel DI
    self.validationViewModel = SignupValidationViewModel(
        validationServcie: ValidationService())
    self.requestViewModel = SignupRequestViewModel(
        accountService: AccountService())
    // 두 뷰모델간 의존성 연결
    self.validationViewModel.eventFromRequestViewModel
        = self.requestViewModel.eventToValidationViewModel
    self.requestViewModel.eventFromValidationViewModel
        = self.validationViewModel.eventToRequestViewModel
    // bind 함수 호출
    self.requestViewModel.bindEvent()
    self.validationViewModel.bindEvent()
}

뷰모델 생성할 때 두 뷰모델 연결도 이런식으로 바뀌게 됩니다..!! 프로콜은 이제 작성하지 않아도 됩니다. 주의할 점은 각 event Subject가 nil값일때 sink하게 되면 구독이 안되므로 각각 서로 Subejct를 할당해준 뒤에 bind하셔야합니다. 따라서 저는 bindEvent함수를 따로 빼서 할당을 완료한 후에 bind해주었습니다.

 

⬇️ bindEvent()

더보기

 

bindEvent함수는 이런식으로 From Subject를 sink해주는 코드를 작성했습니다

func bindEvent() {
    eventFromRequestViewModel?.sink { [weak self] (event: SignupRequestViewModelEvent) in
        guard let self = self else { return }
        switch event {

        case .signup:
            self.eventToRequestViewModel.send(.sendInfo(Info: self.infoState))
            break
        }

    }.store(in: &cancellables)
}

 

바꾼 방식의 장점
✅ 각 뷰모델이 서로 모든 상태를 알지 않아도 되므로 의존성이 낮아졌다. (프로토콜 사용도 안해도됨)
⤷ 직접 접근시보다 옵셔널 체이닝도 덜 작성해도 돼서 코드 가독성도 좋아졌다.
✅ Event 열거형을 통해 소통에 필요한 요소들이 명확해졌다.
단점
❌ 의존성 연결하는 과정이 좀 더 복잡해졌다.

 

코드가 완성되면 전체 로직도 추가하겠습니다.

 

마치며


Massive View Model이 되는 문제를 어떻게 해결하면 좋을까 생각하다가 이런 방법을 고민하고 적용해보게 되었습니다. 한편으로는, 이렇게 ViewModel이 커지는 경우에는 기획단계에서 너무 한 뷰에 기능을 몰아넣지 않았는지 UX적인 검토도 필요할 것 같아 보이네요 ☺️ (하지만 작업하다보면 작은 기능에 많은 로직이 들어가게되는 경우가 많긴하죠..ㅎㅎ)

 

읽어주셔서 감사합니다!

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함