하이어코딩 RSS 태그 관리 글쓰기 방명록 mahiru
2026-03-16 10:27:27
728x90
반응형

성능 최적화 개요

99개 화면, 39개 Provider, 44개 Service를 가진 대규모 Flutter 앱에서 성능은 사용자 경험의 핵심입니다. MICEMore에서 적용한 7가지 성능 최적화 전략을 소개합니다.

전략 적용 영역 효과
Future.wait 병렬 초기화 앱 시작 로딩 시간 40~60% 단축
비블로킹 폰트 프리로드 웹 렌더링 스플래시 500~1500ms 단축
플랫폼별 Firestore 캐시 데이터 접근 오프라인 지원 + 멀티탭 안정성
Stream 구독 관리 실시간 데이터 메모리 누수 방지
Batch Write 대량 데이터 처리 네트워크 요청 최소화
Fire-and-Forget 패턴 부차적 작업 응답 속도 향상
네트워크 상태 모니터링 전체 앱 오프라인 대응

1. Future.wait 병렬 초기화

앱 시작 시 독립적인 초기화 작업을 병렬로 동시 실행합니다.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
  usePathUrlStrategy();
  _setupErrorHandlers();

  // 4개 독립 작업 병렬 실행
  await Future.wait([
    SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
    initializeDateFormatting('ko', null),
    _initializeFirebase(),
    _initializeKakaoSdk(),
  ]);

  runApp(MultiProvider(providers: [...], child: MyApp()));
}

순차 vs 병렬 성능 비교

작업 소요 시간 순차 실행 병렬 실행
화면 방향 고정 ~50ms 0~50ms max(50, 200, 800, 300)
= ~800ms
날짜 포맷 초기화 ~200ms 50~250ms
Firebase 초기화 ~800ms 250~1050ms
카카오 SDK 초기화 ~300ms 1050~1350ms
합계 ~1350ms ~800ms (41% 단축)

2. 비블로킹 폰트 프리로드

웹 환경에서 한글 폰트 로드를 await 없이 트리거합니다.

if (kIsWeb) {
  // 비블로킹: 폰트 로드 시작만 하고 완료를 기다리지 않음
  GoogleFonts.notoSansKr(fontWeight: FontWeight.w400);
  GoogleFonts.notoSansKr(fontWeight: FontWeight.w500);
  GoogleFonts.notoSansKr(fontWeight: FontWeight.w600);
  GoogleFonts.notoSansKr(fontWeight: FontWeight.w700);
  // await GoogleFonts.pendingFonts() 제거
  // → 스플래시 중 500~1500ms 블로킹 방지
}

CanvasKit 렌더러가 시스템 폰트로 폴백하므로, 폰트 로드 전에도 텍스트가 정상 표시됩니다. 백그라운드에서 폰트가 준비되면 자동으로 교체됩니다.

3. 플랫폼별 Firestore 캐시 전략

웹과 모바일에서 서로 다른 캐시 전략을 적용합니다.

if (kIsWeb) {
  // 웹: 영속성 비활성화 (멀티탭 충돌 방지)
  FirebaseFirestore.instance.settings = const Settings(
    persistenceEnabled: false,
  );
} else {
  // 모바일: 무제한 캐시 (오프라인 지원)
  FirebaseFirestore.instance.settings = const Settings(
    persistenceEnabled: true,
    cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
  );
}

플랫폼별 캐시 전략 이유

플랫폼 영속성 캐시 크기 이유
비활성화 - 멀티탭에서 IndexedDB 동시 접근 시 충돌 방지
모바일 활성화 무제한 오프라인 지원, 네트워크 절약, 빠른 데이터 접근

4. Stream 구독 관리: 메모리 누수 방지

39개 Provider가 Firestore Stream을 구독하므로, 체계적인 구독 관리가 필수입니다.

방어적 구독 패턴

class GamificationProvider with ChangeNotifier {
  StreamSubscription<List<LeaderboardEntryModel>>? _leaderboardSubscription;
  StreamSubscription<LeaderboardEntryModel?>? _myRankSubscription;
  StreamSubscription<List<PointRecordModel>>? _myPointHistorySubscription;

