ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Flutter] iOS에서 마이크 권한 팝업이 안 뜨는 이유와 해결 방법
    프로그래밍/flutter로 앱만들기 2025. 5. 15. 13:29
    반응형

     

     

     

     

     

     

    최근 Flutter로 음성 녹음 기능을 구현하면서 아주 흥미롭고 애매한 상황을 겪었다.
    iOS에서 마이크 권한 팝업이 어떤 때는 잘 뜨고, 어떤 때는 전혀 안 뜨는 문제가 있었고,
    덕분에 permission_handler, record 패키지, 그리고 iOS 자체의 권한 처리 구조를 탈탈 털게 됐다.

    이 글은 그 삽질 기록을 정리한 것이다.
    혹시 나처럼 마이크 팝업이 안 떠서 당황하고 있는 Flutter 개발자가 있다면 도움이 될 수 있을 거라고 본다.

    상황 설명

    앱에서 음성 녹음을 시작하는 기능을 구현 중이었다.
    사용한 주요 패키지는 다음과 같다:

    • record: 실제 녹음 기능
    • permission_handler: 권한 요청/상태 확인

    앱 흐름은 간단했다.
    녹음 버튼을 누르면 마이크 권한을 요청하고, 승인되면 녹음을 시작하는 구조였다.

    그런데 iOS에서 이상한 현상이 발생했다.

    • 어떤 기기에서는 마이크 권한 팝업이 잘 뜨고
    • 어떤 기기에서는 아예 팝업이 안 뜨며, PermissionStatus.permanentlyDenied가 반환됨

    처음에는 Info.plist 설정 문제인가 싶었지만, 당연히 NSMicrophoneUsageDescription은 잘 들어가 있었고, Background ModesAudio로 활성화된 상태였다.

    원인 분석

    결론부터 말하자면 이 문제는 코드 로직, 정확히는 마이크에 실제 접근하는 시점과 권한 요청 타이밍의 미묘한 차이에서 발생했다.

    iOS의 권한 구조는 다음과 같다:

    • 시스템이 처음 마이크 접근을 감지하면
    • Info.plist에 설명이 있을 경우 팝업을 띄움
    • 사용자가 거절하면 더 이상 자동으로 묻지 않음
    • 이후 권한 상태는 denied 또는 permanentlyDenied로 고정됨

    문제는 Flutter 환경에서 이 "처음 마이크 접근"이 iOS가 인식할 수 있을 만큼 확실하게 일어나지 않는 경우가 있다는 것.

    예를 들어 이런 코드:

    final status = await Permission.microphone.request();
    if (!status.isGranted) {
      // 권한 거부 처리
    }

    이 코드는 실제로 iOS에서 마이크에 접근하는 게 아니라, 그냥 권한 상태만 확인한다.
    그렇기 때문에 iOS는 "얘가 마이크를 쓰려는지 아닌지 아직 모르겠네?" 하고 팝업을 생략한다.

    결국 팝업이 안 뜨고, 상태만 permanentlyDenied로 떨어져버리는 것.

    해결 방법

    iOS가 마이크 접근을 감지하도록 명확한 시도를 먼저 해주는 게 핵심이다.
    그걸 위해 나는 아래처럼 AudioRecorder().start()를 잠깐 실행하는 코드를 넣었다.

    Future<void> triggerIOSMicrophoneRequest() async {
      final tempDir = await getTemporaryDirectory();
      final dummyPath = '${tempDir.path}/dummy_check.m4a';
    
      final dummyRecorder = AudioRecorder();
    
      try {
        await dummyRecorder.start(
          const RecordConfig(),
          path: dummyPath,
        );
        await Future.delayed(const Duration(milliseconds: 300));
        await dummyRecorder.stop();
      } catch (_) {
        // 무시
      }
    }

    이 코드를 앱 초기화 시점이나 권한 요청 전에 한 번 실행하면,
    iOS는 "이 앱이 진짜로 마이크를 쓰려는구나"라고 판단해서 팝업을 띄운다.

    이 이후부터는 permission_handler에서 권한 상태를 확인해도 정상적으로 granted가 뜬다.

    왜 이런 문제가 Flutter에서만 발생할까?

    Swift로 iOS 네이티브 개발을 할 때는 이런 문제가 없다.
    그 이유는 간단하다. 네이티브에서는 내가 AVAudioSession.sharedInstance().requestRecordPermission()를 직접 호출하고,
    실제 마이크 접근 타이밍도 완전히 제어할 수 있다.

    하지만 Flutter는 cross-platform 프레임워크다 보니 내부에서 어떤 순서로 native bridge가 실행되는지 정확히 알기 어렵다.
    그래서 실제 마이크 접근 이전에 권한 상태를 먼저 물어보는 식의 흐름이 생기면, iOS는 아무 반응도 안 보이고 끝나는 것이다.

    그래서 팝업이 어떨 땐 뜨고, 어떨 땐 안 떴던 이유

    테스트 중 권한 팝업이 뜰 때도 있고, 안 뜰 때도 있었다.
    그건 결국 녹음 버튼을 눌렀을 때 실제로 iOS가 마이크 접근을 감지할 수 있었냐 없었냐의 차이였다.

    • 어떤 시점엔 _recorder.start()가 먼저 실행돼서 팝업이 떴고
    • 어떤 시점엔 permission_handler가 상태부터 물어보는 바람에 팝업이 무시됐다

    요약하자면, iOS에서 마이크 권한 팝업이 Flutter 환경에서 예측 불가능하게 동작한다면,
    실제로 마이크 접근을 시도해서 iOS의 관심을 먼저 끄는 게 유일한 해결책이다.

    recordpermission_handler 패키지가 문제가 아니라,
    그 내부 동작 방식이 iOS의 권한 시스템과 정확히 맞물리지 않는 게 문제였던 셈이다.

    덕분에 iOS와 Flutter 사이의 동기화 타이밍이 얼마나 중요한지 뼈저리게 느끼게 됐다.

    728x90
    반응형
Designed by Tistory.