📚 학습 기록/Flutter 심화 & 프로젝트

Flutter SharedPreferences와 비동기 처리 완전 가이드: 실무 프로젝트 적용 패턴과 성능 최적화

zenjoydev 2025. 7. 12. 00:10

개요

Flutter 애플리케이션에서 데이터 영속성과 비동기 처리는 사용자 경험을 좌우하는 핵심 요소입니다. 본 가이드에서는 SharedPreferences를 활용한 로컬 데이터 저장, JSON 직렬화/역직렬화, 비동기 처리 패턴, Null Safety 적용 방법, 그리고 Static 메서드 패턴을 통한 코드 아키텍처 설계까지 실무에서 검증된 패턴들을 다룹니다.

대상 독자: Flutter 기초를 이해하고 있으며, 실무 프로젝트에서 안정적인 데이터 처리 패턴을 구현하고자 하는 개발자

목차

  1. SharedPreferences 기본 개념과 JSON 변환
  2. 비동기 처리 체이닝과 await 패턴
  3. Map 컬렉션 순회 최적화
  4. Null Safety 실전 적용 가이드
  5. Static 메서드 패턴과 아키텍처 설계
  6. 성능 최적화 및 보안 고려사항
  7. 실무 적용 사례와 문제 해결

SharedPreferences 기본 개념과 JSON 변환

데이터 영속성 전략

SharedPreferences는 Flutter에서 경량 데이터를 영구적으로 저장하는 표준 방법입니다. 복잡한 객체 데이터를 저장하기 위해서는 JSON 직렬화 과정이 필요하며, 이 과정에서 타입 안전성과 에러 처리가 중요합니다.

복합 객체 저장 패턴

class GameSettings {
  final String playerName;
  final int level;
  final List<String> achievements;
  final DateTime lastPlayed;

  GameSettings({
    required this.playerName,
    required this.level,
    required this.achievements,
    required this.lastPlayed,
  });

  // JSON 직렬화
  Map<String, dynamic> toJson() {
    return {
      'playerName': playerName,
      'level': level,
      'achievements': achievements,
      'lastPlayed': lastPlayed.toIso8601String(),
    };
  }

  // JSON 역직렬화 (팩토리 생성자)
  factory GameSettings.fromJson(Map<String, dynamic> json) {
    return GameSettings(
      playerName: json['playerName'] as String,
      level: json['level'] as int,
      achievements: List<String>.from(json['achievements']),
      lastPlayed: DateTime.parse(json['lastPlayed']),
    );
  }

  // 데이터 저장
  Future<void> saveSettings() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = jsonEncode(toJson());
    await prefs.setString('game_settings', jsonString);
  }

  // 데이터 로드
  static Future<GameSettings?> loadSettings() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = prefs.getString('game_settings');
    
    if (jsonString == null) return null;
    
    try {
      final Map<String, dynamic> json = jsonDecode(jsonString);
      return GameSettings.fromJson(json);
    } catch (e) {
      // 데이터 파싱 실패 시 로깅 및 null 반환
      print('Settings loading failed: $e');
      return null;
    }
  }
}

데이터 무결성 보장

JSON 직렬화 과정에서 데이터 무결성을 보장하기 위한 검증 로직을 추가합니다:

class DataValidator {
  static bool isValidGameSettings(Map<String, dynamic> data) {
    return data.containsKey('playerName') &&
           data.containsKey('level') &&
           data.containsKey('achievements') &&
           data.containsKey('lastPlayed') &&
           data['level'] is int &&
           data['achievements'] is List;
  }
}

비동기 처리 체이닝과 await 패턴

then 체이닝의 올바른 사용

비동기 함수 체이닝에서 await 키워드 사용은 필수입니다. 이를 생략할 경우 예상치 못한 실행 순서와 에러가 발생할 수 있습니다.

class PlayerDataManager {
  // ❌ 잘못된 패턴 - await 없는 then 체이닝
  void incorrectInitialization() {
    SharedPreferences.getInstance().then((prefs) {
      // 이 시점에서 prefs가 완전히 초기화되지 않을 수 있음
      final playerData = prefs.getString('player_data');
      processPlayerData(playerData);
    });
  }

  // ✅ 올바른 패턴 - await 사용
  Future<void> correctInitialization() async {
    final prefs = await SharedPreferences.getInstance();
    final playerData = prefs.getString('player_data');
    await processPlayerData(playerData);
  }

  Future<void> processPlayerData(String? data) async {
    if (data == null) {
      await initializeDefaultData();
      return;
    }
    
    try {
      final parsedData = jsonDecode(data);
      await updatePlayerStats(parsedData);
    } catch (e) {
      print('Player data parsing error: $e');
      await handleDataCorruption();
    }
  }
}

