마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

뷰와 뷰모델 관련해서 질문좀 드립니다

0 추천
앞전에 내비게이션 컴포넌트를 사용하는데.. 데이터 전달방법에 대해 질문하다가

SharedViewModel 이라는것을 사용해서 화면간 데이터를 전달하고 공유..?를 했는데요,

결론만 말하자면 앞전에 문제였던 화면전환시에도 데이터가 잘 유지되고는 있는데,

 

SharedViewModel이라는 것이 문서의 코드를 보니 뭐 내가 정의하는 뷰모델 클래스가

SharedViewModel이라는 것을 따로 상속해서 정의하는것이아니라 그냥 일반 ViewModel 클래스를

그대로 정의하서 사용하는데

다만 데이터를 서로 전달하는 두 프래그먼트 화면에서 같은 뷰모델을 사용하는것이더라구요..

대신 다른점은 프래그먼트에서 뷰모델 프로퍼티를 생성할때는 by viewmodels를 사용하는것이아니라

by activitiyViewModels를 사용한다는점? 정도같네요.

 

그런데 이제 궁금한것이, 두가지정도 생겼는데

뷰와 뷰모델의 상관관계에서 뷰당 뷰모델 하나가 정의되어야하고, 뷰모델:뷰는 1:n의 관계를 가질수있다?

이정도같은데.

그러니까 위의 예처럼 데이터를 주고받는 두 화면 A,B에서 하나의 뷰모델을 두개의 뷰(A,B)에서 사용하고 있죠 (1:n)

그렇다면 이제 MVVM 패턴을 적용할때.. A와 B화면은 세부적으로 가지고 있는 view들이 전부 다 다른데

A와 B 두화면의 공통된 viewmodel에는 어떤 데이터?들이 들어가야하나요?

A와 B들이 가지고 있는 뷰들이 서로 다른데 viewmodel에는 A와 B 어떤 뷰들에 대한 데이터에 대한

프로퍼티가 들어가야하는지.. 잘모르겠습니다..

그렇다고 A와 B 각각에 대한 뷰모델을 만들면 또 이상할것같구요..

 

두번째 질문은

A와 B화면에서 이미 데이터를 전달하면서 viewmodel을 정의해 사용했는데,

꼭 B가 A에서만 데이터를 받으라는 정의는 없잖아요? 실제로 제가 만드는 기능이

A에서도 데이터를 받고 화면 C에서도 데이터를 받아야하는데..

그렇다면 여기서 또 B와 C에대한 SharedViewModel 이라는것을 만든다면,

B는 뷰모델을 두개 가지고있는 형태일터인데.. 이런경우는 어떻게 되어야하나요?
codeslave (3,940 포인트) 님이 2021년 7월 8일 질문

1개의 답변

0 추천

그렇다면 이제 MVVM 패턴을 적용할때.. A와 B화면은 세부적으로 가지고 있는 view들이 전부 다 다른데

A와 B 두화면의 공통된 viewmodel에는 어떤 데이터?들이 들어가야하나요?

위의 질문의 답은 님이 구하셔야 하는 답입니다. 왜냐하면 뷰의 구성과와 비지니스 요구사항에 ViewModel에서 다루어야 하는 데이터도 달라지기 때문입니다. 따라서 구체적으로 어떤 데이터를 가져야 하는지 딱히 일반화 시켜서 말씀드리기는 어렵습니다. 한가지 명확한 것은 ViewModel은 Model(보통 API 또는 DB) 등에서 오는 데이터를 뷰에서 보여주기 좋은 형태로 변환해서 전달하는 역할을 한다는 것입니다. 따라서 ViewModel을 공유하는 것은 사실 다른 하면 사이의 데이터를 어떻게 잘 처리할지 하는 고민이 생기기 때문에 어느정도의 trade off가 존재하게 되는 겁니다.

만약 두개의 화면이 다 복잡하다면 처음으로 돌아가서 과연 꼭 ViewModel 을 공유해야만 하는지 디자인에 대한 리뷰를 해보시고, 꼭 필요하다면 ViewModel에는 두 화면이 공통적으로 가지는 데이터만 보관하고 화면 별로 별도의 클래스를 만들어서 이 클래스들에 실제 뷰에 대한 기능을 위임하는 것도 옵션이 될 수 있습니다.

기본적인 아이디어는 아래와 같습니다. 

sealed class AuthCodeRequestResult {
     data class Error(val e: Exception): AuthCodeRequestResult()
     data class Success(val userName: String, message: String): AuthCodeRequestResult()
}


//Note.ViewModel이 아님
class AuthCodeRequestSubViewMoel (
   // Dependencies
) {

  fun send(userName: String): AuthCodeRequestResult {...}
}

sealed class AuthCodeSubmitResult {
     ...
}


//Note.ViewModel이 아님
class AuthCodeSubmitSubViewMoel(
  // Dependencies
) {
   fun submit(userName: String, authCode: String): AuthCodeSubmitResult  {...}
}

