개요
Flutter 앱을 만들다 보면 사용자 설정, 로그인 정보, 앱 상태 등을 저장해야 할 때가 많습니다. 이럴 때 가장 기본적으로 사용하는 것이 바로 SharedPreferences입니다.
이 글에서는 SharedPreferences의 기본 사용법부터 실무에서 바로 쓸 수 있는 고급 기법까지, 초보자도 쉽게 따라할 수 있도록 단계별로 설명하겠습니다.
목차
SharedPreferences가 뭔가요?
SharedPreferences는 간단한 데이터를 기기에 저장할 수 있는 Flutter의 기본 저장소입니다.
언제 사용하나요?
- 사용자 설정 (테마, 언어 등)
- 로그인 상태 유지
- 앱 처음 실행 여부 확인
- 간단한 캐시 데이터
어떤 데이터를 저장할 수 있나요?
- 문자열 (String)
- 숫자 (int, double)
- 불린값 (bool)
- 문자열 리스트 (List<String>)
기본 사용법 익히기
1단계: 패키지 추가하기
먼저 pubspec.yaml에 패키지를 추가해주세요:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.2.2
2단계: 기본적인 저장과 불러오기
import 'package:shared_preferences/shared_preferences.dart';
class BasicExample {
// 데이터 저장하기
Future<void> saveUserName(String name) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('user_name', name);
}
// 데이터 불러오기
Future<String?> getUserName() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('user_name');
}
// 데이터 삭제하기
Future<void> deleteUserName() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('user_name');
}
}
3단계: 다양한 타입 저장해보기
class DataTypeExample {
Future<void> saveVariousData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// 문자열 저장
await prefs.setString('user_name', '홍길동');
// 숫자 저장
await prefs.setInt('user_age', 25);
await prefs.setDouble('user_height', 175.5);
// 불린값 저장
await prefs.setBool('is_logged_in', true);
// 리스트 저장
await prefs.setStringList('favorite_colors', ['빨강', '파랑', '노랑']);
}
Future<void> loadVariousData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? name = prefs.getString('user_name');
int? age = prefs.getInt('user_age');
double? height = prefs.getDouble('user_height');
bool? isLoggedIn = prefs.getBool('is_logged_in');
List<String>? colors = prefs.getStringList('favorite_colors');
print('이름: $name, 나이: $age, 키: $height');
print('로그인 상태: $isLoggedIn');
print('좋아하는 색깔: $colors');
}
}
안전하게 사용하는 방법
문제점: 기본 사용법의 한계
기본 사용법에는 몇 가지 문제가 있습니다:
- 키 이름을 잘못 적으면 에러: 'user_name' vs 'username'
- 데이터가 없을 때 처리: null이 반환될 수 있음
- 반복되는 코드: 매번 SharedPreferences.getInstance() 호출
해결책: 간단한 헬퍼 클래스 만들기
class PrefsHelper {
// 키 이름을 한 곳에서 관리
static const String keyUserName = 'user_name';
static const String keyUserAge = 'user_age';
static const String keyIsLoggedIn = 'is_logged_in';
static const String keyThemeMode = 'theme_mode';
// SharedPreferences 인스턴스를 한 번만 가져오기
static SharedPreferences? _prefs;
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// 안전한 저장 메서드
static Future<void> setString(String key, String value) async {
await _prefs?.setString(key, value);
}
static Future<void> setInt(String key, int value) async {
await _prefs?.setInt(key, value);
}
static Future<void> setBool(String key, bool value) async {
await _prefs?.setBool(key, value);
}
// 안전한 불러오기 메서드 (기본값 포함)
static String getString(String key, {String defaultValue = ''}) {
return _prefs?.getString(key) ?? defaultValue;
}
static int getInt(String key, {int defaultValue = 0}) {
return _prefs?.getInt(key) ?? defaultValue;
}
static bool getBool(String key, {bool defaultValue = false}) {
return _prefs?.getBool(key) ?? defaultValue;
}
// 편리한 사용자 정보 메서드
static Future<void> saveUserInfo(String name, int age) async {
await setString(keyUserName, name);
await setInt(keyUserAge, age);
}
static Map<String, dynamic> getUserInfo() {
return {
'name': getString(keyUserName, defaultValue: '이름 없음'),
'age': getInt(keyUserAge, defaultValue: 0),
};
}
// 로그인 상태 관리
static Future<void> setLoggedIn(bool isLoggedIn) async {
await setBool(keyIsLoggedIn, isLoggedIn);
}
static bool isLoggedIn() {
return getBool(keyIsLoggedIn, defaultValue: false);
}
}
앱 시작할 때 초기화하기
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// SharedPreferences 초기화
await PrefsHelper.init();
runApp(MyApp());
}
이제 훨씬 간단하게 사용할 수 있어요!
class EasyUsageExample {
void saveUser() async {
// 간단하게 저장
await PrefsHelper.saveUserInfo('김철수', 30);
await PrefsHelper.setLoggedIn(true);
}
void loadUser() {
// 안전하게 불러오기 (기본값 포함)
Map<String, dynamic> userInfo = PrefsHelper.getUserInfo();
bool loggedIn = PrefsHelper.isLoggedIn();
print('사용자 정보: $userInfo');
print('로그인 상태: $loggedIn');
}
}
실무에서 자주 쓰는 패턴들
1. 복잡한 데이터 저장하기 (JSON 활용)
가끔은 복잡한 데이터를 저장해야 할 때가 있습니다. 이럴 때는 JSON을 사용합니다:
import 'dart:convert';
class UserProfile {
final String name;
final int age;
final String email;
final List<String> hobbies;
UserProfile({
required this.name,
required this.age,
required this.email,
required this.hobbies,
});
// 객체를 Map으로 변환
Map<String, dynamic> toMap() {
return {
'name': name,
'age': age,
'email': email,
'hobbies': hobbies,
};
}
// Map에서 객체로 변환
factory UserProfile.fromMap(Map<String, dynamic> map) {
return UserProfile(
name: map['name'] ?? '',
age: map['age'] ?? 0,
email: map['email'] ?? '',
hobbies: List<String>.from(map['hobbies'] ?? []),
);
}
}
class ProfileHelper {
static const String keyUserProfile = 'user_profile';
// 프로필 저장
static Future<void> saveProfile(UserProfile profile) async {
String jsonString = jsonEncode(profile.toMap());
await PrefsHelper.setString(keyUserProfile, jsonString);
}
// 프로필 불러오기
static UserProfile? getProfile() {
String jsonString = PrefsHelper.getString(keyUserProfile);
if (jsonString.isEmpty) return null;
try {
Map<String, dynamic> map = jsonDecode(jsonString);
return UserProfile.fromMap(map);
} catch (e) {
print('프로필 불러오기 실패: $e');
return null;
}
}
}
2. 앱 설정 관리하기
class AppSettings {
// 테마 설정
static Future<void> setDarkMode(bool isDark) async {
await PrefsHelper.setBool('is_dark_mode', isDark);
}
static bool isDarkMode() {
return PrefsHelper.getBool('is_dark_mode', defaultValue: false);
}
// 언어 설정
static Future<void> setLanguage(String language) async {
await PrefsHelper.setString('app_language', language);
}
static String getLanguage() {
return PrefsHelper.getString('app_language', defaultValue: 'ko');
}
// 첫 실행 여부 확인
static Future<void> setFirstRun(bool isFirst) async {
await PrefsHelper.setBool('is_first_run', isFirst);
}
static bool isFirstRun() {
return PrefsHelper.getBool('is_first_run', defaultValue: true);
}
// 알림 설정
static Future<void> setNotificationEnabled(bool enabled) async {
await PrefsHelper.setBool('notification_enabled', enabled);
}
static bool isNotificationEnabled() {
return PrefsHelper.getBool('notification_enabled', defaultValue: true);
}
}
3. 캐시 데이터 관리하기
class CacheHelper {
// 캐시 저장 (만료 시간 포함)
static Future<void> saveCache(String key, String data) async {
await PrefsHelper.setString('cache_$key', data);
await PrefsHelper.setString('cache_time_$key', DateTime.now().toIso8601String());
}
// 캐시 불러오기 (만료 확인)
static String? getCache(String key, {Duration maxAge = const Duration(hours: 1)}) {
String data = PrefsHelper.getString('cache_$key');
String timeStr = PrefsHelper.getString('cache_time_$key');
if (data.isEmpty || timeStr.isEmpty) return null;
try {
DateTime savedTime = DateTime.parse(timeStr);
if (DateTime.now().difference(savedTime) > maxAge) {
// 만료된 캐시 삭제
clearCache(key);
return null;
}
return data;
} catch (e) {
return null;
}
}
// 캐시 삭제
static Future<void> clearCache(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('cache_$key');
await prefs.remove('cache_time_$key');
}
}
설정 화면 만들기 실습
이제 배운 내용을 활용해서 실제 설정 화면을 만들어보겠습니다:
1단계: 설정 화면 UI 만들기
class SettingsScreen extends StatefulWidget {
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _isDarkMode = false;
bool _isNotificationEnabled = false;
String _selectedLanguage = 'ko';
@override
void initState() {
super.initState();
_loadSettings();
}
// 설정 불러오기
void _loadSettings() {
setState(() {
_isDarkMode = AppSettings.isDarkMode();
_isNotificationEnabled = AppSettings.isNotificationEnabled();
_selectedLanguage = AppSettings.getLanguage();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('설정'),
),
body: ListView(
children: [
// 다크모드 설정
SwitchListTile(
title: Text('다크 모드'),
subtitle: Text('어두운 테마를 사용합니다'),
value: _isDarkMode,
onChanged: (bool value) {
setState(() {
_isDarkMode = value;
});
AppSettings.setDarkMode(value);
},
),
Divider(),
// 알림 설정
SwitchListTile(
title: Text('알림 허용'),
subtitle: Text('푸시 알림을 받습니다'),
value: _isNotificationEnabled,
onChanged: (bool value) {
setState(() {
_isNotificationEnabled = value;
});
AppSettings.setNotificationEnabled(value);
},
),
Divider(),
// 언어 설정
ListTile(
title: Text('언어 설정'),
subtitle: Text(_getLanguageName(_selectedLanguage)),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
_showLanguageDialog();
},
),
Divider(),
// 사용자 정보
_buildUserInfoSection(),
Divider(),
// 데이터 관리
ListTile(
title: Text('모든 데이터 삭제'),
subtitle: Text('앱의 모든 저장된 데이터를 삭제합니다'),
trailing: Icon(Icons.delete, color: Colors.red),
onTap: () {
_showDeleteDialog();
},
),
],
),
);
}
// 사용자 정보 섹션
Widget _buildUserInfoSection() {
UserProfile? profile = ProfileHelper.getProfile();
if (profile == null) {
return ListTile(
title: Text('사용자 정보 없음'),
subtitle: Text('프로필을 설정해주세요'),
trailing: Icon(Icons.person_add),
onTap: () {
_showProfileDialog();
},
);
}
return Column(
children: [
ListTile(
title: Text('사용자 정보'),
subtitle: Text('${profile.name} (${profile.age}세)'),
trailing: Icon(Icons.edit),
onTap: () {
_showProfileDialog(profile);
},
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Align(
alignment: Alignment.centerLeft,
child: Wrap(
spacing: 8,
children: profile.hobbies.map((hobby) {
return Chip(
label: Text(hobby),
backgroundColor: Colors.blue.withOpacity(0.1),
);
}).toList(),
),
),
),
],
);
}
// 언어 선택 다이얼로그
void _showLanguageDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('언어 선택'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<String>(
title: Text('한국어'),
value: 'ko',
groupValue: _selectedLanguage,
onChanged: (String? value) {
if (value != null) {
setState(() {
_selectedLanguage = value;
});
AppSettings.setLanguage(value);
Navigator.of(context).pop();
}
},
),
RadioListTile<String>(
title: Text('English'),
value: 'en',
groupValue: _selectedLanguage,
onChanged: (String? value) {
if (value != null) {
setState(() {
_selectedLanguage = value;
});
AppSettings.setLanguage(value);
Navigator.of(context).pop();
}
},
),
],
),
);
},
);
}
// 프로필 설정 다이얼로그
void _showProfileDialog([UserProfile? existingProfile]) {
final nameController = TextEditingController(text: existingProfile?.name ?? '');
final ageController = TextEditingController(text: existingProfile?.age.toString() ?? '');
final emailController = TextEditingController(text: existingProfile?.email ?? '');
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(existingProfile == null ? '프로필 설정' : '프로필 수정'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: InputDecoration(labelText: '이름'),
),
TextField(
controller: ageController,
decoration: InputDecoration(labelText: '나이'),
keyboardType: TextInputType.number,
),
TextField(
controller: emailController,
decoration: InputDecoration(labelText: '이메일'),
keyboardType: TextInputType.emailAddress,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('취소'),
),
ElevatedButton(
onPressed: () {
final profile = UserProfile(
name: nameController.text,
age: int.tryParse(ageController.text) ?? 0,
email: emailController.text,
hobbies: existingProfile?.hobbies ?? ['독서', '영화감상'],
);
ProfileHelper.saveProfile(profile);
setState(() {});
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('프로필이 저장되었습니다')),
);
},
child: Text('저장'),
),
],
);
},
);
}
// 데이터 삭제 확인 다이얼로그
void _showDeleteDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('데이터 삭제'),
content: Text('정말로 모든 데이터를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('취소'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () async {
// 모든 데이터 삭제
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.clear();
Navigator.of(context).pop();
setState(() {});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('모든 데이터가 삭제되었습니다')),
);
},
child: Text('삭제', style: TextStyle(color: Colors.white)),
),
],
);
},
);
}
String _getLanguageName(String code) {
switch (code) {
case 'ko': return '한국어';
case 'en': return 'English';
default: return '알 수 없음';
}
}
}
2단계: 앱에서 설정 적용하기
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SharedPreferences 예제',
theme: AppSettings.isDarkMode() ? ThemeData.dark() : ThemeData.light(),
home: MainScreen(),
);
}
}
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('메인 화면'),
actions: [
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SettingsScreen()),
);
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('SharedPreferences 예제 앱'),
SizedBox(height: 20),
if (PrefsHelper.isLoggedIn()) ...[
Text('로그인 상태입니다'),
ElevatedButton(
onPressed: () async {
await PrefsHelper.setLoggedIn(false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('로그아웃되었습니다')),
);
},
child: Text('로그아웃'),
),
] else ...[
Text('로그인이 필요합니다'),
ElevatedButton(
onPressed: () async {
await PrefsHelper.setLoggedIn(true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('로그인되었습니다')),
);
},
child: Text('로그인'),
),
],
],
),
),
);
}
}
자주 하는 실수와 해결책
실수 1: 초기화 안하고 사용하기
// ❌ 잘못된 방법
void badExample() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String value = prefs.getString('key') ?? ''; // 매번 getInstance 호출
}
// ✅ 올바른 방법
void goodExample() {
String value = PrefsHelper.getString('key'); // 미리 초기화된 헬퍼 사용
}
실수 2: null 체크 안하기
// ❌ 위험한 방법
void dangerousExample() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String name = prefs.getString('name')!; // null일 수 있음!
}
// ✅ 안전한 방법
void safeExample() {
String name = PrefsHelper.getString('name', defaultValue: '기본이름');
}
실수 3: 키 이름 하드코딩
// ❌ 관리하기 어려운 방법
await prefs.setString('user_name', name);
String name = prefs.getString('user_name'); // 오타 위험!
// ✅ 관리하기 쉬운 방법
class Keys {
static const String userName = 'user_name';
}
await prefs.setString(Keys.userName, name);
String name = prefs.getString(Keys.userName);
실수 4: await 빼먹기
// ❌ 저장이 완료되기 전에 다음 코드 실행
prefs.setString('key', 'value'); // await 없음!
String value = prefs.getString('key'); // 빈 값일 수 있음
// ✅ 저장 완료 후 다음 코드 실행
await prefs.setString('key', 'value');
String value = prefs.getString('key');
실수 5: 복잡한 데이터를 직접 저장하려고 하기
// ❌ 불가능한 방법
List<UserProfile> users = [...];
await prefs.setStringList('users', users); // 에러!
// ✅ JSON 변환 후 저장
String usersJson = jsonEncode(users.map((u) => u.toMap()).toList());
await prefs.setString('users', usersJson);
정리 및 다음 단계
오늘 배운 내용 요약
- SharedPreferences 기본 사용법: 문자열, 숫자, 불린값 저장
- 안전한 사용법: 헬퍼 클래스로 관리하기
- 복잡한 데이터: JSON 변환으로 객체 저장
- 실무 패턴: 설정 관리, 캐시 관리
- 실제 구현: 설정 화면 만들기
다음에 배우면 좋을 것들
- Hive: 더 빠르고 강력한 로컬 저장소
- SQLite: 복잡한 데이터 관계가 있을 때
- Secure Storage: 민감한 정보 암호화 저장
- Provider 패턴: 설정 변경을 전체 앱에 반영하기
마무리
SharedPreferences는 간단해 보이지만, 올바르게 사용하면 앱의 사용성을 크게 향상시킬 수 있는 중요한 도구입니다. 이 글에서 소개한 패턴들을 활용해서 여러분만의 앱에 적용해보세요!
가장 중요한 것은 안전하게 사용하는 것입니다. 항상 기본값을 제공하고, null 체크를 하고, 키 이름을 체계적으로 관리하세요.
💬 질문과 답변
이 포스트가 도움이 되셨나요?
댓글로 알려주세요:
- SharedPreferences 사용 중 막힌 부분이 있으시면 언제든 질문해주세요!
- 다른 좋은 패턴이나 팁이 있으시면 공유해주세요
- 실제로 만든 설정 화면을 자랑해주세요!
다음 포스팅 주제 추천:
- Hive로 더 빠른 로컬 저장소 만들기
- Provider와 함께 사용하는 설정 관리
- 암호화 저장소로 보안 강화하기
- SQLite로 복잡한 데이터 관리하기
어떤 주제가 가장 궁금하신지 댓글로 투표해주세요! 😊
Flutter 개발에 도움이 되는 더 많은 콘텐츠로 찾아뵙겠습니다! 🚀
태그: Flutter, SharedPreferences, 로컬저장소, 초보자가이드, 설정화면, 데이터저장, 앱개발, 실습예제, 모바일프로그래밍, Flutter입문
'📚 학습 기록 > Dart & Flutter 기초' 카테고리의 다른 글
Flutter TodoList 앱 완전 가이드: 편집 기능 구현을 통한 5가지 핵심 개념 마스터 (3) | 2025.07.10 |
---|---|
Flutter Navigator 완전 가이드: Named Routes와 arguments를 활용한 효율적인 화면 관리 (4) | 2025.07.08 |
Flutter 데이터 그룹핑과 동적 위젯 생성: Map 자료구조를 활용한 확장 가능한 아키텍처 설계 (5) | 2025.07.05 |
🎲 코드팩토리 강의로 Flutter 프로젝트 만들기! (손코딩 후기) (3) | 2025.06.24 |
🚀 Flutter Timer와 PageView로 완벽한 자동 슬라이드 구현하기 | 메모리 누수 방지까지! (3) | 2025.06.17 |