
이번 포스팅에선 '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