  void loadLeaderboard(String eventId, {int? limit}) {
    // 핵심: 새 구독 전 기존 구독 해제
    _leaderboardSubscription?.cancel();
    _leaderboardSubscription =
        _service.getLeaderboard(eventId, limit: limit).listen(
      (data) {
        _leaderboard = data;
        notifyListeners();
      },
      onError: (e) {
        _error = e.toString();
        notifyListeners();
      },
    );
  }

  @override
  void dispose() {
    // 모든 구독 해제
    _leaderboardSubscription?.cancel();
    _myRankSubscription?.cancel();
    _myPointHistorySubscription?.cancel();
    super.dispose();
  }
}

구독 관리 3원칙

  • Cancel-Before-Subscribe: 새 구독 시작 전 기존 구독 해제로 중복 구독 방지
  • Nullable Subscription: StreamSubscription?로 선언하여 null-safe cancel 호출
  • Dispose Cleanup: Provider dispose()에서 모든 구독 일괄 해제

5. Batch Write: 대량 데이터 처리

Firestore에 여러 문서를 한 번에 처리할 때 Batch Write로 네트워크 요청을 최소화합니다.

리더보드 순위 일괄 업데이트

Future<void> _updateRanks(String eventId) async {
  final snapshot = await _firestore
      .collection('events').doc(eventId)
      .collection('leaderboard')
      .orderBy('totalPoints', descending: true)
      .get();

  final batch = _firestore.batch();
  int rank = 1;
  for (var doc in snapshot.docs) {
    batch.update(doc.reference, {'rank': rank});
    rank++;
  }
  await batch.commit(); // 단일 네트워크 요청으로 모든 순위 업데이트
}

알림 전체 읽음 처리

Future<void> markAllAsRead() async {
  final batch = _firestore.batch();
  final unreadNotifications = _notifications.where((n) => !n.isRead);

  for (final notification in unreadNotifications) {
    batch.update(
      _firestore.collection('notifications').doc(notification.id),
      {'isRead': true},
    );
  }
  await batch.commit();

  // Optimistic Update: Firestore 응답 전 로컬 상태 즉시 반영
  _notifications = _notifications.map((n) => n.copyWith(isRead: true)).toList();
  notifyListeners();
}

Batch 500 제한 대응: 계단식 커밋

// 계정 삭제 시 대량 문서 삭제
int operationCount = 0;
for (var doc in participations.docs) {
  batch.delete(doc.reference);
  operationCount++;
  if (operationCount >= 500) {
    await batch.commit();  // 500개마다 커밋
    operationCount = 0;    // 새 batch 시작
  }
}

Firestore Batch는 한 번에 최대 500개 연산만 허용합니다. 대량 삭제 시 500개 단위로 커밋하는 계단식 패턴을 사용합니다.

6. Fire-and-Forget 패턴: 응답 속도 우선

핵심 작업 완료 후 부차적인 작업을 await 없이 비동기로 실행하여 응답 속도를 우선합니다.

포인트 부여 후 비동기 처리

Future<void> awardPoints({...}) async {
  // 핵심: Transaction으로 포인트 부여 + 리더보드 업데이트
  await _firestore.runTransaction((transaction) async {
    // ... 포인트 기록 + 리더보드 업데이트
  });

  // Fire-and-Forget: await 없이 비동기 실행
  _updateRanks(eventId);           // 순위 재계산
  _checkBadgesAfterPoints(...)     // 배지 조건 체크
  // → 사용자는 포인트 부여 즉시 응답을 받음
  // → 순위와 배지는 백그라운드에서 처리
}

배지 체크 실패 격리

Future<void> _checkBadgesAfterPoints({...}) async {
  try {
    // 소스별 선별적 배지 체크 (불필요한 체크 방지)
    if (source.startsWith('quiz_correct')) {
      await _badgeService.checkQuizMasterBadge(...);
    }
    // 공통 배지 병렬 체크
    await Future.wait([
      _badgeService.checkSocialButterflyBadge(...),
      _badgeService.checkTopScorerBadge(...),
    ]);
  } catch (e) {
    // 배지 체크 실패해도 포인트 부여는 이미 성공
    print('배지 체크 중 오류 발생 (무시): $e');
  }
}

