Android 앱 설정 화면 구현 완벽 가이드 — Jetpack DataStore, SharedPreferences 마이그레이션과 다크 모드

Android 앱을 개발하다 보면 설정 화면은 대부분의 프로젝트에서 필요해집니다.

처음에는 설정 항목이 몇 개 없기 때문에 SharedPreferences를 사용하는 경우가 많습니다.

예를 들어 다음과 같은 기능입니다.

  • 다크 모드 설정

  • 알림 활성화 여부

  • 이미지 자동 다운로드

  • 검색 결과 정렬 방식

  • 검색 기록 저장 여부

  • 사용자 인터페이스 옵션

하지만 앱이 복잡해질수록 설정을 저장하는 코드가 Activity와 Fragment 곳곳에 흩어지기 쉽습니다.

설정을 변경했는데 다른 화면에 바로 반영되지 않거나, 비동기 처리 과정에서 설정값을 읽는 순서가 꼬이는 문제도 발생할 수 있습니다.

이러한 문제를 개선하기 위해 Android에서는 Jetpack DataStore를 사용할 수 있습니다.

이번 글에서는 Kotlin + XML + MVVM 기반 Android 앱에서 DataStore를 활용해 설정 화면을 구현하는 방법을 알아보겠습니다.

작성 기준: 2026년 10월. 예제는 Preferences DataStore와 AppCompat 기반 XML UI를 사용합니다. 의존성 버전은 예시이며, 실제 프로젝트에서는 AndroidX 공식 릴리스 정보와 호환성을 확인해야 합니다.


1. SharedPreferences와 DataStore의 차이

기존 Android 앱에서는 설정값을 저장하기 위해 SharedPreferences를 많이 사용했습니다.

val preferences = getSharedPreferences(
    "app_settings",
    Context.MODE_PRIVATE
)

preferences.edit()
    .putBoolean("dark_mode", true)
    .apply()

설정값을 읽을 때는 다음과 같이 사용합니다.

val darkMode = preferences.getBoolean(
    "dark_mode",
    false
)

간단한 설정을 저장하기에는 편리하지만, 설정 항목이 많아지면 데이터 접근을 체계적으로 관리하기 어려울 수 있습니다.

DataStore는 Kotlin Coroutine과 Flow를 활용해 설정값을 비동기적으로 읽고 저장할 수 있도록 설계된 라이브러리입니다.

항목SharedPreferencesPreferences DataStore
데이터 접근동기 읽기 중심Flow 기반 비동기 읽기
데이터 저장apply() 또는 commit()edit()
변경사항 관찰리스너 사용Flow 사용
Coroutine 연동별도 처리기본 지원
데이터 구조Key-ValueKey-Value
트랜잭션 방식제한적원자적 업데이트 지원
기존 데이터 이전직접 구현Migration API 지원

DataStore는 SharedPreferences의 모든 기능을 그대로 대체하는 도구라기보다, 설정 데이터에 적합한 현대적인 저장 방식이라고 이해하면 됩니다.


2. DataStore의 종류

Jetpack DataStore에는 대표적으로 두 가지 방식이 있습니다.

Preferences DataStore

키와 값을 이용해 데이터를 저장합니다.

theme_mode = dark
notification_enabled = true
image_download_wifi_only = true

간단한 앱 설정을 저장할 때 적합합니다.

Proto DataStore

Protocol Buffers를 이용해 명시적인 데이터 구조를 정의합니다.

AppSettings
 ├── themeMode
 ├── notificationEnabled
 └── imageDownloadWifiOnly

데이터 타입과 스키마를 명확하게 관리해야 할 때 유용합니다.

이번 글에서는 개인 개발자가 사용하는 일반적인 Android 앱 설정 화면에 적합한 Preferences DataStore를 사용하겠습니다.

3. DataStore와 SQLite는 어떻게 구분할까?

이미 SQLite를 사용하는 앱이라면 DataStore가 추가로 필요한지 궁금할 수 있습니다.

두 저장소는 용도가 다릅니다.

데이터권장 저장소
다크 모드DataStore
알림 사용 여부DataStore
검색 정렬 옵션DataStore
마지막 선택한 필터DataStore
조류 기본 정보SQLite
사용자 메모SQLite
좋아요 목록SQLite
검색 기록 목록SQLite
대량 관계형 데이터SQLite

DataStore는 대량 데이터 검색이나 JOIN이 필요한 데이터베이스를 대체하는 용도로 적합하지 않습니다.

따라서 앱 설정은 DataStore, 구조화된 업무 데이터는 SQLite로 구분하는 것이 좋습니다.


4. DataStore 의존성 추가하기

app/build.gradle.kts에 다음 의존성을 추가합니다.

dependencies {

    implementation(
        "androidx.datastore:datastore-preferences:1.1.7"
    )

    implementation(
        "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2"
    )

    implementation(
        "androidx.lifecycle:lifecycle-runtime-ktx:2.9.2"
    )

    implementation(
        "androidx.appcompat:appcompat:1.7.1"
    )
}

