DataStore — 설정·키값 저장 (SharedPreferences 대체)
작은 설정값(다크모드 여부·로그인 토큰·온보딩 완료)을 저장할 때, 오래된 SharedPreferences는 동기 API(commit)가 메인 스레드를 막고, 에러를 알리지 않으며, 타입 안전이 약하다. 구글의 현대 권장 대체가 DataStore — 코루틴·Flow 기반의 비동기·트랜잭션·타입 안전 저장소다. (출처: Android — DataStore.)
왜 SharedPreferences가 아니라 DataStore인가
SharedPreferences의 문제는 눈에 잘 안 띄지만 실재한다. 흔히 쓰는 코드부터 보자.
// SharedPreferences — 숨은 함정들
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
prefs.edit().putBoolean("dark", true).apply() // apply: 디스크 쓰기를 백그라운드로 미루나,
// 첫 getSharedPreferences/get 호출은 메인에서 디스크를 읽어 막을 수 있음
val dark = prefs.getBoolean("dark", false) // 동기 — 디스크 로딩 중이면 메인 스레드 블로킹(ANR)
prefs.edit().commit() // commit: 즉시 동기 디스크 쓰기 → 메인에서 부르면 ANR
핵심 문제 세 가지 — (1) commit()이나 첫 로딩이 메인 스레드를 막아 ANR 위험, (2) 쓰기 실패를 알 길이 없음(apply는 조용히 묻음), (3) 잘못된 타입으로 꺼내면 런타임에야 터짐. DataStore는 이 셋을 코루틴/Flow로 푼다.
// DataStore — 비동기·관찰 가능·에러를 예외로
val darkMode: Flow<Boolean> = ... // 읽기는 Flow (값 바뀌면 자동 방출, 디스크 I/O는 백그라운드)
suspend fun setDark(on: Boolean) // 쓰기는 suspend 트랜잭션 (실패 시 예외)
두 종류 — Preferences vs Proto
- Preferences DataStore — 키-값 저장(SharedPreferences와 모양은 비슷하나 비동기). 스키마는 없지만 키에 타입이 있어(
booleanPreferencesKey) 잘못된 타입 접근을 줄인다. 빠르게 도입하기 좋다. - Proto DataStore — protobuf 스키마로 구조를 정의 → 완전한 타입 안전. 여러 연관 설정을 하나의 강타입 객체로 다룰 때 적합하다.
Preferences DataStore — 코드
// 1) DataStore를 Context 확장으로 한 번 선언 (앱당 파일 하나)
val Context.dataStore by preferencesDataStore(name = "settings")
val DARK = booleanPreferencesKey("dark_mode")
// 2) 읽기 — Flow로 관찰 (값이 바뀌면 자동 방출)
val darkMode: Flow<Boolean> = context.dataStore.data
.map { prefs -> prefs[DARK] ?: false } // 키가 없으면 기본값
.catch { e -> if (e is IOException) emit(false) else throw e } // 읽기 실패 처리
// 3) 쓰기 — suspend 트랜잭션 (edit 블록이 원자적)
suspend fun setDark(on: Boolean) {
context.dataStore.edit { prefs -> prefs[DARK] = on }
}
여기서 *읽기가 Flow*라는 점이 SharedPreferences와 가장 다르다 — 설정이 바뀌면 그 변화가 구독 중인 모든 화면에 자동으로 흐른다. ViewModel에서 stateIn([09])으로 StateFlow로 노출하면 설정 화면에서 다크모드를 켜는 즉시 다른 화면도 반응한다(별도 새로고침 없이). 이것이 단방향 데이터 흐름([andarch UDF])과 자연스럽게 맞물린다.
흔한 함정 — DataStore엔 동기 읽기(get)가 없다. 앱 시작 시점에 값이 꼭 필요하면
runBlocking { dataStore.data.first() }로 한 번 읽거나(권장 안 함), Flow로 구독해 준비되면 반영하는 구조로 설계한다.
Proto DataStore — 타입 안전 스키마
.proto 파일로 메시지(설정 구조)를 정의하고 Serializer를 구현하면 — 오타·타입 오류가 컴파일 타임에 잡히는 강타입 설정 객체를 얻는다.
// user_prefs.proto
message UserPrefs { bool dark = 1; int32 font_scale = 2; string token = 3; }
읽기·쓰기 API는 Preferences와 비슷하되 키 대신 생성된 타입의 프로퍼티를 쓴다(prefs.dark). 여러 설정이 하나의 일관된 객체로 묶여, 부분 갱신도 updateData { it.toBuilder().setDark(true).build() }처럼 원자적이다.
SharedPreferences에서 이전
기존 앱은 DataStore가 SharedPreferences를 자동 마이그레이션하도록 도와준다 — preferencesDataStore(name, produceMigrations = { listOf(SharedPreferencesMigration(ctx, "old")) })로 기존 키-값을 한 번에 옮긴다. 점진 전환이 가능하다.
무엇을 저장하나 — DataStore vs Room
- DataStore — 적은 양의 키-값/설정(다크모드·토큰·플래그·온보딩 여부). 대용량·관계형이 아니다.
- Room([06]) — 구조화된 대량 데이터(목록·관계·쿼리·페이징).
- 잘못 쓰면 — 큰 데이터(수천 항목)를 DataStore에 넣으면 변경마다 전체를 직렬화/역직렬화해 비효율적이고 느리다. 설정은 DataStore, 데이터는 Room이 원칙이다.
정리 — 설정은 비동기·Flow로
DataStore의 핵심 — SharedPreferences의 메인 스레드 블로킹·에러 무시·약한 타입을 코루틴/Flow로 해결, Preferences(키-값·간단) vs Proto(스키마·타입 안전), 읽기는 Flow로 관찰(설정 변화가 화면까지 자동 반영)·쓰기는 suspend 트랜잭션(실패는 예외), 동기 읽기가 없으니 구독 구조로 설계, 적은 설정은 DataStore·대량 구조 데이터는 Room. 신규 코드에선 SharedPreferences 대신 DataStore가 정답이다.
한 줄 요약 — DataStore=SharedPreferences 대체(코루틴/Flow·비동기·트랜잭션·에러 신호·동기 읽기 없음). Preferences(키-값·
booleanPreferencesKey·간단) vs Proto(protobuf 스키마·타입 안전). 읽기data.map{}Flow(자동 반영, UDF와 결합)·쓰기edit{}suspend. SharedPreferencesMigration으로 이전. 설정은 DataStore, 대량 데이터는 Room.
(출처: Android — DataStore / Preferences / Proto DataStore / Migrate from SharedPreferences.)