안드로이드(Android) 뷰모델(ViewModel)

들어가며

뷰모델은 안드로이드 AAC(Android Architecture Components)중 하나로 라이브 모델과 함께 굉장히 중요한 역할을 차지합니다. AAC는 구글에서 안드로이드 앱을 좀 더 쉽고 견고하게 개발할 수 있도록 개발자 커뮤니티의 피드백을 받아 만들어지고 있는 라이브러리입니다.

소프트웨어는 마치 사람처럼 성장합니다. 안드로이드가 1.0이 2008년에 발표되었으니 벌써 12년이 되었습니다. 당연히 안드로이드 API도 초기 설계는 부족한 부분이 많이 있었습니다. 이에 따라 어떤 부분은 개발하기도 어렵고 버그가 발생하기 쉬운 구조적인 문제도 아주 많았죠.

하지만 구글이 대단하고 잘하고 있는건 이런 부족한 부분을 끊임없이 발견하고 피드백을 받아 개선시켜주고 있다는 점입니다. AAC가 등장한 배경도, 그 AAC의 하나인 뷰모델이 등장하게 된 배경도 결국은 이런 맥락입니다.

자, 그럼 뷰모델이 해결하고자 하는 문제는 무엇이고, 뷰모델은 이 문제를 어떻게 해결해주고 있을까요? 이번 글에서는 이 두 가지 질문에 대해 알아보도록 하겠습니다.
(결론을 먼저 말하자면 뷰모델은 액티비티의 재생성 시 UI와 관련된 데이터를 유지하기 위해 사용합니다.)

뷰모델이 해결하고 싶은 문제

안드로이드 앱 개발을 한다고 하면, 가장 많이 부딪히는 개념중에 하나가 액티비티의 생명주기입니다. 안드로이드 앱의 거의 모든 UI 요소들은 액티비티에 속하게됩니다. 그리고 이 액티비티는 생명주기를 가집니다.

여기서 생명주기라 함은 크게 액티비티의 생성(onCreate)부터 소멸(onDestroy)까지를 말하고 좀 더 디테일하게 보면 유저에게 최초로 보여지는 시점(onStart - onStop), 포커스를 갖게되는 시점(onResume - onPause) 등을 포함합니다.

액티비티의 생명주기

문제. 액티비티는 시스템에 의해 재생성(re-created)될 수 있습니다.

이런 상황은 단말의 몇몇 설정 변경 시 발생합니다. 가장 흔한 예는 단말의 방향 전환(가로모드, 세로모드), 언어 설정 변경을 생각해 볼 수 있습니다. 이런 시스템 설정 변경은 앱 밖에서 발생하기 때문에 이로 인한 발생되는 액티비티의 재생성은 우리가 제어할 수 없습니다.

그리고 이렇게 액티비티가 재생성이 되면 액티비티가 가지고 있던 데이터는 사라집니다.

위와 같은 이유로 액티비티가 재생성 되었을 때, 좋은 유저 경험을 주기 위해서는 마치 아무일도 없었던 것 처럼 UI와 관련된 데이터를 어딘가에 저장해 두었다가 이를 이용해서 다시 화면을 그려줘야합니다.

기존의 해결책. onSaveInstanceState()와 onRestoreInstanceState()

액티비티의 onSaveInstanceState() 콜백을 이용하면 이런 예외적인 상황에서 저장해두고 싶은 데이터를 번들(Bundle)에 저장할 수 있습니다. 그리고 이렇게 저장해둔 데이터는 액티비티가 재생성될 때 onCreate(savedInstanceState: Bundle?)나 onRestoreInstanceState() 콜백을 통해 다시 전달받을 수 있습니다.

하지만 이 방법은 번들을 사용하기 때문에 저장하고자 하는 데이터의 형태도 제한이 되고, 많은 양의 데이터를 저장하기에는 제한이 있습니다. 번들의 사용에 대해 가이드 하고 있는 공식 문서에서는 50Kb 미만으로 유지하기를 가이드 하고 있습니다.

