Flutter 앱 개발 중 가장 골치 아픈 문제 중 하나는 앱이 완전히 종료된 상황에서 발생한 크래시를 디버깅하는 것입니다. 일반적인 로깅 방식으로는 앱이 종료되면 로그도 함께 사라지기 때문에, 정확한 크래시 원인을 파악하기 어려웠습니다.
특히 다음과 같은 상황에서 문제가 발생했습니다:
이 문제를 해결하기 위해 파일 기반 크래시 로깅 시스템을 구축했습니다. 핵심 아이디어는 로그를 디바이스의 로컬 파일에 저장하여 앱이 종료되더라도 로그가 유지되도록 하는 것입니다.
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CrashLogger {
static const String _logFileName = 'crash_logs.txt';
static const int _maxLogSize = 1024 * 1024; // 1MB
static Future<void> logToFile(String message, {String? tag}) async {
try {
final directory = await getApplicationDocumentsDirectory();
final logFile = File('${directory.path}/$_logFileName');
final timestamp = DateTime.now().toIso8601String();
final logEntry = '[$timestamp] ${tag != null ? '[$tag] ' : ''}$message\n';
// 파일이 존재하지 않으면 생성
if (!await logFile.exists()) {
await logFile.create(recursive: true);
}
// 파일 크기 체크 및 로테이션
final fileSize = await logFile.length();
if (fileSize > _maxLogSize) {
await _rotateLogFile(logFile);
}
// 로그 추가
await logFile.writeAsString(logEntry, mode: FileMode.append);
// 디버그 모드에서는 콘솔에도 출력
if (kDebugMode) {
print('CrashLogger: $logEntry');
}
} catch (e) {
// 파일 로깅 실패 시 Sentry에 전송
Sentry.captureException('Failed to write crash log: $e');
}
}
static Future<void> _rotateLogFile(File logFile) async {
try {
final backupFile = File('${logFile.path}.backup');
if (await backupFile.exists()) {
await backupFile.delete();
}
await logFile.rename(backupFile.path);
await logFile.create();
} catch (e) {
// 로테이션 실패 시 기존 파일 삭제 후 새로 생성
await logFile.delete();
await logFile.create();
}
}
static Future<void> sendLogsToSentry() async {
try {
final logs = await readLogFile();
if (logs.isNotEmpty && logs != '로그 파일이 존재하지 않습니다.') {
Sentry.addBreadcrumb(
Breadcrumb(
message: 'Previous crash logs',
data: {'logs': logs},
timestamp: DateTime.now(),
level: SentryLevel.info,
),
);
// 로그 전송 후 파일 삭제
await clearLogFile();
}
} catch (e) {
Sentry.captureException('Failed to send logs to Sentry: $e');
}
}
// 일반 로그
await CrashLogger.logToFile('사용자가 로그인했습니다');
// 태그가 있는 로그
await CrashLogger.logToFile('API 호출 실패', tag: 'NETWORK');
// 예외 로그
try {
// 위험한 코드
} catch (e) {
await CrashLogger.logToFile('예외 발생: $e', tag: 'ERROR');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 이전 크래시 로그를 Sentry에 전송
await CrashLogger.sendLogsToSentry();
runApp(MyApp());
}
터미널에서 다음 명령어로 실시간 로그를 확인할 수 있습니다:
flutter logs
class ApiService {
static Future<dynamic> request(String url) async {
try {
final response = await http.get(Uri.parse(url));
await CrashLogger.logToFile('API 요청 성공: $url', tag: 'API');
return response;
} catch (e) {
await CrashLogger.logToFile('API 요청 실패: $url, 에러: $e', tag: 'API_ERROR');
rethrow;
}
}
}
class UserBloc extends Bloc<UserEvent, UserState> {
void onTransition(Transition<UserEvent, UserState> transition) {
super.onTransition(transition);
CrashLogger.logToFile(
'상태 변경: ${transition.currentState} -> ${transition.nextState}',
tag: 'BLOC'
);
}
}
파일 기반 크래시 로깅 시스템을 통해 앱 종료 후에도 디버깅 정보를 확보할 수 있게 되었습니다. 특히 프로덕션 환경에서 발생하는 예측하기 어려운 크래시들을 효과적으로 추적할 수 있어, 앱의 안정성 향상에 큰 도움이 되었습니다.
이 시스템을 통해 개발팀은 더 빠르게 문제를 파악하고 해결할 수 있게 되었으며, 사용자 경험 개선에도 기여했습니다.