ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter 앱의 경계는 Dart 코드 바깥에도 있습니다
    AI Agent/flutter 2026. 7. 2. 15:00
    728x90
    반응형

    밤 책상 위 케이블로 노트북에 연결된 아이폰과 체크리스트 카드
    심사 직전에 발견한 Info.plist 한 줄

    Android에서는 완벽하게 돌던 기능이었습니다.
    사진 첨부 버튼을 누르면 갤러리가 열리고, 골라서 올리는 것까지 매끄러웠습니다.
    iOS 빌드도 통과했고, 시뮬레이터에서도 잘 됐습니다.
    문제는 심사 제출을 하루 앞두고 실기기에서 그 버튼을 눌렀을 때였습니다.
    앱이 갤러리를 여는 대신 그냥 조용히 죽었습니다.

    Dart 코드는 양쪽이 완전히 똑같았습니다.
    같은 위젯, 같은 플러그인 호출, 같은 콜백.
    그런데 iOS에는 Dart가 모르는 규칙이 하나 더 있었습니다.
    사진에 왜 접근하는지 사람 말로 적어두지 않으면, 시스템이 앱을 그 자리에서 종료시킵니다.
    그 문장은 Dart 파일이 아니라 Info.plist에 있어야 했습니다.

    Dart가 모르는 규칙

    솔직히 Xcode 콘솔을 열기까지가 오래 걸렸습니다.
    Flutter만 보던 습관 때문에, flutter run 로그를 다시 띄우고 flutter analyze부터 몇 번을 돌렸거든요.
    거기엔 경고 하나 없었습니다.
    코드가 깨끗하다는데 앱은 죽으니 한참을 엉뚱한 데서 헤맸습니다.
    플러그인 GitHub 이슈 탭을 뒤지다가 "iOS는 usage description이 필수"라는 짧은 댓글을 보고서야 Xcode를 열었습니다.

    실기기를 케이블로 물리고 Xcode 콘솔을 열어서야 그 메시지를 봤습니다.
    Flutter 로그 창에는 아무것도 안 떴거든요.
    버튼을 누르는 순간 앱이 사라지는데 Dart 쪽은 조용하니, 처음엔 플러그인이 갤러리를 못 여는 건 줄 알았습니다.
    네이티브 콘솔을 보고 나서야 앱이 못 연 게 아니라 시스템이 앱을 끈 거라는 걸 알았습니다.
    여는 것과 꺼지는 건 완전히 다른 이야기였죠.
    Dart 도구만으로는 이 문제를 들여다볼 창구가 처음부터 없었던 겁니다.

    크래시 로그를 봤더니 Dart 스택이 아니었습니다.
    네이티브 쪽에서 올라온 메시지였고, 요지는 "사진 사용 설명 문자열이 없다"였습니다.
    iOS는 개인정보에 접근하는 앱한테 그 이유를 사용자에게 보여줄 문장을 미리 요구합니다.
    그게 없으면 권한 팝업을 띄우는 대신 앱을 끝내버립니다.
    Android는 이 문장을 이런 방식으로 강제하지 않으니, 같은 Dart 코드가 한쪽에서만 죽은 겁니다.

    고친 건 Dart가 아니라 iOS 설정 파일이었습니다.

    <key>NSPhotoLibraryUsageDescription</key>
    <string>프로필 사진을 등록하려고 사진 보관함에 접근합니다.</string>
    

    한 줄 추가하고 다시 빌드했습니다.
    그런데도 한 번에 믿기지가 않아서, 앱을 지우고 새로 깔아 권한 팝업부터 다시 받아봤습니다.
    이번엔 "사진 접근을 허용하시겠어요?"라는 시스템 창이 제가 적은 그 문장과 함께 떴습니다.
    아까는 이 창이 뜰 새도 없이 앱이 꺼졌던 거였습니다.
    팝업이 뜨고, 갤러리가 열리고, 사진을 고르는 그 평범한 흐름이 하루 전엔 없던 흐름이었습니다.

    코드 리뷰를 백 번 봐도 안 나왔을 문제였습니다.
    문제가 애초에 Dart 파일 안에 없었으니까요.
    diff를 아무리 노려봐도, 안 바뀐 설정 파일은 diff에 뜨지 않습니다.

    경계는 파일 확장자 바깥에 있습니다

    이 일이 저한테 남긴 건 "Info.plist 잊지 말자"보다 조금 큰 이야기였습니다.
    Flutter 앱은 Dart로만 이뤄져 있지 않습니다.
    Info.plist, AndroidManifest.xml, 플러그인이 요구하는 네이티브 설정, 백엔드가 내려주는 응답 형식까지가 다 앱의 일부입니다.
    이것들은 .dart 파일이 아니라서 평소엔 안 보이는데, 버그는 오히려 이 보이지 않는 경계에서 자주 태어납니다.

    돌이켜보면 이게 하필 심사 전날 터진 것도 우연이 아니었습니다.
    개발 내내 저는 Android 폰을 꽂아놓고 짰거든요.
    iOS 실기기는 거의 안 봤고, 시뮬레이터는 이 권한 종료를 재현하지 않았습니다.
    그래서 "되는 환경"만 보면서 다 됐다고 믿은 겁니다.
    안 보이는 경계는 안 보이는 환경에서 조용히 기다리고 있었습니다.

    특히 "한쪽 OS에서만 죽는다"거나 "내 코드는 안 바뀌었는데 어제부터 안 된다" 같은 증상이면, 저는 이제 Dart부터 뒤지지 않습니다.
    그 사이에 낀 네이티브 설정이나 외부 응답부터 의심합니다.
    이번 크래시도 결국 제 Dart 코드가 아니라, 그 코드가 서 있는 iOS라는 바닥이 요구한 조건이었으니까요.

    그리고 이 조건들은 대부분 "없으면 경고"가 아니라 "없으면 종료"입니다.
    Info.plist의 usage description, 백그라운드 모드 설정, URL scheme 같은 것들은 빠져 있어도 빌드는 통과합니다.
    컴파일러가 잡아주는 영역이 아니거든요.
    그래서 이 경계의 버그는 개발 중엔 안 보이다가, 하필 실기기의 특정 흐름에서만 튀어나옵니다.
    제가 심사 전날 만난 게 딱 그거였습니다.

    심사 전날 그린 지도

    그날 이후로 새 플러그인을 붙일 때 확인하는 순서가 하나 늘었습니다.
    pub.dev 페이지의 예제 코드만 보지 않고, iOS와 Android 설정 항목을 먼저 읽습니다.
    권한을 쓰는 플러그인은 열에 아홉 네이티브 설정을 요구하니까요.

    심사는 하루 밀렸습니다.
    다음 릴리스 땐 이 체크를 배포 직전이 아니라 기능 만들 때 같이 하려고, 팀 위키에 항목을 하나 적어뒀습니다.
    그게 실제로 지켜질지는 다음 심사 때 알게 되겠죠.

    728x90
    반응형
Designed by Tistory.