iOS

MVVM (2)

스엠 2022. 12. 19. 17:16

2. 뷰 전환은 어디에서 하는가

mvvm에 관한 두번째 글은 뷰 전환은 어디에서 하는 것인가에 대한 글입니다.

화면 전환에도 몇 가지 방법들과 개념이 제시되었고 크게 보면 다음과 같은 3가지 개념이 주를 이루고 있다. 

화면 전환은 화면에 관한것이니 view에서 핸들링 하는게 맞다.

혹은 view는 자기 화면에 대한 것만 들고 있어야 하니 viewModel에 있어야 한다. 

혹은 viewModel, view둘다 화면전환에 있어서 책임을 가지고 있는 것이 옳지 않아 제 3의 책임자를 두어야 한다. 

 

이중에 어떤게 맞고 어떤게 틀리다라는 것은 없습니다. 자신의 프로젝트 상황에 맞춰서 개념을 선택하고 개발하면 됩니다. 

 

1. 첫번째 방법:  view에서 화면전화 

첫번째는 view에서 뷰전환을 하는 방법입니다. 

보통 UINavigationController를 사용해서 스택을 관리하니 다음과 같은 형태를 띄게 됩니다. 

class ViewController: UIViewController { 


	override func viewDidLoad(){
    		super.viewDidLoad()
        	let btn = UIButton()
        
        	btn.addTarget(self, selector : #selector(btnTapped),for: .touchUpInside)
        

	}
    
    
    @objc func btnTapped(sender : UIButton){ 
    	let view = SecondViewController()
        self.navigationController?.pushViewController(view,animated : true)
    }


}

방식에 대해서는 특별히 설명할 것이 없는 아주 기초적이고 당연한 방법 입니다.

그런데 일각에서는 View는 화면 구현과 UserInteraction만 받고 다른 책임을 가지고 있지 않아야 한다라고 주장을 합니다. 

예를 들어 다음과 같은 상황이 해당 될 수도 있습니다. 

class ViewController: UIViewController { 

	var isLoggedIn : Bool = false
    
	override func viewDidLoad(){
    		super.viewDidLoad()
        	let btn = UIButton()
        
        	btn.addTarget(self, selector : #selector(btnTapped),for: .touchUpInside)
	}
    
    
    @objc func btnTapped(sender : UIButton){
    	if isLoggedIn { 
        	let view = MainViewController()
        	self.navigationController?.pushViewController(view,animated : true)
        }
        else { 
        	let view = LoginViewController()
        	self.navigationController?.pushViewController(view,animated : true)
        }
    }
}

위와 같이 특정 데이터에 의해서 버튼을 눌렀을때 분기를 쳐서 어떤 화면으로 이동할지에 대해서 view에서 비즈니스 로직이 포함됩니다. 

따라서 view가 비즈니스 로직을 들고 있는 것을 회피하기 위해 다음과 같이 구현할 수 있습니다. 

viewmodel에서 판별하여 view에 알려주는 것이죠 

 

class ViewController: UIViewController, ViewModelDelegate { 

	var isLoggedIn : Bool = false
    
    lazy private var viewModel = ViewModel(view : self)
    
	override func viewDidLoad(){
    		super.viewDidLoad()
        	let btn = UIButton()
        
        	btn.addTarget(self, selector : #selector(btnTapped),for: .touchUpInside)
	}
    
    
    @objc func btnTapped(sender : UIButton){
    	viewModel.showNextView()
    }
    
    func pushToMainViewController(){ 
    	let view = MainViewController()
        self.navigationController?.pushViewController(view,animated : true)
    }
    
    func pushToLoginViewController(){ 
    	let view = LoginViewController()
        self.navigationController?.pushViewController(view,animated : true)
    }
}

protocol ViewModelDelegate : AnyObject { 
	func pushtoMainViewController()
    func pushToLoginViewController()

}

class ViewModel { 
	
    weak var view :ViewModelDelegate?
    private var isLoggedIn : Bool = true
    
    init(view : ViewModelDelegate){ 
    	self.view = view 
    }
    