또한 이 방법에서 onSaveInstanceState()는 메인 쓰레드에서 동작해야하기 때문에 여기서 데이터를 저장하는데 시간을 많이 사용하게 되면 그만큼 UI에 버벅거림이 생기게 됩니다.

뷰모델은 이 문제를 어떻게 해결 할 수 있을까?

뷰모델은 아래와 같이 액티비티가 재생성이 되는 시나리오에서는 액티비티의 onDestroy가 호출되더라도 소멸되지 않습니다. 뷰모델은 액티비티내에서 finish()를 직접 호출하거나 사용자가 액티비티를 닫을 때(백키를 누르거나 히스토리에서 제거해서) 소멸됩니다.

뷰모델이 액티비티의 재생성시에도 살아 남으니 해결책은 아주 간단합니다. 액티비티가 재생성 되었을 때에도 유지하고 싶은 데이터는 뷰모델에 저장하면 됩니다.

뷰모델의 생명주기

뷰모델 사용 방법

간단히 화면에 게임의 점수를 표시해 준다고 생각해봅시다. 이 점수는 유저에 의해 업데이트가 되고, 가로모드를 지원하기 때문에 화면 방향이 전환되어도 유저에 의해 업데이트된 점수는 유지되어야 합니다.

이런 요구사항이 있을 때 뷰모델에 저장해야하는 값은 점수가 되어야 합니다. 아래와 같이 score를 저장하는 변수를 뷰모델에 선언해줍니다.

뷰모델 생성
1
2
3
class MyViewModel : ViewModel() {
var scroe: Int = 0
}

이렇게 만든 뷰모델은 생성자를 통해 만들지 않고 ViewModelProvider라는 팩토리 객체를 이용해서 만들어줘야합니다. 일단 뷰모델의 인스턴스를 얻었다면 그 이후 사용법은 간단합니다. 일반적인 클래스 객체를 사용하듯이 데이터를 읽고 쓰면 됩니다.

뷰모델의 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Get ViewModel instance
mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);

// Update ui
updateUi(mViewModel.scoreTeamA);
}

fun updateScore(score: Int) {
// Read and write data from the ViewModel
mViewModel.score = mViewModel.score + 1

// Update UI
textViewScore.text = mViewModel.score.toString()
}

참고로 안드로이드 개발자 페이지에서는 뷰모델과 라이브데이터(LiveData)를 함께 사용하는 예제를 보여주고 있습니다. 그 이유는 뷰모델과 라이브데이터를 함께 사용할 때 그 효용성이 극대화 되기 때문입니다.

지금은 이해를 돕기위해 라이브데이터에 대한 이야기는 하지 않겠습니다. 하지만 나중에 반드시 라이브데이터에 대해서도 공부해 보기를 권해드립니다.

뷰모델에 대해 조금 더 알아보자

앞에서 뷰모델이 어떤 문제를, 어떻게 해결 할 수 있는지 알아보았습니다. 이를 바탕으로 뷰모델을 언제, 어떻게 써야할지 조금 더 구체적으로 알아보도록 하겠습니다.

뷰모델에는 어떤 데이터를 저장해야 할까?

액티비티는 굉장히 다양한 데이터를 가지고 있을 수 있습니다. 그중에 뷰모델에 저장해야할 데이터는 어떤 데이터일까요? 정답은 UI와 연관된 데이터입니다. 앞서 뷰모델이 어떤 문제를 해결하고자 하는지를 이해했다면 당연한 이야기겠죠?

뷰모델을 사용할 때 주의점

뷰모델에는 반드시 액티비티나 프래그먼트(Fragment), 컨텍스트(Context)에 대한 참조를 저장하면 안됩니다. 뷰(Views)도 컨텍스트를 가지기 때문에 뷰에 대한 참조 역시 저장하면 안됩니다.

