📚 학습 기록/Dart & Flutter 기초

Flutter SharedPreferences 완전 정복: 초보자도 쉽게 따라하는 로컬 저장소 사용법

zenjoydev 2025. 7. 7. 16:33

개요

Flutter 앱을 만들다 보면 사용자 설정, 로그인 정보, 앱 상태 등을 저장해야 할 때가 많습니다. 이럴 때 가장 기본적으로 사용하는 것이 바로 SharedPreferences입니다.

이 글에서는 SharedPreferences의 기본 사용법부터 실무에서 바로 쓸 수 있는 고급 기법까지, 초보자도 쉽게 따라할 수 있도록 단계별로 설명하겠습니다.

목차

  1. SharedPreferences가 뭔가요?
  2. 기본 사용법 익히기
  3. 안전하게 사용하는 방법
  4. 실무에서 자주 쓰는 패턴들
  5. 설정 화면 만들기 실습
  6. 자주 하는 실수와 해결책

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');
  }
}

안전하게 사용하는 방법

문제점: 기본 사용법의 한계

기본 사용법에는 몇 가지 문제가 있습니다:

  1. 키 이름을 잘못 적으면 에러: 'user_name' vs 'username'
  2. 데이터가 없을 때 처리: null이 반환될 수 있음
  3. 반복되는 코드: 매번 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);

정리 및 다음 단계

오늘 배운 내용 요약

  1. SharedPreferences 기본 사용법: 문자열, 숫자, 불린값 저장
  2. 안전한 사용법: 헬퍼 클래스로 관리하기
  3. 복잡한 데이터: JSON 변환으로 객체 저장
  4. 실무 패턴: 설정 관리, 캐시 관리
  5. 실제 구현: 설정 화면 만들기

다음에 배우면 좋을 것들

  • Hive: 더 빠르고 강력한 로컬 저장소
  • SQLite: 복잡한 데이터 관계가 있을 때
  • Secure Storage: 민감한 정보 암호화 저장
  • Provider 패턴: 설정 변경을 전체 앱에 반영하기

마무리

SharedPreferences는 간단해 보이지만, 올바르게 사용하면 앱의 사용성을 크게 향상시킬 수 있는 중요한 도구입니다. 이 글에서 소개한 패턴들을 활용해서 여러분만의 앱에 적용해보세요!

가장 중요한 것은 안전하게 사용하는 것입니다. 항상 기본값을 제공하고, null 체크를 하고, 키 이름을 체계적으로 관리하세요.


💬 질문과 답변

이 포스트가 도움이 되셨나요?

댓글로 알려주세요:

  • SharedPreferences 사용 중 막힌 부분이 있으시면 언제든 질문해주세요!
  • 다른 좋은 패턴이나 팁이 있으시면 공유해주세요
  • 실제로 만든 설정 화면을 자랑해주세요!

다음 포스팅 주제 추천:

  1. Hive로 더 빠른 로컬 저장소 만들기
  2. Provider와 함께 사용하는 설정 관리
  3. 암호화 저장소로 보안 강화하기
  4. SQLite로 복잡한 데이터 관리하기

어떤 주제가 가장 궁금하신지 댓글로 투표해주세요! 😊

Flutter 개발에 도움이 되는 더 많은 콘텐츠로 찾아뵙겠습니다! 🚀

 

태그: Flutter, SharedPreferences, 로컬저장소, 초보자가이드, 설정화면, 데이터저장, 앱개발, 실습예제, 모바일프로그래밍, Flutter입문