이번 포스팅에선 'SharedPreferences
' 를 쓰지 않는 방법에 대해 알아보고자 한다. (?)
우리는 Key-Value
타입으로 간단하고 작은 데이터를 로컬에 저장하기 위해 SharedPreferences
라는 녀석을 애용하곤 했다.
그러나 어느 날, 안드로이드 공식 문서에서 SharedPreferences
사용 가이드 문서가 감쪽같이 사라졌다. 그 이유는 JetPack 라이브러리의 구성요소 중 하나인 DataStore
가 혜성같이 등장했기 때문이다.
안드로이드 차원에서 DataStore
의 사용을 거의 멱살잡고 강권하고 있다. 대체 어떤 메리트가 있길래 이러는걸까? 천천히 살펴보자.
이 녀석은 프로토콜 버퍼 (구조화된 데이터를 직렬화하는 메커니즘) 를 사용하여 Key-Value 타입 혹은 유형이 지정된 객체를 로컬에 저장할 수 있는 애다. 솔직이 이름 너무 성의 없는 것 같다
한 가지 멋있는 점은, Coroutine + Flow 를 사용할 수 있어 비동기적이고 일관적인 트랜잭션 방식으로 데이터를 저장한다는 점이다.
DataStore
는 Key-Value 타입으로 구성되어 있는 Preferences DataStore
랑 사용자가 정의한 데이터를 저장할 수 있는 Proto DataStore
이렇게 두 종류가 존재한다. Proto DataStore
를 사용하게 되면, 프로토콜 버퍼를 이용하여 스키마를 정의해야 한다. 이는 데이터의 타입을 보장해줄 뿐더러, SharedPreferences
보다 훨씬 빠르고 단순하다.
[이미지 출처] https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html
일단 항목을 자세히 안 봐도 뭔가 DataStore
가 더 많은 걸 할 수 있는 모양이다. 항목들을 보면 약 파는 것도 아니다.
확실히 SharedPreferences 에 비해 얻는 이점이 많은 것 같다. 괜히 강권하는게 아닌 것 같다. 필자도 쓱 훑어보니 러닝 커브도 높은 편이 아닌 것 같아
DataStore
로 넘어가도 괜찮을 것 같다는 생각을 했다. 따라서 슬슬 최근 개발중인 앱에서SharedPreferences
를 썼던 로직을 마이그레이션 해보려고 한다.
백문이 불여일타. 직접 만져보며 사용법을 익혀보자.
우선 항상 그래왔듯이, app
단위의 build.gradle
의 dependencies
에다가 아래와 같이 추가해주자.
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.datastore:datastore-preferences-core:1.0.0")
아래처럼 DataStore
를 생성해주자. 처음에 preferencesDataStore
등의 키워드가 인식 안 될 수 있는데, 마우스를 갖다대보면 Auto Import 된다.
class DataStoreModule(private val context: Context) {
private val Context.dataStore by preferencesDataStore(name = "data_store")
private val stringKey = stringPreferencesKey("key_name") // String 타입 저장 키값
private val intKey = intPreferencesKey("key_name") // Int 타입 저장 키값
}
DataStore
에서 사용할 키 값은 'type'PreferencesKey("key_name")
과 같이 선언할 수 있다. 해당 예시는 String, Int 타입을 저장하고 싶은 경우이다.
여기서부터 진국이다. 코루틴의 Flow
를 사용할 수 있다. DataStore
에서 데이터를 읽어올 때에, 해당 데이터를 Flow
객체로 전달하게 된다.
// `stringKey` Key 값에 대응하는 Value 반환
val textData: Flow<String> =
context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[stringKey] ?: "" // 아까 만든 Key 이용
}
map()
을 활용하여 아까 만든 키 값 (stringKey
) 에 대응하는 Value 를 가져오는 동작을 한다. 위에서 말했던 것처럼 Flow
형태로 가져오게 된다.
또한 catch()
를 활용하여, 데이터를 읽어오는 것을 실패하는 경우 발생하는 IOException
을 처리해줌으로써 비어있는 값을 전달해준다.
edit()
메소드를 활용하여 데이터를 쓸 수 있다. 그런데 이 작업은 반드시 비동기적으로 수행되어야 하므로, suspend
키워드를 통해 해당 함수가 코루틴 영역에서 동작할 수 있도록 해준다.
// String 값을 `stringKey` 의 Value 로 저장
suspend fun setTextData(text: String) {
context.dataStore.edit { preferences ->
preferences[stringKey] = text // 아까 만든 Key 이용
}
}
DataStore
에서 읽은 데이터를 TextView
에 적용한다고 해보자. 그럼 아래와 같이 코드를 작성해볼 수 있다. CoroutineScope
내에서 수행되어야 한다!
CoroutineScope(Dispatchers.Main).launch {
MyApplication.getInstance().getDataStore().textData.collect {
textView.text = it
}
}
딱 원하는 타이밍에 한 번만 값을 얻으려면 아래와 같이 사용해도 될 것이다.
CoroutineScope(Dispatchers.Main).launch {
textView.text = MyApplication.getInstance().getDataStore().textData.first()
}
아까 우리가 만든 setTextData()
이라는 Suspending 함수를 활용하여 DataStore
에 데이터를 저장하는 동작을 구현해보자. 매우 간단하다!
CoroutineScope(Dispatchers.Main).launch {
MyApplication.getInstance().getDataStore().setTextData("H43RO")
}
🤚🏻 만약 동기적인 동작이 필요한 경우, 아래와 같이 사용하면 된다.
runBlocking { val text = MyApplication.getInstance().getDataStore().textData.first() }
그러나 반드시 UI 쓰레드를 차단할 정도의 동작은 금해야 한다. 알다시피 ANR 혹은 UI 버벅거림이 발생하기 때문이다!
지금까지 DataStore 의 첫 인상을 살펴보았다. 이는 분명히 SharedPreferences 를 대체하기 위해 등장한 녀석인만큼, 많은 장점을 지니고 있다. 특히 Coroutine Flow 를 자체적으로 지원해주다보니 훨씬 비동기적이고 안전한 R/W 동작을 할 수 있는 것이 가장 큰 장점으로 다가온다. (물론 RxJava 도 지원한다!)
시간이 된다면 적극적으로 SharedPreferences → DataStore 마이그레이션을 고려해보면 좋을 것 같다.
https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html