TECH

iOS 사진 접근 권한(Limited) 이슈 회고

JAN 02, 20269분

`showyourtime` 앱을 React Native CLI에서 Expo로 마이그레이션하면서 사진 라이브러리 접근 부분을 `react-native-cameraroll`에서 `expo-media-library`로 교체했다. Expo의 표준 모듈을 사용하면 운영 복잡도를 줄일 수 있고, 사진 저장, 앨범 생성/조회, 자산 관리 등 필요한 기능을 폭넓게 제공한다는 점이 교체 결정의 이유였다. 하지만 이 전환 과정에서 iOS 사진 접근 권한과 관련된 예상치 못한 이슈를 마주하게 되었다. 교체 완료 후 몇 주간 내부 테스트를 진행했다. 기능 테스트에서 사진 저장은 정상 동작했고, 앨범 생성/조회 등 여러 시나리오에서도 문제가 없어 배포를 진행했다. ## 1. 문제 발생: "사진이 저장되지 않는다" 배포 후 고객으로부터 제보가 들어왔다. "사진이 저장되지 않아요." 처음에는 의아했다. 몇 주 동안 테스트했고, 저장 기능은 계속 정상으로 확인되었기 때문이다. 일반적으로 고려할 수 있는 가능성들은 다양했다. 특정 iOS 버전 호환성 문제, 특정 기기 모델 관련 문제, 미디어 형식(HEIC/Live Photo/GIF 등)별 처리 문제, 디스크 공간 부족이나 파일 I/O 오류 등 환경 변수들. 제보해주신 기기와, OS 그리고 사용하셨던 시간대를 바탕으로 Sentry의 여러 이슈들을 면밀히 확인한 결과 저장 시 권한 관련된 내용이 있었다. 권한 테스트를 해보니 Limited 권한 상태에서 문제가 재현되었다. ## 2. iOS 사진 권한 모델의 변화 iOS 14부터 Apple은 사진 권한 모델을 강화했다. 기존의 "허용/거부" 방식에서 다음과 같은 선택지로 확장되었다. 모든 사진 접근 허용(All Photos), 선택한 사진만 접근 허용(Limited Photos), 거부(Denied). 개인정보 보호 관점에서 Limited 권한은 매우 합리적인 기능이다. 사용자는 앱이 모든 사진에 접근할 필요가 없다고 판단할 수 있고, 필요한 사진 몇 장만 선별하여 허용할 수 있다. 실제로 많은 사용자가 이 옵션을 선택한다. Limited 권한은 "모든 사진 접근"을 제한한다. 그러나 앱이 새로 생성한 항목(앱이 추가한 사진/앨범)에 대해서는 OS가 상대적으로 관대하게 처리한다. 따라서 "읽기(조회)"와 "쓰기(저장/추가)"의 권한 의미가 동일하지 않다. 즉, Limited 권한이라고 해서 "사진 저장이 불가능"한 것이 아니라, "사진 조회 범위가 제한되는 것"에 가깝다. ## 3. Expo Media Library의 문제점 분석 기존에 잘 동작하던 기능이 라이브러리 변경으로 동작하지 않는다면 라이브러리 자체의 문제일 가능성이 높다고 판단했다. 라이브러리의 네이티브 코드를 확인한 결과, 이번 이슈의 본질은 '저장은 가능한데, 라이브러리 구현이 이를 차단하고 있었다'는 점이었다. Expo의 iOS 구현을 분석한 결과, 권한 체크 흐름이 두 가지로 나뉘어 있었다. 단순히 "권한이 부여되었는지"만 확인하는 체크(`checkPermissions`)와 "반드시 all 권한이어야만 가능"으로 강제하는 체크(`runIfAllPermissionsWereGranted`). ### 문제가 된 코드 로직 ```swift private func runIfAllPermissionsWereGranted(reject: ..., block: ...) { ... if permissions["status"] as? String != "granted" { reject(...) return } if permissions["accessPrivileges"] as? String != "all" { reject(...) return } block() } ``` 여기서 `permissions["accessPrivileges"]`가 `"all"`이 아니면(즉, `"limited"`여도) 무조건 권한이 없는 것으로 취급했다. iOS는 Limited 상태에서도 "사진 저장/추가"를 허용할 여지가 있지만, Expo 모듈은 그 이전 단계에서 "all이 아니면 불가"로 차단하고 있었다. 개발자 관점에서는 "안전한 선택"일 수 있지만, 실제 OS 정책과 비교하면 "필요 이상으로 보수적"인 접근이었다. ## 4. 아쉬운 테스트 가장 아쉬운 지점은 테스트였다. 몇 주 동안 테스트했지만, 대부분 사진 권한을 "허용(전체 허용)", 즉 `accessPrivileges == "all"` 상태를 전제로 진행했다. 일반적으로 "사진 저장 기능 테스트"라고 하면 저장 API 성공 여부, 앨범 저장 확인, 파일 정상성 등을 검증한다. 그런데 iOS에서 권한은 기능 로직과 분리된 "환경 변수"처럼 작동한다. 권한 상태가 달라지면 동일한 기능이 전혀 다른 경로를 탄다. 개발자가 테스트에서 쉽게 간과하는 이유가 있다. 기본적으로 테스트 기기에는 전체 허용을 설정하는 경우가 많다. 반면 일반 사용자들은 "전체 허용"에 거부감이 있어 Limited를 선택할 가능성이 높다. Limited는 "읽기/쓰기/선택 UI"가 복합적으로 얽히면서 UX까지 변경시킨다. 수주간의 테스트에도 불구하고 실제 사용자 환경에서는 문제가 발생했다. ## 5. 1차 해결: patch-package 활용 원인 분석이 끝나면 해결 방향은 명확해진다. `accessPrivileges == "all"`만 허용하던 체크를 `accessPrivileges == "limited"`도 허용하도록 완화하는 것이다. 하지만 현실적인 제약사항이 있었다. Expo 모듈은 JavaScript 레벨에서 해결할 수 있는 문제가 아니었고, iOS 네이티브 구현에서 예외를 발생시키며 차단하는 구조였다. 공개된 옵션이나 설정으로 우회할 방법이 보이지 않았다. 따라서 `patch-package`를 선택했다. `node_modules` 내 네이티브 코드를 수정하고 패치 파일로 관리하여 빌드 시 항상 해당 수정이 적용되도록 했다. 이렇게 "Limited도 통과"하도록 변경한 결과, 고객이 겪던 "저장 실패" 문제는 해결되었다. ## 6. 2차 문제: 지속적인 iOS 시스템 팝업 그러나 문제는 여기서 끝나지 않았다. Limited 권한 상태에서 저장을 반복 테스트하던 중, iOS가 자주 "추가 사진을 선택하시겠습니까?" 시스템 팝업을 띄우는 현상을 발견했다. 기능은 정상이지만 UX가 계속 방해받는 상황이었다. 이는 특히 `showyourtime` 같은 앱에서는 치명적이었다. 사용자는 "빨리 찍고, 빨리 공유/이탈"하고 싶어하는데, OS 팝업이 중간에 개입하면 사용 흐름이 깨진다. 사용자는 "앱이 자꾸 뭔가를 요구한다"는 부정적 인상을 받게 된다. 더욱 이상한 점은 경쟁 앱을 확인해보니 비슷한 상황에서도 이런 팝업이 뜨지 않았다는 것이었다. 즉, "원래 어쩔 수 없는 정책"이 아니라 "내가 놓친 설정이나 구현이 있다"는 의미였다. Stack Overflow 검색, GitHub 이슈 탐색, LLM에게 상황 설명 후 해결책 문의등을 했다. 하지만 계속 수렴하는 결론은 "iOS 정책상 제한이니 어쩔 수 없다", "Limited에서는 원래 그렇다", "사용자에게 all로 변경하도록 안내하라"였다. 하지만 경쟁 앱에서 팝업이 뜨지 않는 것을 이미 확인한 상태에서 "어쩔 수 없다"는 결론은 현실과 맞지 않았다. 결국 답은 공식 문서에 있었다. `PHPhotoLibraryPreventAutomaticLimitedAccessAlert`라는 키였다. Info.plist에 이 키를 `true`로 설정하면 iOS가 "자동으로" Limited 접근 확장 관련 알림을 띄우는 것을 방지할 수 있다. 대신 필요할 때 앱이 직접 `presentLimitedLibraryPicker`를 호출해서 사용자에게 선택 UI를 제공할 수 있다. 즉, OS가 상황에 따라 알아서 개입하던 UX를 "개발자가 통제"하도록 변경해주는 옵션이다. 적용 결과 저장 기능은 정상 유지되었고, 불필요한 시스템 팝업이 제거되었으며, 경쟁 앱과 동일한 UX를 구현할 수 있었다. ## 7. 그리고. 이번 이슈를 해결하는 과정에서 LLM에 의존하려고 하다가 시간을 허비한 부분들이 있었다. LLM에게 상황을 설명하고 해결책을 문의했지만, 계속 수렴하는 결론은 "iOS 정책상 제한이니 어쩔 수 없다", "Limited에서는 원래 그렇다"였다. LLM이 모든 데이터를 학습했다고 해도 못 찾아주는 게 많다. 최종 해결책은 Apple 공식 문서에서 찾을 수 있었고, 문제를 해결한 것은 AI가 아니라 내가 직접 확인한 네이티브 코드와 공식 문서였다. LLM은 도구일 뿐이고, 최종 답은 여전히 코드와 문서에서 나온다. --- ps. 그리고 며칠 뒤 안드로이드에서 동일한 문의가 들어왔다. Android 16에서 동일한 권한 기능이 있었기 때문이다.

© 2026 w0nder