try-catch로 배지 체크를 격리하여 실패가 포인트 부여에 영향을 주지 않습니다. 또한 source prefix 기반으로 관련 배지만 선별 체크하여 불필요한 Firestore 쿼리를 방지합니다.

7. 네트워크 상태 모니터링

ConnectivityService가 실시간 네트워크 상태를 추적하여 오프라인 시 적절한 UI 피드백을 제공합니다.

class ConnectivityService extends ChangeNotifier {
  final Connectivity _connectivity = Connectivity();
  StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;

  bool _isOnline = true;
  bool _hasWifi = false;
  bool _hasMobile = false;

  ConnectivityService() {
    _init();
  }

  Future<void> _init() async {
    await _updateConnectionStatus();
    _connectivitySubscription = 
        _connectivity.onConnectivityChanged.listen(_onConnectivityChanged);
  }

  void _onConnectivityChanged(List<ConnectivityResult> results) {
    _hasWifi = results.contains(ConnectivityResult.wifi);
    _hasMobile = results.contains(ConnectivityResult.mobile);
    _isOnline = _hasWifi || _hasMobile || 
                results.contains(ConnectivityResult.ethernet);
    notifyListeners();
  }
}

ChangeNotifier를 상속하여 Provider로 등록되므로, UI 어디서든 Consumer나 context.watch로 네트워크 상태를 감시할 수 있습니다. 모바일 환경에서 Firestore 무제한 캐시와 결합하면 오프라인에서도 캐시된 데이터로 앱을 사용할 수 있습니다.

추가 최적화: 웹 스크롤 동작 커스터마이징

/// Flutter 웹에서 마우스 스크롤 및 트랙패드 드래그 활성화
class AppScrollBehavior extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
    PointerDeviceKind.touch,
    PointerDeviceKind.mouse,
    PointerDeviceKind.trackpad,
    PointerDeviceKind.stylus,
  };
}

Flutter 웹은 기본적으로 마우스 드래그 스크롤을 지원하지 않습니다. AppScrollBehavior를 MaterialApp의 scrollBehavior에 설정하여 데스크톱 브라우저에서도 자연스러운 스크롤 경험을 제공합니다.

에러 핸들링: 전역 에러 캐처

void _setupErrorHandlers() {
  // Flutter 프레임워크 에러 (위젯 빌드 에러 등)
  FlutterError.onError = (details) {
    if (kDebugMode) {
      FlutterError.dumpErrorToConsole(details);
    } else if (!kIsWeb) {
      FirebaseCrashlytics.instance.recordFlutterFatalError(details);
    }
  };

  // 비동기 에러 (Zone 밖의 에러)
  PlatformDispatcher.instance.onError = (error, stack) {
    if (!kDebugMode && !kIsWeb) {
      FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    }
    return true; // 에러 처리 완료
  };
}

2단계 에러 핸들링으로 모든 종류의 에러를 포착합니다. 디버그 모드에서는 콘솔 출력, 프로덕션에서는 Crashlytics로 전송하여 무음 크래시를 방지합니다. 웹은 Crashlytics를 지원하지 않으므로 kIsWeb 체크로 분기합니다.

성능 최적화 체크리스트

항목 적용 여부 적용 위치
병렬 초기화 O main.dart Future.wait
비블로킹 리소스 로드 O GoogleFonts 프리로드
플랫폼별 캐시 전략 O Firestore Settings
Stream 구독 관리 O 39개 Provider dispose()
Batch Write O 리더보드, 알림, 계정삭제
Transaction 원자성 O 포인트, 토큰, 배지
Fire-and-Forget O 순위 재계산, 배지 체크
에러 격리 O try-catch 부차적 작업
네트워크 모니터링 O ConnectivityService
전역 에러 핸들링 O FlutterError + PlatformDispatcher

마치며