class AuthCodeViewModel(
  private val authCodeRequstSub: AuthCodeRequestSubViewMoel,
  private val authCodeSubmitSub: AuthCodeSubmitSubViewMoel,
): ViewModel() {
   
   private val _authCodeRequestResultLiveData = MutableLiveData<AuthCodeRequestResult>()
   val authCodeRequestResultLiveData: LiveData<AuthCodeRequestResult> get() = _authCodeRequestResultLiveData     

   private val _authCoeSubmitResultLiveData = MuableLiveData<AuthCodeSubmitResult>()
   val authCoeSubmitResultLiveData: LiveData<AuthCodeSubmitResult> get() = _authCoeSubmitResultLiveData     

   fun sendAuthCodeClicked(userName: String) {
       val result =  authCodeRequstSub.send(userName)
       _authCodeResultLiveData.postValue(result)
   }

  fun submitAuthCode(authCode: String) {
     val userName = authCodeResult.value?.userName ?: throw IllegalStateException("Username cannot be null")
     val result = authCodeSubmitSub.submit(userName = userName, autCode = authCode)
     _authCoeSubmitResultLiveData.postValue(result)
  }
}


class AuthCodeRequestFragment: Fragment() {
    
    private val viewModel: AuthCodeViewModel by ...

   override fun onViewCreated(...) {
       super.onViewCreated(...)
       
       viewModel.authCodeRequestResultLiveData.observe(viewLifeCycleOwner) { result: AuthCodeRequestResult ->
            when (result) {
                  is AuthCodeRequestResult.Error -> showError()
                  is AuthCodeRequestResult.Success -> navigateToAuthCodeSubmitFragment()
            }
       }
   }

   fun sendButtonClicked() {
       viewModel.sendAuthCodeClicked(userNameText.text.toString())
   }

}


class AuthCodeSubmitFragment: Fragment() {

    private val viewModel: AuthCodeViewModel by ...

   override fun onViewCreated(...) {
       super.onViewCreated(...)

       viewModel.authCodeRequestResultLiveData.observe(viewLifeCycleOwner) {
             when (result) {
                  is AuthCodeRequestResult.Error -> throw IllegalStateException("This screen must not be shown when authcode request failed.") // app crash
                  is AuthCodeRequestResult.Success -> {
                         messageText.text = result.message
                  }
            }
       }
       
       viewModel.authCoeSubmitResultLiveData.observe(viewLifeCycleOwner) { result: AuthCoeSubmitResultL->
              when (result) {
                  is AuthCoeSubmitResultL.Error -> showError()
                  is AuthCoeSubmitResultL.Success -> showSuccess()
            }
       }
   }

    fun submitClicked() {
        viewModel.submitAuthCode(authCodeText.text.toString())
    }
}

 

위의 예제는 실제 현업에서도 많이 사용되는  플로우 중의 하나인데요. 예를 들면, 비번을 잊어버리면 사용자 이름을 넣고 SMS를 인증코드를 요청한 다음, 요청 성공시 다음화면으로 이동해서 인증코드를 검증하는 예입니다. 물론 화면의 구성은 요구사항에 따라 다를 수 있지만 화면 간에 데이터 공유를 어떻게 처리하는지만 보시기 바랍니다. 두 화면 간에 authCodeRequestResultLiveData를 공유하고 있고 각각의 화면에 대한 처리는 해당 *SubViewModel이라는 별도의 클래스에 위임을 하고 있는 점에 주목하세요. 

지극히 개인적인 선호사항이 될 수도 있는데, 저 같으면 ViewModel을 복잡하게 만드는 것 보다는 그냥 Navigation Component에 bundle을 넘겨서 처리할 것 같긴합니다. 왜냐면 ViewModel을 공유하게 되면 화면 간에 dependency(의존성?)가 생겨서 코드를 재사용하기가 더 어려워 질 뿐더러, 다른 화면에서 호출하지 말아야할 ViewModel의 함수를 호출할 가능성을 열어두게 되는 것이고 다른 두 화면의 코드를 하나의 클래스에 위치시키는 것은 그다지 좋은 디자인 같아 보이지는 않습니다. 물론 위처럼 간단한 예라면 문제가 없겠지만요.

spark (224,800 포인트) 님이 2021년 7월 8일 답변
spark님이 2021년 7월 8일 수정
감사합니다 코드를 자연스럽게 읽는 능력이 많이 부족해서 하나하나 따라가면서 이해한다고 고생했네요..이해가 잘안되는질문 두가지정도 드리겠습니다.

1.답변하신내용중에 SubViewModel을 별도의클래스에 위임한다고 주목하라고 하셨는데, 강조하신거보면 이 부분이 중요해보이는데 왜 SubViewModel까지 사용하는건가요?