위 버전은 예시입니다. 최신 안정 버전과 프로젝트의 Kotlin·AGP·AndroidX 호환성을 확인한 뒤 적용해야 합니다.

DataStore는 Coroutine과 Flow를 사용하므로 관련 의존성도 프로젝트에 구성되어 있어야 합니다.

5. DataStore 인스턴스 생성하기

DataStore는 같은 파일에 대해 여러 인스턴스를 생성하지 않도록 주의해야 합니다.

가장 간단한 방법은 preferencesDataStore 위임 속성을 사용하는 것입니다.

val Context.dataStore by preferencesDataStore(
    name = "app_settings"
)

이 코드는 일반적으로 파일의 최상위 영역에 선언합니다.

예를 들어 다음과 같은 파일을 만들 수 있습니다.

data/
 └── settings/
      ├── SettingsDataStore.kt
      ├── SettingsRepository.kt
      └── AppSettings.kt

SettingsDataStore.kt에 다음과 같이 작성합니다.

package com.example.app.data.settings

import android.content.Context
import androidx.datastore.preferences.preferencesDataStore

val Context.dataStore by preferencesDataStore(
    name = "app_settings"
)

실제 프로젝트에서는 패키지 이름을 변경해야 합니다.


6. 설정 데이터 모델 정의하기

설정값을 여러 화면에서 일관되게 사용하려면 데이터 클래스로 관리하는 것이 좋습니다.

data class AppSettings(

    val themeMode: ThemeMode = ThemeMode.SYSTEM,

    val notificationEnabled: Boolean = true,

    val wifiOnlyDownload: Boolean = true,

    val saveSearchHistory: Boolean = true
)

테마 모드는 enum으로 정의합니다.

enum class ThemeMode {
    SYSTEM,
    LIGHT,
    DARK
}

이렇게 하면 문자열을 직접 비교하는 것보다 안전하게 설정값을 관리할 수 있습니다.

7. DataStore 설정 키 정의하기

Preferences DataStore에서는 설정 항목마다 키를 정의합니다.

object SettingsKeys {

    val THEME_MODE =
        stringPreferencesKey("theme_mode")

    val NOTIFICATION_ENABLED =
        booleanPreferencesKey(
            "notification_enabled"
        )

    val WIFI_ONLY_DOWNLOAD =
        booleanPreferencesKey(
            "wifi_only_download"
        )

    val SAVE_SEARCH_HISTORY =
        booleanPreferencesKey(
            "save_search_history"
        )
}

사용하는 import는 다음과 같습니다.

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey

키 이름은 앱 업데이트 이후에도 유지하는 것이 좋습니다.

기존 키 이름을 변경하면 저장된 설정을 읽지 못할 수 있기 때문입니다.


8. SettingsRepository 구현하기

설정값을 읽고 저장하는 작업은 Repository에서 관리하겠습니다.

class SettingsRepository(
    private val context: Context
) {

    val settingsFlow: Flow<AppSettings> =
        context.dataStore.data
            .catch { exception ->

                if (exception is IOException) {
                    emit(emptyPreferences())
                } else {
                    throw exception
                }
            }
            .map { preferences ->

                val themeName =
                    preferences[
                        SettingsKeys.THEME_MODE
                    ] ?: ThemeMode.SYSTEM.name

                val themeMode =
                    ThemeMode.entries.firstOrNull {
                        it.name == themeName
                    } ?: ThemeMode.SYSTEM

                AppSettings(
                    themeMode = themeMode,

                    notificationEnabled =
                        preferences[
                            SettingsKeys.NOTIFICATION_ENABLED
                        ] ?: true,

                    wifiOnlyDownload =
                        preferences[
                            SettingsKeys.WIFI_ONLY_DOWNLOAD
                        ] ?: true,

                    saveSearchHistory =
                        preferences[
                            SettingsKeys.SAVE_SEARCH_HISTORY
                        ] ?: true
                )
            }
}

IOException이 발생하면 기본 설정값을 제공하도록 구성했습니다.

다만 파일 읽기 오류가 반복되는 상황에서는 오류 로그와 사용자 안내도 고려해야 합니다.

emptyPreferences()를 반환한다고 해서 손상된 파일이 자동으로 복구되는 것은 아닙니다.

9. 설정값 저장 함수 구현하기

테마 설정을 저장하는 함수입니다.

suspend fun setThemeMode(
    mode: ThemeMode
) {
    context.dataStore.edit { preferences ->

        preferences[
            SettingsKeys.THEME_MODE
        ] = mode.name
    }
}

알림 설정을 저장하는 함수입니다.

suspend fun setNotificationEnabled(
    enabled: Boolean
) {
    context.dataStore.edit { preferences ->

        preferences[
            SettingsKeys.NOTIFICATION_ENABLED
        ] = enabled
    }
}

