Flutter 튜토리얼 49편: 에러 처리와 보고
왜 에러 처리가 중요한가요?
Flutter 앱을 개발하다 보면 예상치 못한 에러가 발생할 수 있습니다. 사용자가 앱을 사용하는 도중 에러가 발생하면 불쾌한 경험을 하게 되고, 이는 앱 평점 하락으로 이어질 수 있습니다. 체계적인 에러 처리와 보고 시스템을 구축하면 문제를 빠르게 파악하고 해결할 수 있습니다.
무엇을 배우나요?
이 튜토리얼에서는 다음 내용을 학습합니다.
- Flutter에서 발생하는 일반적인 에러 유형과 해결 방법
- FlutterError.onError를 사용한 프레임워크 에러 처리
- PlatformDispatcher.onError를 사용한 비동기 에러 처리
- 커스텀 에러 위젯 만들기
- Sentry 등 외부 서비스에 에러 보고하기
어떻게 구현하나요?
1. Flutter의 일반적인 에러 유형
빨간색/회색 에러 화면
앱 실행 중 빨간색(디버그/프로필 모드) 또는 회색(릴리즈 모드) 화면이 나타날 수 있습니다. 이는 주로 처리되지 않은 예외 또는 렌더링 에러로 인해 발생합니다.
RenderFlex 오버플로우
가장 흔한 레이아웃 에러 중 하나입니다. 노란색과 검은색 줄무늬로 오버플로우 영역이 표시됩니다.
// 문제가 되는 코드Widget build(BuildContext context) { return Row( children: [ Icon(Icons.message), Column( children: [ Text('제목'), Text('매우 긴 텍스트가 화면을 넘어갑니다...'), ], ), ], );}해결 방법: Expanded 또는 Flexible 위젯으로 감싸주세요.
Widget build(BuildContext context) { return Row( children: [ Icon(Icons.message), Expanded( // Expanded로 감싸기 child: Column( children: [ Text('제목'), Text('이제 텍스트가 넘치지 않습니다.'), ], ), ), ], );}Vertical viewport was given unbounded height
ListView1를 Column 안에 넣을 때 자주 발생합니다.
// 문제가 되는 코드Widget build(BuildContext context) { return Column( children: [ Text('헤더'), ListView( // Column이 높이 제약을 주지 않음 children: [ ListTile(title: Text('항목 1')), ListTile(title: Text('항목 2')), ], ), ], );}해결 방법: Expanded로 ListView를 감싸주세요.
Widget build(BuildContext context) { return Column( children: [ Text('헤더'), Expanded( // 남은 공간을 ListView가 차지 child: ListView( children: [ ListTile(title: Text('항목 1')), ListTile(title: Text('항목 2')), ], ), ), ], );}2. Flutter가 잡는 에러 처리
Flutter 프레임워크는 build, layout, paint 단계에서 발생하는 에러를 자동으로 잡습니다.
이러한 에러는 FlutterError.onError로 라우팅됩니다.
import 'dart:io';import 'package:flutter/foundation.dart';import 'package:flutter/material.dart';
void main() { // Flutter 프레임워크 에러 처리 FlutterError.onError = (FlutterErrorDetails details) { // 콘솔에 에러 출력 FlutterError.presentError(details);
// 릴리즈 모드에서는 앱 종료 (선택적) if (kReleaseMode) { exit(1); } };
runApp(const MyApp());}FlutterError.presentError
FlutterError.presentError를 호출하면 IDE 콘솔에서 에러 로그를 확인할 수 있습니다. 커스텀 에러 핸들러를 사용할 때도 이 함수를 호출하는 것이 좋습니다.
3. Flutter가 잡지 못하는 에러 처리
비동기 콜백에서 발생하는 에러는 FlutterError.onError로 전달되지 않습니다.
대신 PlatformDispatcher.instance.onError를 사용해야 합니다.
import 'dart:ui';import 'package:flutter/material.dart';
void main() { // Flutter가 잡지 못하는 에러 처리 PlatformDispatcher.instance.onError = (error, stack) { // 에러 로깅 또는 보고 debugPrint('비동기 에러 발생: $error'); debugPrint('스택 트레이스: $stack');
// true를 반환하면 에러가 처리된 것으로 간주 return true; };
runApp(const MyApp());}예시: 플러그인 채널 호출에서의 에러
OutlinedButton( child: const Text('버튼 클릭'), onPressed: () async { // 이 에러는 FlutterError.onError로 가지 않음 const channel = MethodChannel('my-channel'); await channel.invokeMethod('someMethod'); // 에러 발생 시 PlatformDispatcher.onError로 전달됨 },)4. 커스텀 에러 위젯 만들기
빌드 단계에서 에러가 발생하면 기본적으로 빨간색 에러 화면이 표시됩니다.
ErrorWidget.builder를 사용하여 사용자 친화적인 에러 화면을 만들 수 있습니다.
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( builder: (context, widget) { // 커스텀 에러 위젯 정의 Widget error = const Text('문제가 발생했습니다.');
if (widget is Scaffold || widget is Navigator) { error = Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red), SizedBox(height: 16), Text('앗! 문제가 발생했어요.'), SizedBox(height: 8), Text('잠시 후 다시 시도해 주세요.'), ], ), ), ); }
// 에러 위젯 빌더 설정 ErrorWidget.builder = (errorDetails) => error;
if (widget != null) return widget; throw StateError('위젯이 null입니다'); }, ); }}릴리즈 모드에서의 에러 화면디버그 모드에서는 상세한 에러 정보가 도움이 되지만, 릴리즈 모드에서는 사용자 친화적인 메시지를 보여주는 것이 좋습니다.
kReleaseMode상수를 활용하여 모드별로 다른 화면을 표시할 수 있습니다.
5. 모든 에러 종합 처리
모든 유형의 에러를 한 곳에서 처리하는 완전한 예제입니다.
import 'dart:ui';import 'package:flutter/foundation.dart';import 'package:flutter/material.dart';
Future<void> main() async { // 에러 핸들러 초기화 await myErrorsHandler.initialize();
// Flutter 프레임워크 에러 처리 FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); myErrorsHandler.onErrorDetails(details); };
// 비동기 에러 처리 PlatformDispatcher.instance.onError = (error, stack) { myErrorsHandler.onError(error, stack); return true; };
runApp(const MyApp());}
// 에러 핸들러 클래스class MyErrorsHandler { Future<void> initialize() async { // 에러 리포팅 서비스 초기화 }
void onErrorDetails(FlutterErrorDetails details) { // Flutter 에러 보고 debugPrint('Flutter 에러: ${details.exception}'); }
void onError(Object error, StackTrace stack) { // 일반 에러 보고 debugPrint('에러: $error'); debugPrint('스택: $stack'); }}
final myErrorsHandler = MyErrorsHandler();6. Sentry를 사용한 에러 보고
실제 프로덕션 앱에서는 에러를 외부 서비스에 보고하여 모니터링해야 합니다. Sentry는 가장 널리 사용되는 에러 추적 서비스 중 하나입니다.
패키지 설치
flutter pub add sentry_flutterSentry 초기화
import 'package:flutter/widgets.dart';import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async { await SentryFlutter.init( (options) { // 추가 옵션 설정 options.tracesSampleRate = 1.0; // 성능 모니터링 options.debug = kDebugMode; // 디버그 모드에서 로깅 }, appRunner: () => runApp(const MyApp()), );}DSN 보안DSN(Data Source Name)은 민감한 정보입니다. 소스 코드에 직접 포함하지 말고, 환경 변수나
--dart-define을 사용하세요.
# 빌드 시 DSN 전달// 코드에서 환경 변수 사용const sentryDsn = String.fromEnvironment('SENTRY_DSN');
await SentryFlutter.init( (options) => options.dsn = sentryDsn, appRunner: () => runApp(const MyApp()),);프로그래밍 방식 에러 보고
자동 에러 수집 외에도 수동으로 에러를 보고할 수 있습니다.
try { // 위험한 작업 await riskyOperation();} catch (exception, stackTrace) { // Sentry에 에러 보고 await Sentry.captureException( exception, stackTrace: stackTrace, );
// 사용자에게 에러 알림 showErrorSnackBar('작업 중 문제가 발생했습니다.');}사용자 컨텍스트 추가
에러와 함께 사용자 정보를 보내면 디버깅에 도움이 됩니다.
// 사용자 정보 설정Sentry.configureScope((scope) { scope.setUser(SentryUser( id: userId, email: userEmail, ));
// 추가 컨텍스트 scope.setTag('app_version', '1.0.0'); scope.setExtra('device_model', deviceModel);});7. 다른 에러 추적 서비스
Sentry 외에도 다양한 에러 추적 서비스를 사용할 수 있습니다.
| 서비스 | 특징 |
|---|---|
| Firebase Crashlytics | Google 서비스와 긴밀한 통합, 무료 |
| Bugsnag | 상세한 에러 분석, 다양한 플랫폼 지원 |
| Datadog | APM 및 로그 통합, 실시간 모니터링 |
| Rollbar | 코드 배포와 에러 연관 분석 |
Firebase Crashlytics 예시
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp();
// Flutter 에러를 Crashlytics로 전달 FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
// 비동기 에러 처리 PlatformDispatcher.instance.onError = (error, stack) { FirebaseCrashlytics.instance.recordError(error, stack); return true; };
runApp(const MyApp());}8. 실전 에러 처리 패턴
Result 패턴 사용
// Result 타입 정의sealed class Result<T> {}class Success<T> extends Result<T> { final T data; Success(this.data);}class Failure<T> extends Result<T> { final String message; final Object? error; Failure(this.message, [this.error]);}
// 사용 예시Future<Result<User>> fetchUser(String id) async { try { final response = await api.getUser(id); return Success(User.fromJson(response)); } catch (e, stack) { await Sentry.captureException(e, stackTrace: stack); return Failure('사용자 정보를 불러올 수 없습니다.', e); }}
// UI에서 사용final result = await fetchUser('123');switch (result) { case Success(:final data): showUserProfile(data); case Failure(:final message): showErrorMessage(message);}ErrorBoundary 위젯
class ErrorBoundary extends StatefulWidget { final Widget child; final Widget Function(Object error)? fallback;
const ErrorBoundary({ required this.child, this.fallback, super.key, });
@override State<ErrorBoundary> createState() => _ErrorBoundaryState();}
class _ErrorBoundaryState extends State<ErrorBoundary> { Object? _error;
@override void initState() { super.initState(); // 에러 발생 시 재빌드 }
void _handleError(Object error) { setState(() => _error = error); }
@override Widget build(BuildContext context) { if (_error != null) { return widget.fallback?.call(_error!) ?? Center(child: Text('오류가 발생했습니다.')); } return widget.child; }}주의하세요!
build 메서드에서 setState 호출 금지
build메서드 내에서setState나showDialog를 호출하면 안 됩니다. 이는 “setState called during build” 에러를 발생시킵니다.// 잘못된 코드Widget build(BuildContext context) {showDialog(...); // build 중에 호출하면 안 됨!return Container();}// 올바른 코드 - 버튼 콜백에서 호출ElevatedButton(onPressed: () => showDialog(...),child: Text('다이얼로그 열기'),)
에러 정보 노출 주의릴리즈 모드에서 사용자에게 상세한 에러 정보(스택 트레이스 등)를 보여주지 마세요. 보안 취약점이 될 수 있으며, 사용자 경험도 좋지 않습니다.
마무리
Flutter 앱의 에러 처리는 사용자 경험과 앱 품질에 직접적인 영향을 미칩니다.
FlutterError.onError와 PlatformDispatcher.onError를 적절히 활용하고, Sentry나 Firebase Crashlytics 같은 서비스를 통해 에러를 추적하면 문제를 빠르게 발견하고 해결할 수 있습니다.
커스텀 에러 위젯을 만들어 사용자에게 친절한 에러 화면을 보여주는 것도 잊지 마세요!
참고 자료
Footnotes
-
ListView: 스크롤 가능한 리스트를 표시하는 위젯입니다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!