이에 대한 이유는 뷰모델의 생명주기를 생각해보면 알 수 있습니다. 뷰모델이 액티비티에 대한 참조를 갖고 있다고 생각해봅시다. 이 때 단말의 방향이 변경되어 액티비티가 재생성이 되면 어떻게 될까요?

현재 액티비티는 종료되고 새로운 액티비티가 생성되지만 뷰모델은 액티비티가 재생성 되어도 살아있기 때문에 종료된 액티비티의 참조를 갖고 있게됩니다. 결국 해당 액티비티는 뷰모델이 종료될 때까지 계속 메모리에 남게 됩니다. 즉 메모리 릭(Memory leak)이 발생합니다.

다만 여기에 한 가지 예외가 있는데, 어플리케이션의 컨텍스트는 뷰모델에서 가져도 됩니다. 어플리케이션은 액티비티가 아닌 어플리케이션의 생명주기를 따르기 때문입니다. 더해서, 뷰모델에서 시스템 서비스를 사용하기 위해 어플리케이션 컨텍스트가 필요한 경우 AndroidViewModel을 이용하면 됩니다. AndroidViewModel은 이런 경우를 위해 제공되는 뷰모델로 Application에 대한 참조를 가집니다.

뷰모델은 액티비티가 재생성될 때 UI와 관련된 데이터를 저장하기 위해 사용합니다.

또한 뷰모델은 액티비티의 재생성 시 UI 데이터를 저장하는 데 사용합니다. 데이터가 영구히 저장되어야 한다면 프리퍼런스(Preference)나 데이터베이스에 저장해야합니다.

뷰모델을 사용할 때 얻을 수 있는 추가적인 장점

뷰모델을 사용하면 UI와 관련된 데이터를 액티비티나 프래그먼트로부터 분리할 수 있습니다. 일반적으로 UI 개발 시 데이터와, 뷰, 로직을 분리하는 것이 코드의 복잡도를 줄이고 개발을 편하게 해준다는 것은 UI 개발에서 MVC, MVP, MVVM과 같이 다양한 패턴이 존재하는 것을 보아도 알 수 있습니다. 또한 이렇게 데이터와 UI를 분리하면 코드를 테스트 하기도 좋아집니다.

그리고 뷰모델을 올바르게 사용한다면 코드에 대한 가독성도 높아 질 수 있습니다. 예를 들어 어떤 액티비티의 코드를 분석하는데 여기서 뷰모델을 쓰고 있다면 바로 이 액티비티의 UI 관련 데이터는 이 뷰모델 코드를 보면 되겠구나하고 알 수 있습니다.

마무리

이번 글에서는 뷰모델의 개념에 대해 알아보았습니다. 뷰모델이 왜 만들어졌고 어떻게 사용하면 되는지만 알고 바로 뷰모델을 사용해도 되지만 뷰모델과 관련된 개념들은 재밌는 부분이 많습니다.

액티비티의 생명주기, 데이터를 저장하는 다양한 방법들, 라이브데이터, 프래그먼트, 디자인 패턴에서의 뷰모델과, 안드로이드 AAC 뷰모델과의 차이 등이 있는데 이런 개념들을 시간을 두고 하나씩 완전히 익혀두면 굉장히 큰 도움이 될 수 있습니다.

참고로 뷰모델에서 데이터를 저장하는 것은 내부적으로 프래그먼트의 setRetainInstance라는 메커니즘을 사용합니다.

마지막으로 어떤 도구도 상황에 맞게 사용하는 것이 중요합니다. 뷰모델에는 다양한 장점이 있지만 사용해야하는 곳과 주의할 점이 분명합니다. 이런 점들을 잘 알고 필요할 때 사용하는 것이 중요합니다. 이 글이 뷰모델을 사용하는데 조금이라도 도움이 되면 좋겠습니다.

참고자료

CSS 글꼴(Font) 이야기

CSS 글꼴

우리는 하루에도 수많은 글을 봅니다. 스마트폰으로 웹서핑을 하거나 책을 볼 때, 우연히 길에서 스쳐간 간판까지, 눈을 뜨고 있다면 의식적으로든, 무의식적으로든 글을 읽는 걸 피할 수 없죠.