이미지 다운로드 설정도 같은 방식입니다.

suspend fun setWifiOnlyDownload(
    enabled: Boolean
) {
    context.dataStore.edit { preferences ->

        preferences[
            SettingsKeys.WIFI_ONLY_DOWNLOAD
        ] = enabled
    }
}

검색 기록 저장 여부를 변경하는 함수입니다.

suspend fun setSaveSearchHistory(
    enabled: Boolean
) {
    context.dataStore.edit { preferences ->

        preferences[
            SettingsKeys.SAVE_SEARCH_HISTORY
        ] = enabled
    }
}

이 함수들은 앞에서 작성한 SettingsRepository 클래스 내부에 추가합니다.


10. DataStore의 edit()는 어떻게 동작할까?

DataStore의 edit()는 설정값 변경을 원자적으로 처리합니다.

예를 들어 여러 설정을 한 번에 변경할 수 있습니다.

suspend fun resetSettings() {

    context.dataStore.edit { preferences ->

        preferences[
            SettingsKeys.THEME_MODE
        ] = ThemeMode.SYSTEM.name

        preferences[
            SettingsKeys.NOTIFICATION_ENABLED
        ] = true

        preferences[
            SettingsKeys.WIFI_ONLY_DOWNLOAD
        ] = true

        preferences[
            SettingsKeys.SAVE_SEARCH_HISTORY
        ] = true
    }
}

이렇게 하면 여러 설정값을 하나의 업데이트 작업으로 관리할 수 있습니다.

단, DataStore의 트랜잭션은 해당 DataStore 파일 내부의 변경을 대상으로 합니다.

SQLite와 DataStore를 동시에 수정하는 작업을 하나의 공통 트랜잭션으로 묶을 수 있는 것은 아닙니다.


11. MVVM 구조에서 DataStore 사용하기

설정 화면에서는 ViewModel이 Repository를 통해 설정값을 관리하도록 구성할 수 있습니다.

SettingsFragment
       │
       ▼
 SettingsViewModel
       │
       ▼
 SettingsRepository
       │
       ▼
 Preferences DataStore

이 구조의 장점은 Fragment가 DataStore에 직접 접근하지 않아도 된다는 것입니다.

설정 저장 방식이 변경되더라도 UI 코드를 크게 수정하지 않을 수 있습니다.

12. SettingsViewModel 구현하기

class SettingsViewModel(
    private val repository: SettingsRepository
) : ViewModel() {

    val settings =
        repository.settingsFlow.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(
                5_000
            ),
            initialValue = AppSettings()
        )

    fun setThemeMode(
        mode: ThemeMode
    ) {
        viewModelScope.launch {
            repository.setThemeMode(mode)
        }
    }

    fun setNotificationEnabled(
        enabled: Boolean
    ) {
        viewModelScope.launch {
            repository.setNotificationEnabled(enabled)
        }
    }

    fun setWifiOnlyDownload(
        enabled: Boolean
    ) {
        viewModelScope.launch {
            repository.setWifiOnlyDownload(enabled)
        }
    }

    fun setSaveSearchHistory(
        enabled: Boolean
    ) {
        viewModelScope.launch {
            repository.setSaveSearchHistory(enabled)
        }
    }
}

여기서는 StateFlow를 사용했습니다.

기존 MVVM 프로젝트에서 LiveData를 사용하고 있다면 Flow를 LiveData로 변환하는 방식도 가능합니다.

다만 설정값은 지속적으로 변경될 수 있으므로 Flow를 직접 관찰하는 방식이 자연스럽습니다.


13. XML 설정 화면 만들기

설정 화면은 XML로 구성하겠습니다.

예를 들어 다음과 같은 화면입니다.

설정
─────────────────────────

화면 설정

테마
시스템 설정 사용

─────────────────────────

알림 설정

알림 사용              [ON]

─────────────────────────

데이터 설정

Wi-Fi에서만 이미지 다운로드 [ON]

검색 기록 저장          [ON]

─────────────────────────

설정 항목이 많지 않다면 NestedScrollView와 LinearLayout으로 구성할 수 있습니다.

14. 설정 화면 XML 예제

fragment_settings.xml 파일을 작성합니다.

<?xml version="1.0" encoding="utf-8"?>

<androidx.core.widget.NestedScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="20dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/settings_title"
            android:textSize="24sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="@string/settings_theme"
            android:textStyle="bold" />

        <Spinner
            android:id="@+id/spinnerTheme"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <Switch
            android:id="@+id/switchNotification"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="@string/settings_notification" />

        <Switch
            android:id="@+id/switchWifiOnly"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/settings_wifi_only" />

        <Switch
            android:id="@+id/switchSearchHistory"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/settings_search_history" />

    </LinearLayout>

