ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter 에러는 try/catch 하나로 잡히지 않습니다
    AI Agent/flutter 2026. 7. 2. 21:00
    728x90
    반응형

    붉은 줄이 흐릿하게 강조된 모니터와 베젤에 붙은 포스트잇
    try/catch 밖에서 쌓이던 크래시 리포트

    try/catch로 감싼 함수였습니다.
    그 안에서 나는 에러는 다 잡아서 로그로 남기게 해뒀고요.
    그런데도 크래시 리포트 대시보드에는 같은 화면발 크래시가 매일 몇 건씩 쌓였습니다.
    스택 트레이스를 열어봐도 제가 감싼 그 try 블록은 거기 없었습니다.
    분명히 잡고 있는데 안 잡히는 에러라니, 한동안 이게 말이 안 된다고 생각했습니다.

    문제는 Flutter에서 "에러"라고 부르는 것들이 한 층에 살지 않는다는 거였습니다.
    try/catch가 지키는 울타리는 생각보다 좁았습니다.
    그 울타리 밖에서 나는 에러가 세 갈래쯤 있었고, 각각 잡는 자리가 달랐습니다.

    try/catch가 못 넘는 담

    처음엔 제 코드를 의심했습니다.
    어딘가 try/catch가 빠진 함수가 있겠거니 하고, 크래시가 잦은 화면의 async 함수를 죄다 다시 감쌌습니다.
    그런데 다음 날 대시보드엔 같은 크래시가 또 쌓였습니다.
    감쌌는데도 안 잡힌다면, 감싸는 자리가 틀렸다는 뜻이었습니다.

    이걸 눈으로 확인하려고 일부러 작은 실험을 했습니다.
    try 블록 안에서 Future.delayed 뒤에 예외를 하나 던져봤습니다.
    await는 안 붙이고요.
    그랬더니 catch는 아무 반응이 없고 콘솔에만 처리 안 된 예외가 찍혔습니다.
    await를 붙이자 그제야 catch가 잡았고요.
    같은 한 줄인데 await 하나로 잡히고 안 잡히고가 갈렸습니다.
    함수는 진작 리턴했고 catch는 문을 닫은 뒤인데, 예외는 그 닫힌 문 뒤에 뒤늦게 도착한 겁니다.
    제가 "분명히 감쌌는데"라고 착각한 게 이 지점이었습니다.

    그런데 스택을 더 열어보니, 제 함수가 아예 없는 크래시도 섞여 있었습니다.
    위젯을 그리는 도중에 난 예외였습니다.
    이건 제 호출 스택이 아니라 프레임워크가 build를 돌리는 스택 안에서 터지니, try/catch로 감쌀 자리 자체가 없었습니다.
    비동기는 시간이 어긋나서 못 잡았고, 이건 아예 다른 스택이라 못 잡은 거였습니다.
    못 잡는 이유가 하나가 아니었던 거죠.

    build 중에 터지면 누가 받나요

    그래서 잡는 자리를 함수 안이 아니라 앱 전체로 올려야 했습니다.

    void main() {
      FlutterError.onError = (details) {
        FlutterError.presentError(details);
        reportToDashboard(details.exception, details.stack); // 프레임워크발 에러
      };
    
      runZonedGuarded(
        () => runApp(const MyApp()),
        (error, stack) => reportToDashboard(error, stack), // 새어나온 비동기 에러
      );
    }
    

    FlutterError.onError는 위젯 build나 layout처럼 프레임워크 안에서 나는 에러를 받는 자리입니다.
    runZonedGuarded는 그 zone 안에서 처리 안 된 비동기 에러가 마지막으로 떨어지는 그물이고요.
    이 둘을 깔고 나서야 대시보드에 찍히던 크래시의 스택이 비로소 제 코드 어디서 났는지를 가리키기 시작했습니다.

    runZonedGuarded도 못 잡는 게 남습니다

    여기서 끝이 아니었습니다.
    네이티브 쪽에서 프로세스가 통째로 죽는 크래시는 Dart 그물에 애초에 안 걸립니다.
    플러그인이 Swift나 Kotlin 코드에서 죽으면, 그건 Dart까지 올라오기 전에 앱이 사라집니다.
    그래서 이런 크래시는 Dart 로그만 보면 아무것도 없는 것처럼 보입니다.
    대시보드가 비어 있다고 문제가 없는 게 아니라, 문제가 Dart 아래층에 있어서 안 보이는 경우가 있었습니다.

    이 층을 처음 의심한 건, 대시보드는 깨끗한데 사용자 리뷰에 "사진 올릴 때 앱이 꺼진다"는 별점이 달렸을 때였습니다.
    Dart 로그는 아무 문제 없다는데 사용자는 분명히 크래시를 겪고 있었습니다.
    두 기록이 안 맞는 게 오히려 단서였습니다.
    내 그물이 못 닿는 아래층에서 뭔가 죽고 있다는 뜻이었으니까요.
    그래서 이제는 별점과 로그가 어긋나면 로그를 믿기보다 층을 의심합니다.

    이 층까지 보려면 네이티브 크래시를 따로 수집하는 도구가 필요했는데, 그 세팅은 아직 절반만 해뒀습니다.
    어느 층에서 죽었는지를 먼저 나누는 것까지가 이번에 정리된 부분입니다.

    대시보드가 조용해지기까지

    지금은 "에러 처리했어요"라는 말을 예전만큼 쉽게 못 합니다.
    try/catch가 닿는 데가 생각보다 좁다는 걸 그 며칠에 몸으로 알았거든요.
    감싼다고 잡히는 게 아니라, 그 예외가 내 스택 안에서, 내가 문을 닫기 전에 났을 때만 잡힙니다.
    그 조건을 벗어난 것들이 매일 조용히 대시보드에 쌓이고 있었던 거고요.
    FlutterError.onErrorrunZonedGuarded도 만능은 아니라, 각자 닿는 데까지만 받아줍니다.

    그리고 이 그물을 깐 뒤로 대시보드에 찍히는 리포트의 스택이 훨씬 읽을 만해졌습니다.
    예전엔 "어디선가 죽었다"만 알았다면, 지금은 최소한 어느 층에서 났는지가 스택 맨 위에 남습니다.
    원인을 좁히는 시작점이 생긴 거죠.

    try/catch 하나로 다 잡힐 거라 믿었던 그 며칠이 사실 제일 위험했습니다.
    잡히는 줄 알았으니 대시보드를 안 봤을 테니까요.
    다음엔 미뤄둔 네이티브 층 수집부터 마저 붙일 생각입니다.

    728x90
    반응형
Designed by Tistory.