    func showNextView(){ 
    	if isLoggedIn { 
        	self.view?.pushToMainViewController()
        }
        else { 
        	self.view?.pushToLoginViewController()
        }
    }


}

위와 같이  유저 인터렉션을 viewmodel에 넘겨주고 viewmodel에서 비즈니스 로직을 수행한 후 protocol을 이용해 view로 화면 전환 이벤트를 보내줍니다. 

 

저는 이렇게 하는 방법을 선호하고 주로 이렇게 개발하고 있습니다.

근데 이것도 모자라 view는 다른 view에 대해서 알필요가 전혀 없어!라고 말하며 viewmodel혹은 coordinator, router등으로 화면전환 이벤트를 빼는 방법도 있습니다.

 

2. 두번째 방법:  ViewModel에서 화면전화 

view가 다른 view에 관하여 전혀 정보가 없게 만드는 방법 중 1가지 입니다. 

바로 viewModel에서 화면 전환 이벤트를 다루는 것이죠

방법은 간단합니다. viewModel그냥 ViewController객체를 넘겨주면 됩니다. 

바로 이렇게요. 

class ViewController: UIViewController, ViewModelDelegate { 

    lazy private var viewModel = ViewModel(view : self,viewController : self)
    
	override func viewDidLoad(){
    		super.viewDidLoad()
        	let btn = UIButton()
        
        	btn.addTarget(self, selector : #selector(btnTapped),for: .touchUpInside)
	}
    
    
    @objc func btnTapped(sender : UIButton){
    	viewModel.showNextView()
    }
    
}

protocol ViewModelDelegate : AnyObject { 


}

class ViewModel { 
	
    weak var view :ViewModelDelegate?
    weak var viewController : UIViewController?
    
    private var isLoggedIn : Bool = true
    
    init(view : ViewModelDelegate,viewController : UIViewController){ 
    	self.view = view 
        self.viewController = viewController
    }
    
    func showNextView(){ 
    	if isLoggedIn { 
        	let view = MainViewController()
        	self.viewController?.pushViewController(view,animated : true)
        }
        else { 
        	let view = LoginViewController()
        	self.view?.pushToLoginViewController(view, animated : true)
        }
    }
}

확실히 화면 전환 이벤트를 ViewModel로 옮기면서 달리 view가 가벼워지는 것을 볼 수 있습니다. 

하지만 화면 전환 이벤트를 ViewModel로 옮김에 따라 이번에는 ViewModel이 살짝 무거워 졌네요.

그런데 여기서 한가지 의혹이 들만한 점이 있습니다. 

바로 ViewModel의 책임 바운더리에 대한 의문입니다. 

흔히 MVVM에서 ViewModel은 UIupdate에 관한 사항들을 데이터 바인딩을 통해 View로 알려주거나 

아니면 Model의 데이터를 read,update,delete 하는 비즈니스 로직 역할을 가지고 있습니다.

 

이렇듯이 ViewModel또한 화면전환을 하기엔 적절지 않아 보입니다(물론 strict하게 볼때는 그렇지만 MVVM자체가 standard한 기준이 없으므로 집단 내부 사정에 따라 각자 다르게 구현하고 해석할 수 있습니다.)

더군다나 아까 위에서 말했듯이 화면전환할게 많아질수록 ViewModel이 Massive해 질 수 밖에 없죠 

 

따라서 이러한 단점들을 보완하기 위해 새로운 개념이 나옵니다. 

Coordinator Pattern 아예 화면전화을 담당하는 새로운 Layer가 등장하게 됩니다. 

따라서 MVVM-C라는 2가지 패턴을 혼합하여 사용하는 경우도 많습니다. 

 

하지만 적어도 제가 볼땐 Coordinator Pattern 또한 명확한 가이드라인이 없습니다. 

따라서 구현하는 방법도 3가지 정도로 나뉘는 것으로 보입니다. 

 

Coordinator Pattern의 구현방법은 다음 글에서 이어 정리하도록 하겠습니다. 

 

 

 

 

'iOS' 카테고리의 다른 글

MVVM(4) - Coordinator  (0) 2022.12.27
MVVM(3) - Coordinator  (0) 2022.12.23
MVVM (1) - 바인딩 방법들  (1) 2022.12.09
KeyChain vs UserDefaults(2)  (0) 2022.11.22
KeyChain vs UserDefaults(1)  (0) 2022.11.17