</androidx.core.widget.NestedScrollView>

여기서는 이해하기 쉽도록 기본 Switch와 Spinner를 사용했습니다.

실제 프로젝트에서는 Material Components의 MaterialSwitch나 설정 화면 디자인에 맞는 커스텀 View를 사용할 수 있습니다.


15. strings.xml에 설정 문구 등록하기

설정 화면의 문자열은 하드코딩하지 않고 리소스로 관리하는 것이 좋습니다.

<resources>

    <string name="settings_title">설정</string>

    <string name="settings_theme">테마</string>

    <string name="settings_notification">
        알림 사용
    </string>

    <string name="settings_wifi_only">
        Wi-Fi에서만 이미지 다운로드
    </string>

    <string name="settings_search_history">
        검색 기록 저장
    </string>

    <string-array name="theme_options">
        <item>시스템 설정 사용</item>
        <item>라이트 모드</item>
        <item>다크 모드</item>
    </string-array>

</resources>

다국어를 지원하는 앱이라면 values-ko, values-en 등의 리소스 디렉터리에서 번역 문자열을 관리할 수 있습니다.


16. Fragment에서 설정값 관찰하기

설정 화면은 DataStore의 값을 관찰해 UI에 반영해야 합니다.

예를 들어 다음과 같이 구현할 수 있습니다.

viewLifecycleOwner.lifecycleScope.launch {

    viewLifecycleOwner.repeatOnLifecycle(
        Lifecycle.State.STARTED
    ) {

        viewModel.settings.collect { settings ->

            binding.switchNotification.isChecked =
                settings.notificationEnabled

            binding.switchWifiOnly.isChecked =
                settings.wifiOnlyDownload

            binding.switchSearchHistory.isChecked =
                settings.saveSearchHistory
        }
    }
}

repeatOnLifecycle()을 사용하면 Fragment가 적절한 생명주기 상태일 때 Flow를 수집할 수 있습니다.

다만 설정값을 UI에 반영하는 과정에서 Switch 리스너가 불필요하게 실행되지 않도록 구성해야 합니다.

17. Switch 변경 이벤트 연결하기

사용자가 Switch를 변경하면 ViewModel에 전달합니다.

binding.switchNotification
    .setOnCheckedChangeListener { _, checked ->

        viewModel.setNotificationEnabled(
            checked
        )
    }

Wi-Fi 설정도 같은 방식입니다.

binding.switchWifiOnly
    .setOnCheckedChangeListener { _, checked ->

        viewModel.setWifiOnlyDownload(
            checked
        )
    }

검색 기록 설정입니다.

binding.switchSearchHistory
    .setOnCheckedChangeListener { _, checked ->

        viewModel.setSaveSearchHistory(
            checked
        )
    }

실제 프로젝트에서는 초기 UI 바인딩과 사용자 입력을 구분하는 것이 좋습니다.

예를 들어 현재 값과 다른 경우에만 저장하도록 처리하면 불필요한 DataStore 업데이트를 줄일 수 있습니다.


18. 다크 모드 구현하기

Android 앱에서는 일반적으로 다음 세 가지 테마 모드를 제공합니다.

설정동작
시스템 설정 사용기기 테마 설정을 따름
라이트 모드밝은 테마 사용
다크 모드어두운 테마 사용

AppCompat을 사용하는 XML 기반 앱이라면 AppCompatDelegate를 이용해 테마를 변경할 수 있습니다.

19. AppCompatDelegate로 테마 변경하기

fun applyTheme(
    mode: ThemeMode
) {

    val nightMode = when (mode) {

        ThemeMode.SYSTEM ->
            AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM

        ThemeMode.LIGHT ->
            AppCompatDelegate.MODE_NIGHT_NO

        ThemeMode.DARK ->
            AppCompatDelegate.MODE_NIGHT_YES
    }

    AppCompatDelegate.setDefaultNightMode(
        nightMode
    )
}

테마를 변경하면 Activity가 재생성될 수 있습니다.

따라서 화면 상태와 사용자 입력이 손실되지 않도록 ViewModel 및 SavedState 처리를 고려해야 합니다.

20. 앱 시작 시 저장된 테마 적용하기

DataStore는 비동기적으로 설정값을 읽습니다.

따라서 앱이 실행된 뒤 테마를 변경하면 잠깐 다른 테마가 표시되는 현상이 발생할 수 있습니다.

이를 흔히 테마 깜빡임이라고 부릅니다.

테마 적용 시점을 설계할 때는 다음 사항을 고려해야 합니다.

  • 앱 시작 시 기본 테마 결정

  • DataStore 설정 로딩

  • 저장된 테마 적용

  • Activity 재생성 가능성

  • SplashScreen 표시 시간

  • 테마 적용 완료 전 화면 노출

예를 들어 Application에서 비동기적으로 설정을 읽고 테마를 적용할 수 있지만, 이 경우 첫 화면이 이미 표시된 뒤 테마가 바뀔 수 있습니다.