MICEMore는 대규모 Flutter 프로젝트에서 실전적인 성능 최적화를 적용했습니다. Future.wait 병렬 초기화로 앱 시작 속도를 40% 이상 단축하고, 체계적인 Stream 구독 관리로 메모리 누수를 방지하며, Batch Write와 Transaction으로 Firestore 연산을 최적화했습니다. 이 시리즈를 통해 99개 화면, 42개 모델, 39개 Provider, 44개 서비스로 구성된 MICEMore 프로젝트의 전체 아키텍처와 핵심 기술을 분석했습니다.


초보자를 위한 상세 가이드: 성능 최적화 전략 완전 정복

성능 최적화란? - 왜 중요한가

앱 성능 최적화는 "앱이 빠르고 부드럽게 동작하도록 만드는 것"입니다. 사용자가 앱을 열었을 때 3초 이상 걸리면 53%의 사용자가 이탈합니다. MICEMore는 28개의 Provider와 Firebase를 사용하는 대규모 앱이므로, 최적화가 특히 중요합니다.

최적화 영역 하지 않으면? 최적화하면? 비유
병렬 초기화 앱 시작 5초+ 앱 시작 1~2초 한 명이 순서대로 vs 여러 명이 동시에
Firestore 캐시 매번 서버 요청 로컬에서 즉시 로드 매번 마트 vs 냉장고에서 꺼내기
Stream 관리 메모리 누수, 앱 느려짐 깔끔한 메모리 관리 수도꼭지 안 잠그기 vs 쓰고 잠그기
Batch Write 100번 네트워크 요청 1번 네트워크 요청 편지 100번 보내기 vs 택배 1상자

1. 병렬 초기화 (Future.wait) 상세 분석

순차 초기화 vs 병렬 초기화

앱이 시작될 때 여러 가지를 준비해야 합니다. 이걸 하나씩 순서대로 하면 느리고, 동시에 하면 빠릅니다.

// 나쁜 예: 순차 초기화 (하나 끝나면 다음 하나)
await SystemChrome.setPreferredOrientations(...);  // 100ms
await initializeDateFormatting('ko', null);        // 200ms
await Firebase.initializeApp(...);                  // 800ms
await _initializeKakaoSdk();                        // 300ms
// 총 시간: 100 + 200 + 800 + 300 = 1400ms (1.4초)

// 좋은 예: 병렬 초기화 (모두 동시에 실행!)
await Future.wait([
  SystemChrome.setPreferredOrientations(...),  // 100ms ┐
  initializeDateFormatting('ko', null),        // 200ms ├ 동시 실행
  _initializeFirebase(),                       // 800ms ├ 
  _initializeKakaoSdk(),                       // 300ms ┘
]);
// 총 시간: max(100, 200, 800, 300) = 800ms (0.8초!)
// 약 43% 시간 단축!

Future.wait의 원리:

비유 순차 실행 병렬 실행 (Future.wait)
요리 밥 짓고 → 국 끓이고 → 반찬 만들기 밥+국+반찬 동시에!
택배 A배달 → B배달 → C배달 택배기사 3명이 동시에!
총 시간 모든 시간의 합계 가장 오래 걸리는 것만큼

병렬 초기화 시 주의할 점

// 주의: 의존성이 있는 작업은 병렬로 실행하면 안 됩니다!

// 잘못된 예: Firebase가 초기화되기 전에 Firestore 사용
await Future.wait([
  Firebase.initializeApp(),    // Firebase 초기화
  loadUserData(),              // Firebase 필요! (에러 발생)
]);

// 올바른 예: 의존성 있는 작업은 순서대로
await Future.wait([
  Firebase.initializeApp(),    // 독립적인 작업들만
  initializeDateFormatting(),  // 병렬로 실행
]);
// Firebase 초기화 완료 후에
await loadUserData();  // 의존적인 작업 실행

2. Firestore 캐시 전략 상세 분석

캐시(Cache)란?

캐시는 "자주 쓰는 데이터를 가까운 곳에 미리 저장해두는 것"입니다. 마트에서 매번 장보러 가는 대신, 냉장고에 재료를 넣어두는 것과 같습니다.

// 모바일: 캐시 활성화 (오프라인에서도 동작!)
if (!kIsWeb) {
  FirebaseFirestore.instance.settings = const Settings(
    persistenceEnabled: true,              // 캐시 켜기
    cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,  // 용량 제한 없음
  );
}