에러 처리와 복구 전략

class RobustDataLoader {
  static Future<Map<String, dynamic>?> loadWithFallback(String key) async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final jsonString = prefs.getString(key);
      
      if (jsonString != null) {
        return jsonDecode(jsonString) as Map<String, dynamic>;
      }
    } catch (e) {
      print('Data loading failed for key $key: $e');
      // 데이터 복구 시도
      await attemptDataRecovery(key);
    }
    
    return null;
  }

  static Future<void> attemptDataRecovery(String key) async {
    // 백업 데이터 또는 기본값으로 복구 로직
    print('Attempting data recovery for $key');
  }
}

Map 컬렉션 순회 최적화

forEach를 활용한 효율적 데이터 처리

Map 컬렉션의 forEach 메서드는 키-값 쌍을 효율적으로 처리할 수 있는 방법을 제공합니다.

class ScoreAnalyzer {
  static void analyzePerformance(Map<String, int> subjectScores) {
    final Map<String, String> analysisResults = {};
    
    subjectScores.forEach((subject, score) {
      String performance;
      if (score >= 90) {
        performance = '우수';
      } else if (score >= 80) {
        performance = '양호';
      } else if (score >= 70) {
        performance = '보통';
      } else {
        performance = '개선 필요';
      }
      
      analysisResults[subject] = performance;
      print('$subject: $performance ($score점)');
    });
    
    _generatePerformanceReport(analysisResults);
  }
  
  static void _generatePerformanceReport(Map<String, String> results) {
    final excellentSubjects = <String>[];
    final improvementNeeded = <String>[];
    
    results.forEach((subject, performance) {
      if (performance == '우수') {
        excellentSubjects.add(subject);
      } else if (performance == '개선 필요') {
        improvementNeeded.add(subject);
      }
    });
    
    print('우수 과목: ${excellentSubjects.join(', ')}');
    print('개선 필요 과목: ${improvementNeeded.join(', ')}');
  }
}

감정 분석 시스템 구현

class EmotionAnalyzer {
  static final Map<String, List<String>> emotionKeywords = {
    '기쁨': ['행복해', '좋아', '신나', '재밌어'],
    '슬픔': ['슬퍼해', '울면', '우울해'],
    '화남': ['짜증나', '화나', '싫어']
  };

  static List<String> analyzeText(String text) {
    final List<String> detectedEmotions = [];
    
    emotionKeywords.forEach((emotion, keywords) {
      for (String keyword in keywords) {
        if (text.contains(keyword)) {
          detectedEmotions.add(emotion);
          break; // 해당 감정 찾으면 다음 감정으로
        }
      }
    });
    
    return detectedEmotions;
  }

  static Map<String, int> getEmotionStatistics(List<String> texts) {
    final Map<String, int> emotionCounts = {};
    
    for (String text in texts) {
      final emotions = analyzeText(text);
      for (String emotion in emotions) {
        emotionCounts[emotion] = (emotionCounts[emotion] ?? 0) + 1;
      }
    }
    
    return emotionCounts;
  }
}

Null Safety 실전 적용 가이드

안전한 데이터 접근 패턴

Null Safety는 런타임 에러를 컴파일 타임에 방지하는 중요한 기능입니다. 실무에서는 다양한 null 체크 패턴을 적절히 조합하여 사용해야 합니다.

class SafeDataAccessor {
  static String getPlayerDisplayName(SharedPreferences prefs) {
    final String? storedName = prefs.getString('player_name');
    return storedName ?? '익명 사용자';
  }

  static UserProfile? loadUserProfile(SharedPreferences prefs) {
    final String? profileJson = prefs.getString('user_profile');
    
    if (profileJson == null) return null;
    
    try {
      final Map<String, dynamic> data = jsonDecode(profileJson);
      return UserProfile.fromJson(data);
    } catch (e) {
      print('Profile parsing error: $e');
      return null;
    }
  }

  static String getServiceType(int? age) {
    if (age == null) return '나이 정보 없음';
    
    return switch (age) {
      < 4 => '유아 전용',
      >= 4 && < 13 => '테스트 + 학습',
      _ => '성인'
    };
  }
}

연산자를 활용한 안전한 체이닝

class ChainingSafetyExample {
  // Null-aware 연산자 활용
  static void displayUserInfo(User? user) {
    print('사용자명: ${user?.name ?? "정보 없음"}');
    print('이메일: ${user?.email ?? "미설정"}');
    print('프로필 사진: ${user?.profile?.photoUrl ?? "기본 이미지"}');
  }