따라서 테마 깜빡임을 완전히 피해야 하는 앱이라면 시작 화면의 테마 적용 전략을 별도로 설계하는 것이 좋습니다.


21. Spinner로 테마 선택하기

설정 화면에서 Spinner를 사용하면 세 가지 테마 중 하나를 선택할 수 있습니다.

val themeModes = listOf(
    ThemeMode.SYSTEM,
    ThemeMode.LIGHT,
    ThemeMode.DARK
)

Spinner 어댑터를 설정합니다.

val adapter = ArrayAdapter.createFromResource(
    requireContext(),
    R.array.theme_options,
    android.R.layout.simple_spinner_item
)

adapter.setDropDownViewResource(
    android.R.layout.simple_spinner_dropdown_item
)

binding.spinnerTheme.adapter = adapter

선택 이벤트를 처리합니다.

binding.spinnerTheme.onItemSelectedListener =
    object : AdapterView.OnItemSelectedListener {

        override fun onItemSelected(
            parent: AdapterView<*>?,
            view: View?,
            position: Int,
            id: Long
        ) {

            val selectedMode =
                themeModes[position]

            viewModel.setThemeMode(
                selectedMode
            )
        }

        override fun onNothingSelected(
            parent: AdapterView<*>?
        ) = Unit
    }

이 예제는 선택 이벤트를 ViewModel에 전달하는 기본 구조입니다.

실제 프로젝트에서는 DataStore에서 읽은 기존 설정을 Spinner에 반영하고, 초기 선택 이벤트가 불필요한 저장을 발생시키지 않도록 해야 합니다.

또한 settingsFlow에서 테마 변경을 관찰해 applyTheme()를 호출하는 위치를 일관되게 정해야 합니다.


22. 테마 설정을 여러 화면에 반영하기

다크 모드는 설정 화면에서만 적용되는 기능이 아닙니다.

앱의 모든 Activity와 Fragment에서 동일한 테마를 사용해야 합니다.

AppCompat 기반 앱에서는 AppCompatDelegate.setDefaultNightMode()를 통해 앱 전체의 기본 야간 모드를 설정할 수 있습니다.

다만 Activity별로 서로 다른 테마를 강제로 적용하거나, 일부 화면에서 직접 배경색을 지정하면 일관성이 깨질 수 있습니다.

예를 들어 다음과 같은 하드코딩은 피하는 것이 좋습니다.

android:background="#FFFFFF"

대신 테마 속성을 활용할 수 있습니다.

android:background="?attr/colorBackground"

텍스트 색상도 테마 속성을 사용하는 것이 좋습니다.

android:textColor="?android:attr/textColorPrimary"

실제 사용 가능한 속성은 적용한 테마에 따라 달라질 수 있으므로 프로젝트의 테마 구성을 확인해야 합니다.


23. 이미지 다운로드 정책을 설정으로 관리하기

이미지가 많은 앱에서는 Wi-Fi에서만 원본 이미지를 다운로드하도록 설정할 수 있습니다.

예를 들어 다음과 같은 정책입니다.

Wi-Fi에서만 이미지 다운로드: ON

Wi-Fi 연결
    └── 고해상도 이미지 다운로드 허용

모바일 데이터 연결
    └── 썸네일만 표시

다만 DataStore에 설정값을 저장하는 것만으로 네트워크 요청이 자동으로 제한되지는 않습니다.

이미지 다운로드를 수행하는 Repository나 WorkManager에서 해당 설정값을 읽어 실제 정책에 반영해야 합니다.

24. WorkManager와 설정값 연결하기

예를 들어 Wi-Fi에서만 다운로드하도록 설정했다면 WorkManager에서 다음 조건을 적용할 수 있습니다.

val constraints =
    Constraints.Builder()
        .setRequiredNetworkType(
            NetworkType.UNMETERED
        )
        .build()

UNMETERED는 일반적으로 데이터 사용량에 따른 과금 제한이 없는 네트워크를 의미합니다.

반드시 Wi-Fi만을 의미하는 것은 아닙니다.

따라서 설정 이름을 엄밀하게 표현하려면 '데이터 사용량 제한이 없는 네트워크에서만 다운로드'와 같은 문구를 고려할 수 있습니다.


25. 검색 기록 저장 여부 적용하기

검색 기록 저장 설정도 DataStore에서 관리할 수 있습니다.

예를 들어 사용자가 검색 기록 저장을 비활성화했다면 검색어를 SQLite에 저장하지 않도록 처리합니다.

suspend fun saveSearchKeyword(
    keyword: String
) {

    val settings =
        settingsRepository.settingsFlow.first()

    if (!settings.saveSearchHistory) {
        return
    }

    searchHistoryRepository.insert(
        keyword
    )
}

이렇게 하면 설정값을 실제 기능에 반영할 수 있습니다.

