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#

ListView1Column 안에 넣을 때 자주 발생합니다.

// 문제가 되는 코드
Widget build(BuildContext context) {
return Column(
children: [
Text('헤더'),
ListView( // Column이 높이 제약을 주지 않음
children: [
ListTile(title: Text('항목 1')),
ListTile(title: Text('항목 2')),
],
),
],
);
}

해결 방법: ExpandedListView를 감싸주세요.

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는 가장 널리 사용되는 에러 추적 서비스 중 하나입니다.

패키지 설치#

Terminal window
flutter pub add sentry_flutter

Sentry 초기화#

import 'package:flutter/widgets.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = 'https://[email protected]/project-id';
// 추가 옵션 설정
options.tracesSampleRate = 1.0; // 성능 모니터링
options.debug = kDebugMode; // 디버그 모드에서 로깅
},
appRunner: () => runApp(const MyApp()),
);
}
DSN 보안

DSN(Data Source Name)은 민감한 정보입니다. 소스 코드에 직접 포함하지 말고, 환경 변수나 --dart-define을 사용하세요.

Terminal window
# 빌드 시 DSN 전달
flutter run --dart-define=SENTRY_DSN=https://[email protected]/project-id
// 코드에서 환경 변수 사용
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 CrashlyticsGoogle 서비스와 긴밀한 통합, 무료
Bugsnag상세한 에러 분석, 다양한 플랫폼 지원
DatadogAPM 및 로그 통합, 실시간 모니터링
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 메서드 내에서 setStateshowDialog를 호출하면 안 됩니다. 이는 “setState called during build” 에러를 발생시킵니다.

// 잘못된 코드
Widget build(BuildContext context) {
showDialog(...); // build 중에 호출하면 안 됨!
return Container();
}
// 올바른 코드 - 버튼 콜백에서 호출
ElevatedButton(
onPressed: () => showDialog(...),
child: Text('다이얼로그 열기'),
)
에러 정보 노출 주의

릴리즈 모드에서 사용자에게 상세한 에러 정보(스택 트레이스 등)를 보여주지 마세요. 보안 취약점이 될 수 있으며, 사용자 경험도 좋지 않습니다.

마무리#

Flutter 앱의 에러 처리는 사용자 경험과 앱 품질에 직접적인 영향을 미칩니다. FlutterError.onErrorPlatformDispatcher.onError를 적절히 활용하고, Sentry나 Firebase Crashlytics 같은 서비스를 통해 에러를 추적하면 문제를 빠르게 발견하고 해결할 수 있습니다. 커스텀 에러 위젯을 만들어 사용자에게 친절한 에러 화면을 보여주는 것도 잊지 마세요!

참고 자료#

Footnotes#

  1. ListView: 스크롤 가능한 리스트를 표시하는 위젯입니다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 49편: 에러 처리와 보고
https://moodturnpost.net/posts/flutter/flutter-error-handling/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차