  // 조건부 실행
  static void updateUserPreferences(User? user, Map<String, dynamic> prefs) {
    user?.preferences?.updateAll((key, value) => prefs[key] ?? value);
  }
}

Static 메서드 패턴과 아키텍처 설계

싱글톤 패턴과 Static 메서드의 적절한 사용

Static 메서드는 상태가 없는 유틸리티 함수나 팩토리 메서드에 적합합니다. 설정 관리나 검증 로직에서 효과적으로 활용할 수 있습니다.

class AppConfigManager {
  static const String CONFIG_KEY = 'app_config';
  
  // 기본 설정 생성
  static AppConfig createDefault() {
    return AppConfig(
      theme: 'light',
      language: 'ko',
      soundEnabled: true,
      notificationEnabled: true,
      autoSaveInterval: 30, // seconds
    );
  }
  
  // 설정 유효성 검증
  static bool isValidConfig(Map<String, dynamic> config) {
    final requiredKeys = ['theme', 'language', 'soundEnabled'];
    return requiredKeys.every((key) => config.containsKey(key));
  }
  
  // 설정 마이그레이션
  static Map<String, dynamic> migrateConfig(
    Map<String, dynamic> oldConfig, 
    String fromVersion
  ) {
    switch (fromVersion) {
      case '1.0':
        // v1.0에서 v2.0으로 마이그레이션
        oldConfig['autoSaveInterval'] = 30;
        break;
      case '1.1':
        // v1.1에서 v2.0으로 마이그레이션
        oldConfig['notificationEnabled'] = true;
        break;
    }
    
    return oldConfig;
  }
  
  // 설정 로드 with 마이그레이션
  static Future<AppConfig> loadConfig() async {
    final prefs = await SharedPreferences.getInstance();
    final configJson = prefs.getString(CONFIG_KEY);
    
    if (configJson == null) {
      return createDefault();
    }
    
    try {
      Map<String, dynamic> config = jsonDecode(configJson);
      
      // 버전 체크 및 마이그레이션
      final version = config['version'] as String? ?? '1.0';
      if (version != '2.0') {
        config = migrateConfig(config, version);
        config['version'] = '2.0';
        
        // 마이그레이션된 설정 저장
        await prefs.setString(CONFIG_KEY, jsonEncode(config));
      }
      
      return AppConfig.fromJson(config);
    } catch (e) {
      print('Config loading failed, using default: $e');
      return createDefault();
    }
  }
}

팩토리 패턴 구현

abstract class DataProcessor {
  void process(Map<String, dynamic> data);
  
  static DataProcessor create(String type) {
    switch (type) {
      case 'user':
        return UserDataProcessor();
      case 'game':
        return GameDataProcessor();
      case 'analytics':
        return AnalyticsDataProcessor();
      default:
        throw ArgumentError('Unknown processor type: $type');
    }
  }
}

성능 최적화 및 보안 고려사항

성능 최적화 전략

class PerformanceOptimizer {
  // 배치 쓰기 최적화
  static Future<void> batchWrite(Map<String, String> data) async {
    final prefs = await SharedPreferences.getInstance();
    
    // 모든 쓰기 작업을 배치로 처리
    final futures = data.entries.map((entry) =>
      prefs.setString(entry.key, entry.value)
    );
    
    await Future.wait(futures);
  }
  
  // 캐시 메커니즘
  static final Map<String, dynamic> _cache = {};
  static const Duration _cacheTimeout = Duration(minutes: 5);
  static final Map<String, DateTime> _cacheTimestamps = {};
  
  static Future<String?> getCachedData(String key) async {
    // 캐시 유효성 검사
    final timestamp = _cacheTimestamps[key];
    if (timestamp != null && 
        DateTime.now().difference(timestamp) < _cacheTimeout) {
      return _cache[key];
    }
    
    // 캐시 미스 시 실제 데이터 로드
    final prefs = await SharedPreferences.getInstance();
    final data = prefs.getString(key);
    
    if (data != null) {
      _cache[key] = data;
      _cacheTimestamps[key] = DateTime.now();
    }
    
    return data;
  }
}

보안 고려사항

class SecureDataManager {
  // 민감한 데이터 암호화 저장
  static Future<void> storeSecureData(String key, String data) async {
    final encryptedData = _encrypt(data);
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('secure_$key', encryptedData);
  }
  
  static String _encrypt(String data) {
    // 실제 구현에서는 crypto 패키지 사용
    return base64Encode(utf8.encode(data));
  }
  
  static String _decrypt(String encryptedData) {
    return utf8.decode(base64Decode(encryptedData));
  }
  