다만 검색 기록 저장을 비활성화했을 때 기존 검색 기록까지 삭제할지는 별도의 정책으로 정해야 합니다.

사용자가 기대하는 동작을 설정 화면에 명확하게 안내하는 것이 좋습니다.


26. SharedPreferences에서 DataStore로 이전하기

기존 앱에서 SharedPreferences를 사용하고 있다면 DataStore로 이전할 수 있습니다.

예를 들어 기존 설정 파일이 다음과 같다고 가정하겠습니다.

getSharedPreferences(
    "legacy_settings",
    Context.MODE_PRIVATE
)

DataStore에서는 SharedPreferencesMigration을 사용할 수 있습니다.

27. SharedPreferencesMigration 구현하기

기존 DataStore 선언을 다음과 같이 변경할 수 있습니다.

val Context.dataStore by preferencesDataStore(
    name = "app_settings",
    produceMigrations = { context ->

        listOf(
            SharedPreferencesMigration(
                context,
                "legacy_settings"
            )
        )
    }
)

이 예제는 기존 SharedPreferences 데이터를 Preferences DataStore로 이전하는 기본 방식입니다.

단, 기존 SharedPreferences의 키와 DataStore의 키가 다르다면 별도의 변환 로직이 필요할 수 있습니다.

또한 마이그레이션을 적용한 뒤에는 기존 SharedPreferences에 같은 설정값을 계속 저장하지 않도록 코드를 정리해야 합니다.

28. 마이그레이션 시 주의사항

마이그레이션에서는 다음 사항을 확인해야 합니다.

  • 기존 설정 파일 이름

  • 기존 키 이름

  • 저장된 값의 타입

  • DataStore 키와의 호환성

  • 기본값 처리

  • 마이그레이션 실패 처리

  • 앱 업데이트 후 설정 유지

  • 이전 버전으로 다운그레이드하는 경우의 동작

특히 기존 앱에서 dark_mode를 Boolean으로 저장했는데 새 앱에서는 theme_mode를 문자열로 저장한다면 단순한 키 복사만으로는 충분하지 않습니다.

이 경우 기존 값을 새로운 설정 구조로 변환해야 합니다.


29. 설정값 초기화 기능 구현하기

설정 화면에는 기본값으로 복원하는 기능을 추가할 수 있습니다.

예를 들어 다음과 같은 버튼입니다.

설정 초기화

앱 설정을 기본값으로 되돌립니다.
사용자 메모와 좋아요는 삭제되지 않습니다.

[초기화]

설정 초기화는 DataStore에 저장된 설정값만 대상으로 하는 것이 좋습니다.

사용자 메모나 SQLite 데이터까지 함께 삭제하면 예상하지 못한 데이터 손실이 발생할 수 있습니다.

30. DataStore 초기화 구현하기

설정 파일의 모든 키를 제거하려면 다음과 같이 처리할 수 있습니다.

suspend fun clearSettings() {

    context.dataStore.edit { preferences ->

        preferences.clear()
    }
}

이렇게 하면 다음 조회부터 코드에 정의한 기본값이 사용됩니다.

하지만 같은 DataStore 파일에 다른 기능의 설정도 저장되어 있다면 모두 삭제됩니다.

따라서 실제 서비스에서는 초기화 대상 키만 선택적으로 제거하는 방식이 더 안전할 수 있습니다.


31. Product Flavor별 설정 관리하기

하나의 Android 프로젝트에서 여러 앱을 만드는 경우에는 Product Flavor를 사용할 수 있습니다.

예를 들어 다음과 같은 구성입니다.

productFlavors {

    create("bird") {
        dimension = "animal"
        applicationIdSuffix = ".bird"
    }

    create("fish") {
        dimension = "animal"
        applicationIdSuffix = ".fish"
    }
}

각 Flavor가 서로 다른 Application ID로 설치된다면 일반적으로 앱별 내부 저장소도 분리됩니다.

따라서 같은 DataStore 파일 이름을 사용하더라도 서로 다른 앱의 설정이 섞이지 않습니다.

다만 같은 Application ID를 공유하는 빌드 변형이나 데이터 이전 정책이 있는 경우에는 별도로 확인해야 합니다.

32. Flavor별 기본 설정 지정하기

조류 앱과 어류 앱의 기본 설정이 다를 수도 있습니다.

예를 들어 다음과 같습니다.

설정조류 앱어류 앱
다크 모드시스템 설정시스템 설정
이미지 다운로드제한 없음제한 없는 네트워크만
검색 기록저장저장
기본 정렬국명국명

이 경우 Flavor별 리소스나 설정 제공 클래스를 사용해 기본값을 구분할 수 있습니다.

다만 DataStore에 이미 사용자가 저장한 설정이 있다면 기본값보다 저장된 사용자 설정을 우선해야 합니다.