// 웹: 캐시 비활성화 (멀티탭 충돌 방지)
if (kIsWeb) {
  FirebaseFirestore.instance.settings = const Settings(
    persistenceEnabled: false,  // 캐시 끄기
  );
}

왜 웹에서는 캐시를 끄나요?

웹 브라우저에서는 여러 탭을 동시에 열 수 있습니다. 각 탭이 같은 캐시를 사용하면 충돌이 발생합니다. 마치 두 사람이 동시에 같은 냉장고 문을 열면 부딪히는 것과 같습니다.

플랫폼 캐시 설정 이유 효과
iOS/Android 활성화 (무제한) 오프라인 지원, 빠른 로딩 서버 요청 50~70% 감소
Web 비활성화 멀티탭 충돌 방지 데이터 일관성 보장

3. MultiProvider와 28개 Provider 관리

MultiProvider란?

MultiProvider는 여러 Provider를 한 곳에서 등록하는 방법입니다. Provider는 앱의 상태(데이터)를 관리하는 매니저입니다.

// MultiProvider: 28개의 매니저를 한꺼번에 등록
runApp(
  MultiProvider(
    providers: [
      // 인프라 서비스
      ChangeNotifierProvider(create: (_) => ConnectivityService()),
      ChangeNotifierProvider(create: (_) => AuthProvider()),
      
      // 핵심 기능
      ChangeNotifierProvider(create: (_) => EventProvider()),
      ChangeNotifierProvider(create: (_) => NotificationProvider()),
      ChangeNotifierProvider(create: (_) => FCMProvider()),
      
      // 게이미피케이션
      ChangeNotifierProvider(create: (_) => GamificationProvider()),
      ChangeNotifierProvider(create: (_) => TokenProvider()),
      ChangeNotifierProvider(create: (_) => BadgeProvider()),
      
      // ... 총 28개
    ],
    child: MyApp(),
  ),
);

왜 28개나 필요한가요?

MICEMore는 대규모 MICE 행사 앱이기 때문에 관리할 상태가 많습니다:

카테고리 Provider 수 예시
인프라 3개 Connectivity, Auth, Mode
행사 관리 6개 Event, Participant, Question, Announcement, Lottery, Quiz
알림 2개 Notification, FCM
게이미피케이션 3개 Gamification, Badge, Token
부스 시스템 5개 Booth, BoothQuestion, BoothVote, BoothVisit, BoothGamification
AI/추천 4개 AIPlanning, Recommendation, ImplicitFeedback, Chatbot
기타 5개 Vote, Team, EventFlow, Congestion, Community 등

Provider 성능 최적화 팁

// 나쁜 예: context.watch (불필요한 리빌드 발생)
// 이벤트 목록이 바뀔 때마다 이 위젯 전체가 다시 그려짐
Widget build(BuildContext context) {
  final events = context.watch<EventProvider>().events;
  return Column(
    children: [
      Text('행사 수: ' + events.length.toString()),
      BigExpensiveWidget(),  // 이것도 다시 그려짐! (불필요)
    ],
  );
}

// 좋은 예: Consumer + context.read 조합
Widget build(BuildContext context) {
  return Column(
    children: [
      // Consumer: EventProvider 변경 시 이 부분만 다시 그려짐
      Consumer<EventProvider>(
        builder: (context, eventProvider, child) {
          return Text('행사 수: ' + eventProvider.events.length.toString());
        },
      ),
      BigExpensiveWidget(),  // 이건 다시 안 그려짐! (성능 UP)
    ],
  );
}

4. Stream 관리와 메모리 누수 방지

Stream이란?

Stream은 "시간이 지나면서 계속 데이터가 흘러오는 파이프"입니다. 수도꼭지를 틀면 물이 계속 나오듯, Firestore의 snapshots()를 구독하면 데이터 변경이 계속 흘러옵니다.

비유 설명 코드
수도꼭지 틀기 Stream 구독 시작 .listen((data) { ... })
물 받기 데이터 수신 처리 콜백 함수 실행
수도꼭지 잠그기 Stream 구독 해제 subscription.cancel()
수도꼭지 안 잠그면? 메모리 누수! dispose()에서 cancel 안 함