제가 추측해보자면 두개의 프래그먼트가 같은 뷰모델을 사용하지만,
이 두 뷰(프래그먼트)는 각자의 당연 역할이 다르겠죠.
하나는 허가 코드 제출?(submit) 프래그먼트, 하나는 허가 요청(request)코드 프래그먼트..

역할이 다른 뷰모델이지만 하나의 뷰모델에서 역할에 맞지 않는 메소드가 있는건 너무 안좋겠죠. 만약에 SubViewModel 클래스가 없어서
 submit()과 send()가 SubViewModel이 아니라 공통된 ViewModel에 있다면
요청할때 submit을 호출한다던가 제출할때 request()를 호출한다던가 접근의 위험성이 높아지기떄문에 분리를한것같은데..
비단 위험성때문뿐만 아니라 말했듯이 역할의 분리같은 개념때문에도 있겠구요..
맞을까요..?

2.지난번 글에서도 bundle을 사용하라고 하셨는데
( https://developer.android.com/guide/navigation/navigation-pass-data?hl=ko#Safe-args )
여기 링크에서 safe-args와 스크롤 조금 더 아래로 내려보시면 bundle을 사용하는것이 나오는데, 그 아래 bundle을 사용하는 방법 말씀이실까요?
safe-args를 사용하는 방법과 bundle을 사용하는 방법은 뭐가 다른가요?

지난번에 navigation graph에 argument 태그로 정의하고 safe args 쓰다가
화면전환후 돌아오면 계속 디폴트값으로 초기화됐던 기억이 있어서..
1.  OOP에서 상당히 자주 언급되는 하나로 SOLID principle이란게 있습니다. 각각의 알파벳은 특정한 코드 디자인에 대한 이니셜입니다. S는 Single Responsibility라고 하는 건데, 단일 책임, 즉 어떤 클래스나 함수는 한가지 책임만을 지도록 설계한다는 것입니다. 한가지 책임이란 어떤 요구사항이 있을 때 한가지 이유에 의해서만 변경을 해야 한다는 것이죠. 이 책임이란 단어 때문에 논쟁의 여지가 있을 수 있기 때문에 팀내에서 어떤 것이 한가지 책임인지에 대한 동의가 필요합니다. 그때 그때 상황에 따라 달라질 수도 있습니다.
당연히 화면이 다르면 하는 일도 다르기 때문에 분리를 하는 것은 지극히 자연스럽습니다.  굳이 Single Responsibility까지 가지 않더라도 이 부분은 상당히 직관적이라 보여집니다. 수정사항이 생겼을 때 건드려야할 코드의 양을 최소화하는 것이 좋기 때문이죠. 어떤 요구사항이 g하나 생겼는데 위에 있는 것처럼 분리를 한다면 한개의 클래스만 건드려도 될 텐데, 두개의 클래스를 다 건드려야 한다면 작업시간도 길어지고 버그같은 걸 만들 확률도 더 커지기 때문입니다.
2. safe-arg는 플러그인+라이브러이구요. 결국에는 내부적으로는 Bundle을 사용하게 되어 있습니다. safe-arg는 타입에 안전하게 값을 넘길 수 있도록 해주는데(예를 들면, String 타입을 넘겨야 하는데 실수로 Int타입을 넘기지 못하도록 컴파일 타임에 에러를 발생시켜줍니다.), 저는 딱히 safe-arg를 추가해서 얻는 이득이 별로 없는 것 같아 사용하지는 않고 있습니다. 필요하시다면 당연히 사용하시면 됩니다. 그리고 safe-args는 사용하더라도 말씀하신 이슈는 해결해주지 못합니다.
감사합니다. safe args도 내부적으로 bundle인데 해결하지 못하나요?
아무튼..safe args말고 bundle을 사용해서 다시한번 해보겠습니다
화면 복귀 후 bundle로 전달된 값이 다시 세팅되는 것은 안드로이드 시스템이 bundle을 값을 복구하기 위해 보관하기 때문입니다. 그래야 디바이스 로테이션 같은 configuration change에도 값을 복구할 수가 있기 때문이죠. Bundle로 넘긴 값이 있고 다른 프레그먼트에서 이 값이 업데이트 된 후 다시 돌아왔다면 값이 변경되었는지의 여부를 알아야 하고 여기에 따라 변경된 값을 다시 읽어 오도록 해야겠죠. Navigation Component는 내부적으로 LiveData를 사용해서 프레그먼트간에 값을 전달할 수 있도록 하는 mechanism이 존재합니다. 마치 startActivityForResult 처럼 말이죠. 이걸 통해서 다른 프레그먼트에서 변경된 값을 전달 받을 수 있습니다.

아래의 두 링크를 한번 자세히 읽어보세요.
https://developer.android.com/guide/navigation/navigation-pass-data#bundle
https://developer.android.com/guide/navigation/navigation-programmatic?hl=zh-tw
...