글은 기본적으로 정보를 전달하지만 같은 글이라도 다양한 서체로 표현될 수 있습니다. 사람마다 글씨체가 모두 다른 것처럼 말이죠. 이에 따라 세상에는 굉장히 많은 글꼴이 개발되어 사용되고 있습니다. 애플의 스티브 잡스가 글꼴을 굉장히 중요하게 다루었다는건 굉장히 유명한 이야기입니다.

프로그래머인 저에게도 글꼴을 다룰일은 굉장히 많습니다. 개발하는 서비스에 따라 디자이너가 원하는 글꼴이 다르기 때문에 디자인 팀에서 선정한 글꼴을 적용해야합니다. 또한 개발환경을 세팅할 때 좋아하는 글꼴을 설치하고 이를 IDE에 적용하는 것은 마치 신성한 의식과도 같은 일입니다.

이번 글에서는 프로그래머로서 글꼴을 다룰 때 자주 마주치는 몇 가지 개념들에 대해 다루도록 하겠습니다.

세리프(Serif)와 산 세리프(Sans-Serif)

출처 https://www.w3schools.com

세리프(Serif)

세리프는 ‘장식’을 의미합니다. 위에 이미지에서 세리프 글꼴을 보면 선의 끝에 굴림 처리가 되어있고, 선의 굵기도 다른 특징을 가집니다. 한글로 치면 궁서체가, 영어로는 타임즈 뉴 로만(Times New Roman)이 대표적이죠. 보통 인쇄물에 많이 사용됩니다.

산 세리프(Sans-Serif)

Sans는 ‘없음’을 나타냅니다. 즉 세리프가 없다라는 의미가 되니 장식이 없다라고 받아들이면 되겠죠? 위의 이미지에서 산 쉐리프 서체를 보면 선의 굵기가 동일하고 장식이 없습니다. 한글 글꼴에서는 고딕 서체들을 떠올리면 됩니다. 주로 모니터 화면에서 보는 서체에 많이 사용됩니다.

참고로 디지털 세상에서 산 세리프 글꼴이 자주 쓰이게 된건 과거에는 디스플레이 기능의 제약상 곡선을 매끄럽게 표현하기 어려워 세리프 글꼴을 표현하기 어려웠던 이유가 있었다고 하네요.

세리프와 산 세리프의 차이

둘의 차이를 위에서 간단히 살펴 보았습니다. 표면적으로는 글꼴에 ‘장식’이 있는지 없는지가 핵심이지만 조금만 더 이 둘의 차이를 알아보도록 하겠습니다.

먼저 세리프와 산 세리프의 차이는 가독성(readability)과 판독성(legibility)을 가지고 볼 수 있습니다.

가독성은 많은 양의 글을 볼 때의 읽기 쉬운 정도를 나타내는데 일반적으로 신문과 같은 인쇄물에서는 쉐리프가, 모니터 화면 상에서는 산세리프가 가독성이 높다고 봅니다. 다만 이건 사람에 따라, 익숙한 정도에 따라 달라지는 주관적인 부분입니다.

판독성은 각각의 글자가 정확히 어떤 글자를 나타내는지를 인식할 수 있는 정도를 나타냅니다. 예를 들어 i와 l은 글꼴에 따라 매우 비슷하게 보일 수 있습니다. 프로그래머가 코딩을 할 때 가장 먼저 프로그래밍 하기 좋은 글꼴을 설정하는데 그 이유가 여기에 있습니다.

프로그래머가 사용하는 글꼴들은 대부분 이 판독성이 좋은 것들이 많습니다. 아래의 예는 네이버에서 만든 D2Coding 이라는 글꼴입니다. 이미지를 보시면 헷갈리기 쉬운 글자들이 구분되기 쉽도록 신경을 많이 쓴 것이 느껴 지시나요? 그리고 이런 노력들이 앞에서 이야기한 판독성을 높여줍니다.