33. 설정값을 매번 읽어도 될까?

DataStore의 설정값은 Flow로 제공됩니다.

따라서 매번 first()를 호출하는 것보다 앱 구조에 따라 Flow를 관찰하고 필요한 상태를 유지하는 방식이 적합할 수 있습니다.

예를 들어 이미지 다운로드 정책처럼 작업 시작 시 한 번 확인하면 되는 값은 first()를 사용할 수 있습니다.

반면 다크 모드처럼 변경 즉시 화면에 반영해야 하는 값은 지속적으로 관찰하는 것이 좋습니다.

사용 상황권장 접근
화면에 설정 표시Flow 관찰
테마 변경 반영Flow 관찰
다운로드 시작 전 확인first()
Worker 실행 시 확인작업 시작 시 읽기
설정 변경edit()

중요한 것은 UI 스레드를 차단하는 방식으로 DataStore 값을 읽지 않는 것입니다.


34. DataStore와 멀티프로세스

일반적인 Android 앱은 하나의 프로세스에서 동작합니다.

이 경우 일반적인 Preferences DataStore 구성이 적합합니다.

하지만 별도의 프로세스에서 Service를 실행하는 앱이라면 주의해야 합니다.

서로 다른 프로세스에서 동일한 DataStore 파일에 접근해야 한다면 멀티프로세스용 DataStore 구성을 검토해야 합니다.

또한 같은 파일에 대해 일반 DataStore와 MultiProcessDataStore를 혼용해서는 안 됩니다.

대부분의 개인 개발 앱에서는 멀티프로세스 구성이 필요하지 않으므로 불필요하게 복잡하게 만들 필요는 없습니다.

35. 민감한 정보는 DataStore에 그대로 저장해도 될까?

DataStore는 일반적인 앱 설정을 저장하는 데 적합합니다.

하지만 비밀번호, 인증 토큰, API Secret과 같은 민감한 정보를 일반 Preferences DataStore에 평문으로 저장하는 것은 주의해야 합니다.

DataStore 자체가 모든 값을 자동으로 암호화하는 것은 아닙니다.

민감한 정보를 저장해야 한다면 Android Keystore와 적절한 암호화·키 관리 구조를 함께 검토해야 합니다.

특히 서버의 비밀 API 키를 앱 내부에 저장하는 방식은 근본적인 보안 해결책이 아닙니다.


36. 설정 화면 테스트하기

설정 화면은 단순해 보이지만 앱 전체 동작에 영향을 줄 수 있습니다.

다음과 같은 테스트가 필요합니다.

테스트확인할 내용
앱 최초 설치기본 설정 적용
다크 모드 선택테마 즉시 변경
앱 재실행설정 유지
시스템 테마 변경시스템 모드에서 반영
알림 설정 변경실제 알림 정책 반영
다운로드 정책 변경네트워크 조건 적용
검색 기록 비활성화신규 기록 저장 중단
설정 초기화기본값 복원
기존 앱 업데이트SharedPreferences 마이그레이션
Flavor별 설치설정 저장소 분리
프로세스 종료 후 재실행설정 유지
DataStore 읽기 오류기본값 및 오류 처리

특히 앱 업데이트 후 설정값이 유지되는지 확인하는 것이 중요합니다.

37. DataStore 단위 테스트

DataStore를 사용하는 Repository는 테스트용 저장소를 이용해 검증할 수 있습니다.

예를 들어 다음과 같은 항목을 테스트합니다.

  1. 초기값이 올바르게 반환되는지

  2. 테마 변경 후 저장되는지

  3. 여러 설정을 연속으로 변경해도 유지되는지

  4. 초기화 후 기본값으로 돌아가는지

  5. 잘못된 enum 문자열을 안전하게 처리하는지

테스트에서는 운영 앱의 DataStore 파일을 직접 사용하지 않는 것이 좋습니다.

임시 파일이나 테스트 전용 DataStore 인스턴스를 사용해 테스트 간 영향을 방지해야 합니다.


38. 자주 발생하는 문제와 해결 방법

문제 1. 설정을 변경했는데 다른 화면에 반영되지 않음

원인:

설정값을 앱 시작 시 한 번만 읽고 이후 변경을 관찰하지 않는 경우입니다.

해결:

Flow 또는 StateFlow를 이용해 변경사항을 관찰합니다.

문제 2. 다크 모드 변경 시 화면이 다시 생성됨

원인:

AppCompatDelegate가 테마 변경을 반영하면서 Activity를 재생성할 수 있습니다.

해결:

ViewModel과 SavedState를 이용해 화면 상태를 유지합니다.

문제 3. Spinner가 처음 열릴 때 설정값을 다시 저장함

원인:

초기 선택 이벤트가 사용자 입력과 동일하게 처리되는 경우입니다.

해결:

초기 UI 설정과 사용자 변경 이벤트를 구분합니다.