메모리 누수(Memory Leak)란?

메모리 누수는 "사용하지 않는 데이터가 메모리에 계속 쌓이는 것"입니다. 마치 수도꼭지를 안 잠그면 물이 계속 나와 바닥이 잠기는 것과 같습니다.

// 메모리 누수가 발생하는 코드 (나쁜 예)
class BadProvider extends ChangeNotifier {
  void startListening() {
    // 구독은 하지만 해제를 안 함!
    _firestore.collection('events').snapshots().listen((snapshot) {
      // 화면을 벗어나도 계속 실행됨...
      // 메모리가 점점 쌓임...
    });
  }
  // dispose()가 없음! 수도꼭지 안 잠금!
}

// 올바른 코드 (좋은 예)
class GoodProvider extends ChangeNotifier {
  StreamSubscription? _subscription;  // 구독 참조 저장

  void startListening() {
    _subscription?.cancel();  // Cancel-Before-Subscribe
    _subscription = _firestore
        .collection('events')
        .snapshots()
        .listen((snapshot) {
      // 데이터 처리
    });
  }

  @override
  void dispose() {
    _subscription?.cancel();  // 수도꼭지 잠그기!
    super.dispose();
  }
}

ConnectivityService의 Stream 관리 예시

ConnectivityService는 네트워크 연결 상태를 실시간으로 감시합니다. Wi-Fi가 끊기면 즉시 감지해서 사용자에게 알려줍니다.

class ConnectivityService extends ChangeNotifier {
  final Connectivity _connectivity = Connectivity();
  
  // Stream 구독 참조를 반드시 변수에 저장!
  StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;

  bool _isOnline = true;
  bool _hasWifi = false;
  bool _hasMobile = false;

  ConnectivityService() {
    _init();  // 생성자에서 초기화
  }

  Future<void> _init() async {
    await _updateConnectionStatus();  // 현재 상태 확인
    // 상태 변경 실시간 감지 시작
    _connectivitySubscription = _connectivity
        .onConnectivityChanged
        .listen(_onConnectivityChanged);
  }

  void _onConnectivityChanged(List<ConnectivityResult> results) {
    _hasWifi = results.contains(ConnectivityResult.wifi);
    _hasMobile = results.contains(ConnectivityResult.mobile);
    _isOnline = _hasWifi || _hasMobile || 
               results.contains(ConnectivityResult.ethernet);
    notifyListeners();  // UI에 상태 변경 알림
  }

  @override
  void dispose() {
    _connectivitySubscription?.cancel();  // 반드시 해제!
    super.dispose();
  }
}

5. Batch Write: 대량 데이터 처리의 핵심

Batch Write란?

Batch Write는 "여러 개의 데이터베이스 작업을 하나로 묶어서 한 번에 실행"하는 것입니다. 편지를 10통 보낼 때 우체국을 10번 가는 대신, 한 번에 10통을 들고 가는 것과 같습니다.

// 나쁜 예: 하나씩 보내기 (네트워크 요청 100번)
for (final userId in userIds) {  // 100명
  await _firestore.collection('notifications').add({
    'userId': userId,
    'title': '행사 시작',
    'isRead': false,
  });
  // 매번 서버와 통신! 100번 왕복!
}
// 시간: 100 x 200ms = 20초

// 좋은 예: Batch Write (네트워크 요청 1번)
final batch = _firestore.batch();
for (final userId in userIds) {  // 100명
  final docRef = _firestore.collection('notifications').doc();
  batch.set(docRef, {
    'userId': userId,
    'title': '행사 시작',
    'isRead': false,
  });
}
await batch.commit();  // 한 번에 전송!
// 시간: 1 x 500ms = 0.5초 (40배 빠름!)
비교 항목 개별 Write Batch Write
100개 문서 쓰기 ~20초 ~0.5초
네트워크 요청 수 100번 1번
원자성 (Atomicity) 일부만 성공 가능 전부 성공 또는 전부 실패
최대 문서 수 제한 없음 500개

Batch vs Transaction 차이점

