[Kotlin Android JetPack] 데이터 로직을 뷰에서 분리해보기 - ViewModel 기초

l2hyunwoo·2020년 12월 6일
5

JetPack Review

목록 보기
2/5
post-thumbnail

Clean Coding & Seperate Of Concern


안드로이드 뷰를 그리고 기능구현을 할 수 있다 보면 이제 어떻게 하면 더 좋은 코드를 작성할 수 있는 지 고민을 하게 된다.

여기서 좋은 코드라는 것은 남이 심지어 내가 N개월, N년 뒤에 읽기만 해도 문맥이 파악되는 코드를 의미하며 여기에 좋은 유지보수성을 가지고 있다면 리팩토링을 하거나 기능 업데이트를 할 때에도 훨씬 편할 것이다.

아직 나는 개발 자체를 시작하지 않은 학생 개발자이지만 코드를 작성하기 위해 가장 먼저
1. 자신만의 코딩 컨벤션(코딩 규칙)을 정하고
2. 기능을 함수/클래스 단위로 분리(관심사 분리, Seperate Of Concern)시켜 코드의 재사용성을 높여야
할 것이다.

이번 게시글에서는 뷰(Activity, Fragment) 단에서 어떻게 기능을 분리시킬 수 있는 지, 그 기능을 도와주는 Android Architecture Component(AAC)인 ViewModel을 소개하고자 한다.

안드로이드에서의 View 기능 분리


우리가 단일 페이지에서 검색 서비스를 운용한다고 생각을 해보자. 그렇다면 그 페이지에서는
  1. 검색어 입력
  2. 서버 통신을 통해 검색어에 대한 결과 데이터 가져오기
  3. 데이터를 View에 뿌려주기

와 같은 작업이 발생할 것이다. 그렇다면 이 모든 과정을 뷰에서 처리하는 것이 좋을까? Google에서 제시한 앱 아키텍처 가이드에 의하면 기존 코드를 화면 처리(View), UI에서 발생하는 이벤트/데이터 로직 처리(ViewModel), 앱 내/리모트 데이터 송수신 처리(Repository, Model) 로 분리할 수 있다.

위와 같이 기능을 분리할 시 우선 코드가 Clean해지고 유지보수성이 높아진다는 장점이 있으며 더욱이 앱 사용자는 다양한 사용자 환경에서 앱과 상호작용을 할 것이다.

예를 들어 동영상을 시청할 경우 세로로 동영상을 시청하다가 핸드폰을 돌려 가로로 동영상을 시청할 수도 있을 것이다. 이 때, 뷰는 종료되었다가 다시 만들어지는데 뷰에 있었던 모든 데이터들이 화면전환 한 번 했다고 모두 사라지는 것이다. 이외에도 모든 환경 변경 사항을 일일이 대응하는 것보다 뷰와는 독립된 클래스를 만들어서 각기의 기능을 수행하는 것이 개발자의 개발 능률을 더 높일 수 있을 것이다.

ViewModel

뷰의 수명주기에 관계없이, 뷰가 종료되거나 숨겨져도 데이터를 계속 보존할 수 없을까? Google 진영에서 내놓은 해답은 바로 ViewModel이다.

ViewModel은 UI 컨트롤러(뷰)에서 데이터 로직을 분리시키기 위해 만든 클래스로 뷰에서 데이터 입력을 받거나 특정 이벤트로 인해 데이터 처리를 해야할 때, ViewModel에서 이 업무를 하도록 책임을 할당한다.

이는 ViewModel의 특이한 생명주기에서 기인하는데, ViewModel은 ViewModelProvider라는 클래스에서 생명주기를 정해준다. 이런 생명주기로 인해 뷰모델 객체는 뷰가 완전히 종료될 때까지 존속한다.

ViewModel 실습

EditText에서 값 가져와서 TextView에 집어넣자

우선 이번 실습의 의의는 다음과 같다

  • ViewModel에 데이터를 저장할 수 있다
  • ViewModel에서 데이터를 가져와 View에 삽입할 수 있다

즉 이번 실습을 통해서 뷰모델에 데이터를 저장하고 역할 분리를 시도할 수 있을 것이다.
kotlin-android-extension은 제거한 채로 실습을 진행했다. 이후 게시글을 통해 더 발전시킬 것이다.

gradle 의존성 설정

	// Android-KTX
    implementation "androidx.core:core-ktx:1.3.2"
    implementation "androidx.fragment:fragment-ktx:1.2.5"

ViewModel은 ViewModelProvider를 통해 객체를 제공받는다. 그러나 KTX(안드로이드 코틀린 확장함수 라이브러리)를 활용하면 by viewModels()(Fragment에서는 by activityViewModels())로 객체를 제공받는다.

ViewModel 클래스를 제작한다

class MainViewModel : ViewModel() {
    private var name = ""

    fun setName(newName: String) {
        name = newName
    }

    fun getName() = name
}

ViewModel을 상속받는 클래스를 만들고 이름을 저장하는 변수와 setter, getter를 만들어준다

Activity에서 ViewModel을 생성하기

class MainActivity : AppCompatActivity() {
    private val mainViewModel: MainViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.btn_main_input).setOnClickListener {
            val word = findViewById<EditText>(R.id.et_main_name).text.toString()
            mainViewModel.setName(word)
            Snackbar.make(
                findViewById(R.id.main_layout),
                "ViewModel에 ${word}가 저장되었습니다.",
                Snackbar.LENGTH_SHORT
            ).show()
        }

        findViewById<Button>(R.id.btn_main_get).setOnClickListener {
            findViewById<TextView>(R.id.txt_name).text = mainViewModel.getName()
        }
    }
}

Q. 왜 ViewModel은 뷰에서 직접 생성하지 않나요?
뷰에서는 ViewModel 객체를 직접 생성받지 않는다. 이는 위에서 언급한 생명주기와 관련되어 있기 때문인데, 객체를 클래스 내부에서 생성하면 그 객체는 그 클래스의 생명주기에 영향을 받는다.

또한 ViewModel은 위에 생명주기 그림에서 보듯이 onCreate가 몇 번 호출되어도 단 하나의 객체로 존재해야한다. 만약 내부에서 객체를 생성하는 방식으로 ViewModel을 주입한다면 뷰의 환경이 변할 때마다 계속 생성해야할 것이다. 따라서 ViewModel은 외부(Delegate Pattern 혹은 ViewModelProvider)에서 제공받는 방식으로 생성한다.

위와 같이 뷰에 뷰모델을 생성하였다면 클릭 리스너를 달아서 해당 기능에 ViewModel 내의 멤버 변수를 조작하는 방식으로 데이터를 처리한다.

실습 결과

<참고 자료>

실습 코드
https://github.com/l2hyunwoo/SampleDataBinding 에서 feature/viewmodel 참고

profile
이현우의 개발 브이로그

0개의 댓글