문제 4. 기존 SharedPreferences 설정이 사라짐

원인:

기존 키 이름이나 값의 타입이 새로운 DataStore 구조와 다를 수 있습니다.

해결:

마이그레이션 대상 파일과 키를 확인하고 필요한 변환 로직을 추가합니다.

문제 5. 설정 초기화 후 메모까지 삭제됨

원인:

설정 저장소와 사용자 데이터 저장소를 구분하지 않은 경우입니다.

해결:

DataStore 설정 초기화와 SQLite 사용자 데이터 삭제를 별도 기능으로 분리합니다.


39. 개인 개발자에게 추천하는 구현 순서

처음부터 복잡한 설정 시스템을 만들 필요는 없습니다.

다음 순서로 적용하는 것이 현실적입니다.

1단계: 설정 데이터 정의

어떤 설정을 저장할지 목록을 정합니다.

2단계: DataStore 구성

Preferences DataStore와 설정 키를 정의합니다.

3단계: Repository 구현

설정 읽기와 저장 로직을 한곳에 모읍니다.

4단계: ViewModel 연결

StateFlow를 통해 설정값을 UI에 전달합니다.

5단계: XML 설정 화면 구현

Switch, Spinner 등으로 설정을 변경합니다.

6단계: 실제 기능 연결

다크 모드, 알림, 이미지 다운로드 정책 등에 설정값을 반영합니다.

7단계: 기존 설정 마이그레이션

SharedPreferences를 사용하던 앱이라면 데이터 이전을 적용합니다.

8단계: 테스트

앱 재시작, 업데이트, 초기화, 테마 변경 등을 확인합니다.


40. Android 설정 화면 구현 체크리스트

  • DataStore 의존성 추가

  • DataStore 인스턴스 중복 생성 방지

  • 설정 데이터 모델 정의

  • 설정 키 이름 관리

  • SettingsRepository 구현

  • Flow로 설정값 읽기

  • edit()로 설정값 저장

  • ViewModel에서 설정 상태 관리

  • Fragment 생명주기에 맞춰 Flow 수집

  • 초기 UI 이벤트와 사용자 변경 구분

  • 다크 모드 시스템·라이트·다크 지원

  • 테마 변경 시 화면 상태 유지

  • 테마 속성 기반 UI 색상 사용

  • 알림 설정을 실제 알림 로직에 반영

  • 이미지 다운로드 정책 적용

  • 검색 기록 저장 여부 적용

  • SharedPreferences 마이그레이션

  • 기존 설정 키 및 타입 확인

  • 설정 초기화 기능 구현

  • 사용자 데이터와 설정 데이터 분리

  • Product Flavor별 기본 설정 확인

  • 멀티프로세스 사용 여부 확인

  • 민감한 정보 평문 저장 방지

  • 앱 재시작 후 설정 유지 테스트

  • 앱 업데이트 후 마이그레이션 테스트

41. 마무리

Android 앱의 설정 화면은 단순히 Switch와 Spinner를 배치하는 작업이 아닙니다.

사용자가 선택한 설정을 안전하게 저장하고, 앱의 여러 화면과 기능에 일관되게 반영하는 구조가 필요합니다.

특히 Kotlin + XML + MVVM 기반 앱에서는 다음 원칙이 중요합니다.

  1. 일반적인 앱 설정은 Preferences DataStore로 관리합니다.

  2. SQLite 사용자 데이터와 설정 데이터를 분리합니다.

  3. 설정 읽기와 저장은 Repository에서 담당합니다.

  4. ViewModel은 StateFlow로 설정 상태를 제공합니다.

  5. Fragment는 생명주기에 맞춰 설정값을 관찰합니다.

  6. 다크 모드는 AppCompatDelegate로 적용합니다.

  7. 설정값 저장과 실제 기능 적용을 구분합니다.

  8. 기존 SharedPreferences 데이터는 마이그레이션합니다.

  9. 설정 초기화 시 사용자 데이터를 삭제하지 않습니다.

  10. 앱 업데이트와 재시작 후에도 설정이 유지되는지 테스트합니다.

DataStore는 앱 설정을 비동기적으로 관리하고 변경사항을 관찰할 수 있다는 점에서 유용합니다.

특히 이미지 다운로드 정책, 검색 옵션, 알림 설정, 다크 모드처럼 여러 화면에서 공통으로 사용하는 설정이 많아질수록 효과적입니다.

설정 화면을 잘 설계하면 사용자의 선택을 안정적으로 유지할 수 있을 뿐 아니라, 앱의 기능이 증가하더라도 설정 관리 코드를 일관되게 유지할 수 있습니다.


참고 자료

태그: Android, Kotlin, Jetpack DataStore, Preferences DataStore, SharedPreferences, MVVM, StateFlow, Coroutine, AppCompatDelegate, 다크 모드, Android 설정 화면, 사용자 환경설정, AndroidX