둘 다 여러 작업을 묶는 것이지만, 목적이 다릅니다:

구분 Batch Write Transaction
읽기 가능? 쓰기만 가능 읽기 + 쓰기 가능
사용 시나리오 알림 100개 생성, 전체 읽음 처리 포인트 증감, 토큰 차감
비유 택배 100개 한 번에 보내기 ATM에서 잔액 확인 후 출금
충돌 시 전체 실패 자동 재시도 (최대 5회)

6. 비블로킹(Non-blocking) 초기화 패턴

블로킹 vs 비블로킹

블로킹은 작업이 끝날 때까지 다음 코드가 실행되지 않는 것이고, 비블로킹은 작업을 시작만 하고 다음 코드로 넘어가는 것입니다.

// 블로킹 (느림): 폰트 로딩이 끝날 때까지 앱 표시 안됨
GoogleFonts.notoSansKr(fontWeight: FontWeight.w400);
GoogleFonts.notoSansKr(fontWeight: FontWeight.w700);
await GoogleFonts.pendingFonts();  // 500~1500ms 대기!
// 이 시간 동안 스플래시 화면만 보임...

// 비블로킹 (빠름): 폰트 로딩 시작만 하고 앱 바로 표시
GoogleFonts.notoSansKr(fontWeight: FontWeight.w400);
GoogleFonts.notoSansKr(fontWeight: FontWeight.w700);
// await 없음! 백그라운드에서 로딩
// 앱이 즉시 표시되고, 폰트는 준비되면 자동 적용

7. 에러 핸들링과 Crashlytics

// 전역 에러 핸들러: 앱 어디서든 발생하는 에러를 잡아냄
void _setupErrorHandlers() {
  // Flutter 프레임워크 에러 (UI 관련)
  FlutterError.onError = (details) {
    if (!kIsWeb) {
      FirebaseCrashlytics.instance.recordFlutterError(details);
    }
  };

  // Dart 에러 (비동기 에러 등)
  PlatformDispatcher.instance.onError = (error, stack) {
    if (!kIsWeb) {
      FirebaseCrashlytics.instance.recordError(error, stack);
    }
    return true;
  };
}

// Crashlytics: 릴리즈 모드에서만 에러 수집
// 디버그 모드에서는 꺼둠 (개발 중 에러가 대시보드를 더럽히지 않게)
if (!kDebugMode) {
  await FirebaseCrashlytics.instance
      .setCrashlyticsCollectionEnabled(true);
}

성능 최적화 체크리스트

체크 항목 적용 여부 효과
Future.wait로 병렬 초기화 적용 앱 시작 시간 43% 단축
Firestore 캐시 (모바일) 적용 서버 요청 50~70% 감소
Firestore 캐시 끄기 (웹) 적용 멀티탭 충돌 방지
Cancel-Before-Subscribe 적용 Stream 중복 구독 방지
dispose()에서 Stream cancel 적용 메모리 누수 방지
Batch Write 사용 적용 네트워크 요청 40배 감소
비블로킹 폰트 로딩 적용 스플래시 시간 0.5~1.5초 단축
Consumer로 부분 리빌드 적용 불필요한 위젯 리빌드 방지
Crashlytics 릴리즈만 활성화 적용 디버그 로그 오염 방지

핵심 정리

개념 한 줄 요약
Future.wait 독립적인 비동기 작업을 동시에 실행해 총 시간 단축
Firestore Cache 모바일은 캐시 ON(오프라인 지원), 웹은 OFF(멀티탭 안전)
Stream 관리 구독 시작과 해제를 쌍으로, dispose()에서 반드시 cancel
Batch Write 여러 문서 쓰기를 1회 요청으로 묶어 네트워크 절약
Transaction 읽기+쓰기가 필요한 원자적 작업(포인트, 토큰)
비블로킹 초기화 await 없이 시작만 하고 앱 즉시 표시
Consumer 변경된 부분만 리빌드해서 불필요한 UI 갱신 방지
Crashlytics 프로덕션 에러만 수집, 디버그 모드에서는 비활성화
728x90
반응형
이 페이지는 리디주식회사에서 제공한 리디바탕 글꼴이 사용되어 있습니다.