사진 정리 앱에 알림 카드를 넣으려다
Airout 은 쌓인 스크린샷과 비슷한 사진을 iPhone 안에서 찾아 하루 10장씩 비우는 앱입니다. 그런데 이 앱의 출발점은 사실 알림 정리였습니다. 다른 앱의 알림을 읽거나 지우는 API 는 iOS 에 없고, 합법적으로 쓸 수 있는 신호는 Screen Time 의 앱별 알림 수 하나뿐입니다. 그 숫자는 DeviceActivityReport 라는 리포트 익스텐션의 화면 안에서만 그릴 수 있고, 앱 본체로는 꺼내 올 수 없습니다(ADR-0002). 그래서 "이번 주에 나를 가장 많이 울린 앱"을 리포트 뷰가 직접 그리는 카드로 설계했고, 10월 3일에 코드와 빌드·테스트까지 마쳐 두었습니다.
남은 건 Apple 의 Family Controls 배포 권한이었습니다. 신청서를 내고 몇 주 기다리는 절차로 알고 영어 설명문까지 써 두었는데, 10월 3일에 신청 페이지를 열어 보니 폼이 없고 약관과 "Get Entitlement" 버튼 하나뿐이었습니다. 심사형 신청이 약관 수락형 셀프서비스로 바뀐 것입니다.
약관의 핵심은 앱의 주된 목적이었습니다. 부모가 자녀의 앱 사용을 관리하는 것, 또는 개인이 집중과 생산성을 위해 자기 기기를 관리하는 것. Airout 의 주된 목적은 사진 정리이고 알림 카드는 부가 기능입니다. 버튼을 누르는 건 계약 동의라 그 자리에서 멈추고 선택지 셋을 적었습니다. 1.0 을 Screen Time 없이 내거나, 동의하고 켜거나(나중에 권한이 회수될 수 있음), 알림 카드를 집중 기능으로 키운 뒤 동의하거나(1.0 범위 밖). 10월 4일에 첫 번째로 정해 플래그·엔티틀먼트·익스텐션 포함을 함께 껐고, 익스텐션 타깃 정의는 남겼습니다.
이틀 만에 되살리고, 서명에서 한 번 막히다
10월 6일에 알림 쪽을 앱의 방향으로 살리기로 하면서 길을 두 갈래로 냈습니다. 하나는 권한이 필요 없는 캡처 읽기로, 사용자가 찍은 스크린 타임 알림 화면을 인덱서가 이미 뽑아 둔 OCR 텍스트에서 앱 이름과 횟수로 읽습니다. 다른 하나가 Screen Time 카드를 되살리는 것이었고, 이번에는 약관에 제가 직접 동의했습니다. 주된 목적은 "개인이 자기 기기의 방해를 줄이는 집중 목적"으로 서술했습니다.
끈 것을 다시 켤 때는 세 곳을 함께 켜야 합니다. 플래그 주석에도 그렇게 남겼습니다.
enum FeatureFlags {
/// Screen Time 리포트 카드. 2026-10-04에 뺐다가 PO가 2026-10-06 Family Controls 약관에 동의해 되살렸다.
/// 끌 때는 셋을 함께 끈다: 이 플래그, 앱 엔티틀먼트의 Family Controls, 앱 타깃의 리포트 익스텐션 의존성(project.yml).
/// 세부: .claude/intent/launch/family-controls.md
static let screenTime = true
}
개발 서명 Release 빌드는 통과했는데, TestFlight 업로드에서 exportArchive 가 "Invalid extension … must be built as an ExtensionKit extension" 으로 거부했습니다. iOS 26 SDK 는 DeviceActivityReport 를 예전 방식(NSExtension, PlugIns 폴더)이 아니라 ExtensionKit 익스텐션으로만 받는다는 뜻이었습니다. 고친 건 project.yml 몇 줄입니다.
AiroutScreenTimeReport:
# iOS 26 배포 서명은 DeviceActivityReport를 ExtensionKit 익스텐션(Extensions 폴더, EXAppExtensionAttributes)으로만 받는다.
# NSExtension(PlugIns)으로 묶으면 exportArchive가 "Invalid extension"으로 거부한다(TestFlight 10 시도, 2026-10-06).
type: extensionkit-extension
...
EXAppExtensionAttributes:
EXExtensionPointIdentifier: com.apple.deviceactivityui.report-extension
커밋에는 이걸로 TestFlight 1.0.0 (10) 업로드가 성공했다고 적혀 있습니다. 같은 날 빌드가 (19)까지 올라갔고, 밤 10시 56분에 심사에 냈습니다.
스토어 스크린샷 3번, 되살린 알림 순위 화면. 앱 이름은 스크린샷용 샘플이다.
자동 거절, 그리고 '허용하기'라는 버튼 이름
10월 8일에 자동 메시지가 왔습니다. 자동 분석으로 보니 Screen Time API 를 쓰는데 Family Controls 엔티틀먼트 없이 제출했다는 내용이었고, 버전은 REJECTED 가 됐습니다.
먼저 사실부터 확인했습니다. 같은 아카이브를 App Store 서명으로 다시 내보내 보니 앱과 리포트 익스텐션 모두 com.apple.developer.family-controls = true 였고, 포털의 두 App ID 에도 Family Controls Distribution capability 가 켜져 있었습니다. 기록에는 자동 검사가 셀프서비스로 받은 권한을 못 알아본 것으로 "보인다"고 적었습니다. 확인된 원인이 아니라 추정입니다.
선택지는 둘이었습니다. 증거를 붙여 해결 센터에 답장하거나, 1.0 에서 Screen Time 을 빼고 캡처 읽기만으로 다시 내는 것. 답장을 골랐고, 거절이 유지되면 두 번째 길로 가기로 같은 줄에 적었습니다. 답장에는 두 번들의 서명, 개인 승인만 쓰고 제한 API 는 쓰지 않는다는 것, 알림 수가 기기 밖으로 나가지 않는다는 것을 넣었습니다.
10월 12일에 온 심사 메시지에는 Family Controls 이야기가 없었습니다. 체크리스트에는 "답장으로 해소"라고만 있고 Apple 의 답은 따로 적혀 있지 않습니다. 대신 새 지적이 두 개 왔습니다. 2.1 은 Screen Time 기능이 어디 있느냐는 질문이라, 코드 대신 심사 메모와 답장에 홈 → 알림 카드 → 스크린 타임 승인 → 이번 주 순위로 이어지는 단계를 적었습니다.
다른 하나가 5.1.1(iv) 였습니다. 사진 권한을 묻기 전 안내 화면의 버튼이 "사진 접근 허용하기" 라서 사용자를 허용 쪽으로 이끈다는 지적입니다. 결정은 시스템 권한 창에서 하고, 그 앞 화면은 설명만 해야 한다는 원칙입니다.
// App Review 5.1.1(iv): 권한 요청 전 화면의 버튼은 "허용"이 아니라 "계속"이다(결정은 시스템 창에서).
Text(String(localized: "onboarding.access.continue", defaultValue: "Continue"))
.font(.headline)
한국어는 "계속", 영어는 "Continue" 입니다. 누르면 시스템 권한 창이 뜨는 흐름은 그대로이고, 온보딩 첫 화면인 스토어 스크린샷 6번은 바뀌지 않았습니다.
스토어 스크린샷 6번, 온보딩 첫 화면. 버튼 이름을 바꾼 사진 권한 안내는 이 다음 화면이다.
확인한 것과 남은 것
같은 날 1.0.0 (20) 을 올려 오전 10시 46분에 다시 냈습니다. 출시는 10월 13일 12시 23분, 한국과 미국 App Store 에 동시에 READY_FOR_SALE 로 올라갔습니다. 첫 제출부터 출시까지 일주일이 채 안 걸렸고, 약관 앞에서 멈춘 날부터 세면 열흘이었습니다.
남은 것도 적어 둡니다. AdMob 은 14일 커밋 제목 기준으로 스토어 연결과 앱 인증을 마치고 Google 검토를 기다리는 데서 기록이 멈춰 있습니다. review-fixes intent 에는 Screen Time 카드의 "자동으로 보기" 버튼도 같은 지적을 받으면 문구를 바꾼다는 줄이 남아 있습니다. 제품 문서 한 절에는 Family Controls 를 "1.0 제외"로 정한 10월 4일 상태가 아직 남아 있어, 되살렸다는 다른 절과 맞지 않습니다. 다음에 고칠 곳입니다.
판단이 갈린 곳은 두 번이었고, 두 번 다 물러설 길을 같은 줄에 적어 두고 골랐습니다.
Airout 은 쌓인 스크린샷·비슷한 사진·흔들린 사진을 iPhone 안에서 찾아 하루 10장씩 비우는 앱입니다. 분석은 전부 기기 안에서 하고, 서버도 계정도 없습니다.