d2coding

모노스케이프(Monoscape)

앞에서 세리프와 산 세리프에 대해 알아보았습니다. 그럼 두 번째. 글꼴을 이야기 할 때 모노스케이프는 무엇을 의미하는 걸까요?

답은 간단합니다. 모노스케이프 글꼴을 모든 글자의 가로길이가 같은 글꼴을 이야기합니다. 모노스케이프란 단어 자체가 고정너비라는 뜻이기도 합니다. 그럼 모노스케이프 글꼴은 언제 주로 사용할까요? 바로 프로그래밍을 할 때 가장 많이 선택 합니다. 글꼴이 고정너비여야만 코드가 동일한 간격으로 정렬되어 편집도 쉽고 보기도 좋기 때문이죠. 엄밀히 말하면 보통 모노스케이프이면서 산 세리프인 글꼴을 많이 사용합니다.

참고로 저는 코딩을 할 때 D2Coding이나 나눔고딕코딩 글꼴을 주로 사용하는데요. 이 두 글꼴은 한국에서 개발된 글꼴이라 한글도 잘 지원이 되고 무료이기까지 합니다.

아무래도 우린 한국 사람이니 코드를 짜다보면 주석이나, 코드 자체에도 한글이 들어갈 일이 생깁니다. 이럴 때 외국에서 만든 글꼴을 쓰면 한글이 예쁘게 나오지 않는 경우가 많은데(폰트에 한글 자체가 아예 안들어가 있기 때문에) D2Coding이나 나눔고딕코딩을 쓰면 이런 문제가 깨끗이 해결됩니다. 참고로 이 두 글꼴은 네이버에서 만든 글꼴입니다. 한때 제가 몸담았던 회사라 괜히 더 애정이 가네요. :)

d2coding

폰트 패밀리(Font Families)

글꼴은 우리가 글을 사용하며 함께 발전해 왔습니다. 어떤 운영체제를 사용하든 글꼴 설정에 들어가서 내가 사용할 수 있는 글꼴을 확인해 보면 그 숫자에 아마 놀라실겁니다.

이렇게 많은 글꼴중에 원하는 글꼴을 좀 더 쉽게 적용하려면 어떻게 해야할까요? 그리고 내가 사용하려고 하는 글꼴이 시스템에 설치가 되어있지 않다면 어떻게 해야할까요? 폰트 패밀리는 이런 문제를 해결해 줍니다.

아래는 CSS에서 폰트 패밀리를 설정하는 예제입니다. 아래 코드는 이렇게 읽을 수 있습니다.

시스템에 Verdana 글꼴이 있으면 Verdana를, 없으면 Arial을, Arial도 없으면 산 세리프 글꼴 중에 하나를 사용해주세요.

1
font-family: Verdana, Arial, sans-serif;

즉 글꼴이 없는 경우를 대비해서 좌측에서부터 순서대로 예비 글꼴을 지정할 수 있는 것이죠. 이때 특정 글꼴을 지정하거나, 앞서 우리가 배웠던 글꼴의 형태를 지정해 줄 수 있습니다. 위의 예에서는 산 세리프(sans-serif) 부분이 이에 해당합니다.

마무리

이번 글에서는 프로그래머로서 글꼴을 다룰 때 알아야할 아래의 세 가지 주요 개념에 대해 알아보았습니다.

  1. 세리프와 산 세리프
  2. 모노스케이프
  3. 폰트 패밀리

세리프와 산 세리프, 모노스케이프는 글꼴에 대한 전반적인 개념이고 폰트 패밀리는 CSS에서 글꼴을 지정하는 방식입니다. 폰트 패밀리는 CSS에서 대표적으로 사용되지만 다른 언어나, 툴 등에서도 자주 사용됩니다. 다만 사용되는 곳에 따라 구체적인 사용법은 조금 다를 수 있습니다. 하지만 우리는 개념을 알고 있으니 문서를 보면 금방 이해하고 적용할 수 있겠죠?