Dart 에러처리
이 문서 주변 탐색
주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.
시리즈 흐름
이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.
관련 문서
같이 읽을 만한 관련 문서가 아직 없습니다.
이 문서를 참조하는 문서
이 문서를 참조하는 다른 문서가 아직 없습니다.
1. 에러 처리의 중요성
에러 처리가 없으면?
void main() {
// ❌ 에러 처리 없음 - 프로그램 크래시!
int result = int.parse('abc'); // FormatException 발생
print(result); // 여기 도달 못함
print('프로그램 계속...'); // 실행 안 됨
}에러 처리가 있으면?
void main() {
// ✅ 에러 처리 - 프로그램 계속 실행
try {
int result = int.parse('abc');
print(result);
} catch (e) {
print('숫자 변환 실패: $e');
}
print('프로그램 계속...'); // 정상 실행!
}에러 처리는 프로그램의 안정성을 보장함
사용자에게 친절한 에러 메시지를 제공할 수 있음
문제 발생 시 로깅과 디버깅이 가능해짐
Flutter 앱이 크래시되면 사용자 경험이 크게 저하됨
2. Exception vs Error
두 가지 유형의 차이
구분 | Exception | Error |
|---|---|---|
성격 | 예상 가능한 문제 | 프로그래밍 실수 |
처리 | 잡아서 처리 | 잡지 말고 수정 |
예시 | 네트워크 실패, 잘못된 입력 | null 참조, 범위 초과 |
Exception 예시
void main() {
// FormatException - 예상 가능, 처리해야 함
try {
int.parse('hello');
} on FormatException catch (e) {
print('숫자가 아님: $e');
}
// IOException - 예상 가능, 처리해야 함
try {
// 파일 읽기 실패 가능
} on IOException catch (e) {
print('파일 읽기 실패: $e');
}
}Error 예시 (잡지 말 것!)
void main() {
var list = [1, 2, 3];
// RangeError - 프로그래밍 실수, 코드를 수정해야 함
// print(list[10]); // ❌ 에러! 인덱스 범위 초과
// TypeError - 프로그래밍 실수
// dynamic value = 'hello';
// int number = value; // ❌ 타입 불일치
// StackOverflowError - 무한 재귀
// void infinite() => infinite(); // ❌ 스택 오버플로우
}내장 Exception 종류
// 자주 만나는 Exception들
FormatException // 형식 오류 (숫자 파싱 실패 등)
IOException // 입출력 오류
TimeoutException // 시간 초과
HttpException // HTTP 오류
StateError // 잘못된 상태에서 호출
ArgumentError // 잘못된 인자
// 자주 만나는 Error들 (잡지 말 것!)
RangeError // 범위 초과
TypeError // 타입 불일치
NullThrownError // null 던짐
StackOverflowError // 스택 오버플로우
OutOfMemoryError // 메모리 부족Exception: "이런 상황이 발생할 수 있어" → 처리 코드 작성
Error: "코드가 잘못됐어" → 코드 자체를 수정
실무에서 Error를 catch하는 것은 안티패턴임
Exception도 무분별하게 잡지 말고, 구체적인 타입을 지정할 것
3. try-catch-finally
기본 구조
void main() {
try {
// 에러가 발생할 수 있는 코드
var result = riskyOperation();
print('결과: $result');
} catch (e) {
// 에러 발생 시 실행
print('에러 발생: $e');
} finally {
// 항상 실행 (에러 여부 상관없이)
print('정리 작업');
}
}on 키워드로 특정 예외 처리
void main() {
try {
var value = int.parse('hello');
} on FormatException {
// FormatException만 처리
print('숫자 형식이 아님');
} on ArgumentError {
// ArgumentError만 처리
print('잘못된 인자임');
}
}catch로 예외 객체 받기
void main() {
try {
var value = int.parse('hello');
} on FormatException catch (e) {
// e: 예외 객체
print('에러 메시지: ${e.message}');
}
}스택 트레이스 받기
void main() {
try {
throw Exception('문제 발생!');
} catch (e, stackTrace) {
// e: 예외 객체
// stackTrace: 호출 스택 정보
print('에러: $e');
print('스택 트레이스:\n$stackTrace');
}
}여러 예외 타입 처리
void main() {
try {
// 여러 종류의 에러가 발생할 수 있는 코드
processData();
} on FormatException catch (e) {
// 형식 에러
print('형식 오류: $e');
} on IOException catch (e) {
// 입출력 에러
print('I/O 오류: $e');
} on Exception catch (e) {
// 그 외 모든 Exception
print('기타 예외: $e');
} catch (e) {
// Exception이 아닌 것까지 포함 (권장하지 않음)
print('알 수 없는 에러: $e');
}
}finally 활용
void main() {
var file = openFile('data.txt');
try {
var content = file.readAll();
processContent(content);
} catch (e) {
print('에러: $e');
} finally {
// 에러가 발생해도 파일은 반드시 닫아야 함
file.close();
print('파일 닫힘');
}
}on은 예외 타입 지정,catch는 예외 객체 받기임on Type catch (e)로 둘을 조합할 수 있음catch (e, s)로 스택 트레이스도 받을 수 있음finally는 리소스 정리(파일, 연결 닫기)에 필수임순서: 구체적인 예외 → 일반적인 예외 (위에서 아래로)
4. throw와 rethrow
throw - 예외 던지기
void validateAge(int age) {
if (age < 0) {
throw ArgumentError('나이는 음수일 수 없음: $age');
}
if (age > 150) {
throw ArgumentError('나이가 너무 큼: $age');
}
}
void main() {
try {
validateAge(-5);
} on ArgumentError catch (e) {
print('유효성 검사 실패: $e');
}
}아무거나 던질 수 있음 (권장하지 않음)
void main() {
// 문자열도 던질 수 있음 (❌ 권장하지 않음)
// throw '에러야!';
// 숫자도 던질 수 있음 (❌ 권장하지 않음)
// throw 404;
// ✅ Exception이나 Error를 상속한 객체를 던질 것
throw FormatException('잘못된 형식');
}rethrow - 예외 다시 던지기
void processData() {
try {
// 데이터 처리
riskyOperation();
} catch (e) {
// 로깅만 하고
print('에러 발생, 로깅: $e');
// 상위로 다시 던짐
rethrow;
}
}
void main() {
try {
processData();
} catch (e) {
// 여기서 최종 처리
print('최종 에러 처리: $e');
}
}throw vs rethrow
void example() {
try {
throw Exception('원본 에러');
} catch (e) {
// throw e; // ❌ 스택 트레이스가 여기서 새로 시작
// throw Exception('새 에러'); // ❌ 완전히 다른 에러
rethrow; // ✅ 원본 스택 트레이스 유지
}
}throw는 새로운 예외를 던짐rethrow는 잡은 예외를 원본 스택 트레이스 그대로 다시 던짐로깅 후 상위로 전파할 때는 항상
rethrow를 사용할 것throw e는 스택 트레이스가 손실되므로 피해야 함
5. 커스텀 Exception
기본 커스텀 Exception
// Exception 인터페이스 구현
class ValidationException implements Exception {
final String message;
ValidationException(this.message);
@override
String toString() => 'ValidationException: $message';
}
void validateEmail(String email) {
if (!email.contains('@')) {
throw ValidationException('유효하지 않은 이메일: $email');
}
}
void main() {
try {
validateEmail('invalid-email');
} on ValidationException catch (e) {
print(e); // ValidationException: 유효하지 않은 이메일: invalid-email
}
}에러 코드가 있는 Exception
class ApiException implements Exception {
final int statusCode;
final String message;
final String? errorCode;
ApiException({
required this.statusCode,
required this.message,
this.errorCode,
});
@override
String toString() => 'ApiException($statusCode): $message';
// 상태 코드별 팩토리 생성자
factory ApiException.notFound([String? message]) {
return ApiException(
statusCode: 404,
message: message ?? '리소스를 찾을 수 없음',
errorCode: 'NOT_FOUND',
);
}
factory ApiException.unauthorized([String? message]) {
return ApiException(
statusCode: 401,
message: message ?? '인증이 필요함',
errorCode: 'UNAUTHORIZED',
);
}
factory ApiException.serverError([String? message]) {
return ApiException(
statusCode: 500,
message: message ?? '서버 오류가 발생함',
errorCode: 'INTERNAL_ERROR',
);
}
}
// 사용
void main() {
try {
throw ApiException.notFound('사용자를 찾을 수 없음');
} on ApiException catch (e) {
switch (e.statusCode) {
case 401:
print('로그인 필요');
break;
case 404:
print('찾을 수 없음: ${e.message}');
break;
default:
print('에러: ${e.message}');
}
}
}계층적 Exception
// 기본 앱 예외
abstract class AppException implements Exception {
final String message;
final String? code;
final dynamic originalError;
AppException(this.message, {this.code, this.originalError});
@override
String toString() => '$runtimeType: $message';
}
// 네트워크 관련 예외
class NetworkException extends AppException {
NetworkException(super.message, {super.code, super.originalError});
}
// 인증 관련 예외
class AuthException extends AppException {
AuthException(super.message, {super.code, super.originalError});
}
// 유효성 검사 예외
class ValidationException extends AppException {
final Map<String, String>? fieldErrors;
ValidationException(
super.message, {
super.code,
this.fieldErrors,
});
}
// 사용
void main() {
try {
throw AuthException('토큰이 만료됨', code: 'TOKEN_EXPIRED');
} on AuthException catch (e) {
print('인증 에러: ${e.message}');
// 로그인 화면으로 이동
} on NetworkException catch (e) {
print('네트워크 에러: ${e.message}');
// 재시도 버튼 표시
} on AppException catch (e) {
print('앱 에러: ${e.message}');
// 일반 에러 처리
}
}implements Exception으로 Exception 인터페이스를 구현함toString()을 오버라이드하면 디버깅이 쉬워짐계층적 Exception을 만들면 공통 처리와 개별 처리를 분리할 수 있음
팩토리 생성자로 자주 쓰는 예외를 편리하게 만들 수 있음
6. assert (디버깅용)
assert 기본
void processAge(int age) {
// 디버그 모드에서만 체크 (릴리즈에서는 무시됨)
assert(age >= 0, '나이는 음수일 수 없음');
assert(age < 150, '나이가 너무 큼');
print('나이 처리: $age');
}
void main() {
processAge(25); // OK
processAge(-5); // 디버그 모드: AssertionError
// 릴리즈 모드: 정상 실행
}assert vs throw
class User {
final String name;
final int age;
User(this.name, this.age) {
// assert: 개발자 실수 방지 (디버그용)
assert(name.isNotEmpty, 'name은 비어있으면 안 됨');
// throw: 실제 유효성 검사 (프로덕션에서도 동작)
if (age < 0) {
throw ArgumentError('age는 음수일 수 없음');
}
}
}구분 | assert | throw |
|---|---|---|
동작 환경 | 디버그 모드만 | 항상 |
목적 | 개발 중 실수 발견 | 실제 에러 처리 |
성능 영향 | 릴리즈에서 없음 | 항상 있음 |
assert 활용 예시
class Rectangle {
final double width;
final double height;
Rectangle(this.width, this.height)
: assert(width > 0, 'width는 양수여야 함'),
assert(height > 0, 'height는 양수여야 함');
}
// 초기화 리스트에서 assert 사용
class Circle {
final double radius;
Circle(this.radius) : assert(radius > 0);
double get area {
assert(radius > 0); // 메서드 내에서도 사용 가능
return 3.14159 * radius * radius;
}
}assert는 디버그 모드에서만 동작함 (flutter run vs flutter run --release)개발 중 실수를 빨리 발견하기 위한 도구임
사용자 입력 검증에는
throw를 사용할 것내부 로직의 불변 조건(invariant) 체크에 적합함
7. Null Safety와 에러 방지
Nullable 타입 안전하게 다루기
void main() {
String? name; // nullable
// ❌ 위험: null이면 런타임 에러
// print(name.length);
// ✅ 방법 1: null 체크
if (name != null) {
print(name.length); // 안전
}
// ✅ 방법 2: ?. (null-aware 접근)
print(name?.length); // null이면 null 반환
// ✅ 방법 3: ?? (기본값)
print(name ?? '기본값');
// ✅ 방법 4: ! (null이 아님을 확신할 때만)
name = 'Hello';
print(name!.length); // 5
}late 키워드 주의점
class UserProfile {
late String name; // 나중에 초기화
void loadProfile() {
// API에서 로드
name = 'John';
}
void printName() {
// ⚠️ loadProfile() 호출 전에 접근하면 LateInitializationError!
print(name);
}
}
void main() {
var profile = UserProfile();
// profile.printName(); // ❌ LateInitializationError
profile.loadProfile();
profile.printName(); // ✅ John
}안전한 타입 캐스팅
void main() {
dynamic value = 'hello';
// ❌ 위험: 타입이 다르면 TypeError
// int number = value as int;
// ✅ 안전한 캐스팅
if (value is int) {
int number = value; // 자동 캐스팅
print(number);
}
// ✅ 또는 null 반환
int? maybeNumber = value is int ? value : null;
}Dart의 Null Safety는 컴파일 타임에 null 에러를 방지함
!는 정말 확신할 때만 사용할 것. 남용하면 런타임 에러의 원인이 됨late는 반드시 초기화될 것이 보장될 때만 사용할 것is체크 후에는 자동으로 타입 캐스팅됨 (smart cast)
8. 비동기 에러 처리
async/await에서 try-catch
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 1));
throw Exception('네트워크 오류');
}
Future<void> main() async {
try {
var data = await fetchData();
print('데이터: $data');
} catch (e) {
print('에러 발생: $e');
}
}Future.then()에서 에러 처리
void main() {
fetchData()
.then((data) => print('데이터: $data'))
.catchError((e) => print('에러: $e'))
.whenComplete(() => print('완료'));
}여러 Future의 에러 처리
Future<void> main() async {
// Future.wait: 하나라도 실패하면 전체 실패
try {
var results = await Future.wait([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
} catch (e) {
print('하나 이상 실패: $e');
}
// 개별 에러 처리가 필요하면
var results = await Future.wait([
fetchUser().catchError((e) => null),
fetchPosts().catchError((e) => []),
fetchComments().catchError((e) => []),
]);
}Stream 에러 처리
void main() {
final stream = Stream.periodic(
Duration(seconds: 1),
(count) {
if (count == 3) throw Exception('3번째에서 에러!');
return count;
},
).take(5);
stream.listen(
(data) => print('데이터: $data'),
onError: (error) => print('에러: $error'),
onDone: () => print('완료'),
cancelOnError: false, // 에러 발생해도 계속 구독
);
}Zone으로 전역 에러 처리
import 'dart:async';
void main() {
runZonedGuarded(
() {
// 이 안에서 발생하는 모든 비동기 에러 캐치
Timer(Duration(seconds: 1), () {
throw Exception('타이머에서 에러!');
});
},
(error, stackTrace) {
print('Zone에서 캐치: $error');
print('스택: $stackTrace');
},
);
}async/await와try/catch조합이 가장 깔끔함Future.catchError의 타입 지정:test: (e) => e is SpecificExceptionStream의
cancelOnError: false로 에러 발생해도 계속 구독할 수 있음runZonedGuarded로 전역 비동기 에러를 캐치할 수 있음
9. 실전 에러 처리 패턴
패턴 1: Result 타입 (Either 패턴)
// 성공 또는 실패를 명시적으로 표현
sealed class Result<T> {
const Result();
}
class Success<T> extends Result<T> {
final T data;
const Success(this.data);
}
class Failure<T> extends Result<T> {
final String message;
final Exception? exception;
const Failure(this.message, [this.exception]);
}
// 사용
Future<Result<User>> fetchUser(String id) async {
try {
final response = await api.getUser(id);
return Success(User.fromJson(response));
} on NetworkException catch (e) {
return Failure('네트워크 오류', e);
} on ApiException catch (e) {
return Failure(e.message, e);
} catch (e) {
return Failure('알 수 없는 오류');
}
}
void main() async {
final result = await fetchUser('123');
switch (result) {
case Success(data: var user):
print('사용자: ${user.name}');
case Failure(message: var msg):
print('에러: $msg');
}
}패턴 2: 에러 경계 (Error Boundary)
class ErrorBoundary {
static Future<T?> guard<T>(
Future<T> Function() action, {
void Function(Object error, StackTrace stack)? onError,
}) async {
try {
return await action();
} catch (e, s) {
onError?.call(e, s);
return null;
}
}
static T? guardSync<T>(
T Function() action, {
void Function(Object error, StackTrace stack)? onError,
}) {
try {
return action();
} catch (e, s) {
onError?.call(e, s);
return null;
}
}
}
// 사용
void main() async {
final user = await ErrorBoundary.guard(
() => fetchUser('123'),
onError: (e, s) => print('에러 로깅: $e'),
);
if (user != null) {
print('사용자: ${user.name}');
} else {
print('사용자를 불러올 수 없음');
}
}패턴 3: 재시도 로직
Future<T> retry<T>(
Future<T> Function() fn, {
int maxAttempts = 3,
Duration delay = const Duration(seconds: 1),
bool Function(Exception)? retryIf,
}) async {
int attempts = 0;
while (true) {
try {
attempts++;
return await fn();
} on Exception catch (e) {
// 재시도 조건 확인
if (retryIf != null && !retryIf(e)) {
rethrow;
}
if (attempts >= maxAttempts) {
rethrow;
}
print('시도 $attempts 실패, $delay 후 재시도...');
await Future.delayed(delay);
// 지수 백오프
delay *= 2;
}
}
}
// 사용
void main() async {
try {
final data = await retry(
() => fetchData(),
maxAttempts: 3,
retryIf: (e) => e is NetworkException, // 네트워크 에러만 재시도
);
print('성공: $data');
} catch (e) {
print('최종 실패: $e');
}
}패턴 4: 에러 로깅 서비스
class ErrorLogger {
static final ErrorLogger _instance = ErrorLogger._internal();
factory ErrorLogger() => _instance;
ErrorLogger._internal();
void log(
Object error,
StackTrace? stackTrace, {
String? context,
Map<String, dynamic>? extra,
}) {
// 콘솔 출력
print('=== ERROR ===');
print('Context: $context');
print('Error: $error');
print('Extra: $extra');
if (stackTrace != null) {
print('Stack:\n$stackTrace');
}
// 프로덕션에서는 Sentry, Crashlytics 등으로 전송
// Sentry.captureException(error, stackTrace: stackTrace);
}
}
// 사용
void main() async {
try {
await fetchData();
} catch (e, s) {
ErrorLogger().log(
e,
s,
context: 'fetchData 호출',
extra: {'userId': '123'},
);
}
}10. Flutter에서의 에러 처리
전역 에러 핸들러
void main() {
// Flutter 프레임워크 에러 처리
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.presentError(details);
// 에러 로깅 서비스로 전송
ErrorLogger().log(
details.exception,
details.stack,
context: 'FlutterError',
);
};
// 비동기 에러 처리
PlatformDispatcher.instance.onError = (error, stack) {
ErrorLogger().log(error, stack, context: 'PlatformDispatcher');
return true; // 에러 처리됨
};
runApp(MyApp());
}ErrorWidget 커스터마이징
void main() {
// 에러 위젯 커스터마이징 (빨간 에러 화면 대신)
ErrorWidget.builder = (FlutterErrorDetails details) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('문제가 발생함'),
TextButton(
onPressed: () {/* 재시도 */},
child: Text('다시 시도'),
),
],
),
),
);
};
runApp(MyApp());
}FutureBuilder/StreamBuilder 에러 처리
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
// 에러 상태
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('에러: ${snapshot.error}'),
ElevatedButton(
onPressed: () {
// setState로 재시도
},
child: Text('다시 시도'),
),
],
),
);
}
// 로딩 상태
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
// 성공 상태
return Text('사용자: ${snapshot.data!.name}');
},
);
}
}본 글은 AI의 도움을 받아 작성되었습니다.
읽기 시리즈
현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.