프로그래밍/flutter로 앱만들기

[Flutter] iOS에서 마이크 권한 팝업이 안 뜨는 이유와 해결 방법

kugancity 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
반응형