  // 데이터 무결성 검증
  static bool verifyDataIntegrity(String data, String checksum) {
    final computedChecksum = _computeChecksum(data);
    return computedChecksum == checksum;
  }
  
  static String _computeChecksum(String data) {
    // 실제 구현에서는 crypto 해시 함수 사용
    return data.hashCode.toString();
  }
}

실무 적용 사례와 문제 해결

일반적인 문제와 해결책

문제 상황원인해결 방안

getInstance() 호출 시 에러 await 키워드 누락 await SharedPreferences.getInstance() 사용
JSON 파싱 실패 null 체크 미흡 try-catch와 null 체크 조합
DateTime 저장 실패 직렬화 불가능한 타입 toIso8601String() 변환 후 저장
UI 레이아웃 오버플로 제약 조건 미설정 Expanded나 Flexible 위젯 사용
비동기 함수 컴파일 에러 async 키워드 누락 Future<T> + async 패턴 적용

실무 프로젝트 적용 예시

class ProductionDataManager {
  static const String _userDataKey = 'user_data_v2';
  static const String _appStateKey = 'app_state_v2';
  
  // 프로덕션 환경에서의 안전한 데이터 로드
  static Future<UserData?> loadUserData() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final jsonString = prefs.getString(_userDataKey);
      
      if (jsonString == null) {
        await _initializeDefaultUserData();
        return await loadUserData(); // 재귀 호출로 기본 데이터 로드
      }
      
      final userData = UserData.fromJson(jsonDecode(jsonString));
      await _validateAndMigrateUserData(userData);
      
      return userData;
    } catch (e) {
      // 에러 로깅 및 복구
      await _logError('loadUserData', e);
      await _attemptDataRecovery();
      return null;
    }
  }
  
  static Future<void> _validateAndMigrateUserData(UserData userData) async {
    // 데이터 검증 및 필요시 마이그레이션
    if (userData.version < UserData.currentVersion) {
      final migratedData = await _migrateUserData(userData);
      await saveUserData(migratedData);
    }
  }
  
  static Future<UserData> _migrateUserData(UserData oldData) async {
    // 데이터 마이그레이션 로직
    return oldData.copyWith(version: UserData.currentVersion);
  }
  
  static Future<void> _logError(String operation, dynamic error) async {
    print('Error in $operation: $error');
    // 실제 프로덕션에서는 크래시리틱스나 로깅 서비스 연동
  }
}

결론 및 다음 단계

본 가이드에서 다룬 패턴들을 적용하면 Flutter 애플리케이션에서 안정적이고 효율적인 데이터 처리 시스템을 구축할 수 있습니다. 특히 SharedPreferences와 JSON 직렬화를 통한 데이터 영속성, 올바른 비동기 처리 패턴, Null Safety 적용은 프로덕션 환경에서 필수적인 요소입니다.

추가 학습 권장사항

  1. 상태 관리 패턴: Provider, Riverpod, BLoC 패턴 학습
  2. 데이터베이스 통합: Hive, SQLite와의 연동 패턴
  3. 고급 라우팅: Navigator 2.0과 Go Router 활용
  4. 성능 모니터링: Flutter Performance 도구 활용
  5. 테스트 전략: 단위 테스트 및 통합 테스트 구현

참고 자료


질문과 답변

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

🔍 기술 토론 참여

댓글로 의견을 나누어주세요:

  • 이 구현 방식에 대한 개선점이나 대안이 있으시면 공유해주세요
  • 실제 프로덕션 환경에서 적용해보신 경험담을 들려주세요
  • 성능이나 보안 관련 추가 고려사항이 있다면 알려주세요

💡 지식 공유

커뮤니티와 함께 성장해요:

  • 관련 기술의 최신 동향이나 업데이트 정보
  • 다른 플랫폼(React Native, Xamarin 등)과의 비교 경험
  • 팀 개발 환경에서의 적용 방법과 주의사항

🚀 다음 주제 제안

어떤 기술 주제를 다뤄드릴까요?

  1. Provider 패턴 심화 - 복잡한 상태 관리 아키텍처
  2. Hive vs SQLite 성능 비교 - 대용량 데이터 처리 최적화
  3. Navigator 2.0 실무 가이드 - 고급 라우팅과 딥링크 구현
  4. Flutter 테스트 전략 - 단위/위젯/통합 테스트 완전 가이드

가장 궁금한 주제를 댓글로 알려주세요!

관련 포스트

태그: Flutter, SharedPreferences, JSON직렬화, 비동기처리, NullSafety, 데이터영속성, 성능최적화, 모바일개발, 실무패턴, 아키텍처설계