P0-1 Firebase의 iOS 설정 파일(GoogleService-Info.plist)이 레포에 없다 푸시와 구글 로그인이 "우리 Firebase 프로젝트가 어디인지" 알아내는 파일. 이게 없으면 Xcode 프로젝트를 만드는 첫 단계에서 멈춘다.
등장하는 것
- GoogleService-Info.plist
- Firebase가 발급하는 iOS 전용 설정 파일. 프로젝트 ID, API 키, 구글 로그인용 OAuth 클라이언트 ID가 들어있다. 안드로이드의
google-services.json과 같은 역할이고, 그 파일은 이미 레포에 있다. - @react-native-firebase/app
- 앱에서 Firebase를 켜는 패키지. 이 앱에선 FCM 푸시 때문에 들어와 있다.
- @react-native-google-signin
- 구글 로그인 버튼의 네이티브 SDK. 로그인 후 받은 idToken을 bedar-api에 넘겨 자체 토큰으로 바꾼다.
- URL scheme
- 구글 로그인 창이 끝나고 앱으로 되돌아오는 주소. 위 plist 안의
REVERSED_CLIENT_ID값으로 만들어진다.
왜 iOS에서 깨지나
이 파일을 읽는 플러그인이 두 개다. @react-native-firebase/app(app.json:68)과 @react-native-google-signin(app.json:107). 후자는 옵션 없이 이름만 적혀 있어 Firebase에서 값을 읽어오는 경로로 동작한다.
즉 plist가 없으면 prebuild 단계에서 바로 실패하고, 억지로 넘겨도 되돌아올 주소가 없어 iOS 구글 로그인이 동작하지 않는다.
근거
// app.json:14 — 파일을 가리키고 있는데 "googleServicesFile": "./GoogleService-Info.plist" // 실제 디렉터리에는 없음 (ls → MISSING) // node_modules/@react-native-google-signin/.../withGoogleSignIn.js options ? withGoogleSignInWithoutFirebase(...) // 옵션 있으면 직접 지정 : withGoogleSignIn(config) // ← 현재 경로. plist 를 읽는다 IOSConfig.Google.withGoogle IOSConfig.Google.withGoogleServicesFile
bedar-bf0ee에 iOS 앱(kr.bedar.app)을 추가하고 GoogleService-Info.plist를 받아 bedar-RN/ 루트에 둔다. .gitignore엔 걸려 있지만 .easignore에선 일부러 빼지 않았으므로 로컬에 두기만 하면 EAS 클라우드 빌드에 자동으로 올라간다(google-services.json과 동일한 방식).딸린 확인: iOS 앱을 추가하면 iOS용 OAuth 클라이언트 ID가 새로 생긴다. 서버(bedar-api)가 구글 토큰을 검증할 때 웹 클라이언트 ID만 허용하고 있다면 iOS 것도 허용 목록에 넣어야 한다.
P0-2 OCR 라이브러리가 요구하는 iOS 최소 버전이 프로젝트 설정보다 높다 "iOS 15.5 이상에서만 쓸 수 있다"는 라이브러리를 iOS 15.1 설정에 넣어서 라이브러리 설치(pod install)가 거부된다. 그런데 그 버전을 올릴 도구가 레포에 없다.
등장하는 것
- @react-native-ml-kit/
text-recognition - 사진 속 한글을 기기 안에서 읽어내는 OCR. 이 앱에선 주소 검색에 쓴다 — 주소가 적힌 사진을 찍으면 글자를 뽑아 주소로 바꾼다(
src/tools/ocr.ts→AddressSearchModal). 한국어를 못 읽는 사용자를 위한 기능이다. - CocoaPods / podspec
- iOS용 라이브러리 설치 도구. 각 라이브러리의
podspec파일에 "iOS 몇 버전부터 쓸 수 있다"가 적혀 있고, 앱 설정이 그보다 낮으면 설치를 거부한다. - deployment target
- 앱이 지원하는 iOS 최소 버전. Expo SDK 54의 기본값은 15.1이다.
- expo-build-properties
- 지금 설치돼 있지 않은 공식 플러그인.
app.json에서 iOS 최소 버전 같은 네이티브 빌드 설정을 바꿀 수 있게 해준다. CNG 방식에선 이게 유일한 통로다.
왜 iOS에서 깨지나
Android는 이런 제약이 없어 그냥 빌드됐다. iOS는 CocoaPods가 The platform of the target ... is not compatible with RNMLKitTextRecognition으로 설치 자체를 중단한다.
근거
@react-native-ml-kit/text-recognition/RNMLKitTextRecognition.podspec
s.platforms = { :ios => "15.5" } ← 요구
expo-modules-core/ExpoModulesCore.podspec
s.platforms = { :ios => '15.1' } ← 현재 프로젝트
npx expo install expo-build-properties 로 설치한 뒤 app.json의 plugins에 아래를 추가한다.
["expo-build-properties", { "ios": { "deploymentTarget": "15.5" } }]
최소 버전을 15.5로 올리면 iOS 15.0~15.4 기기는 앱을 설치할 수 없게 된다(점유율은 매우 낮다). OCR을 iOS에서 포기하면 이 문제 자체가 없어진다 — P2-6 참고.P0-3 빌드 설정 파일(eas.json)에 iOS 항목이 아예 없다 지금까지 안드로이드만 빌드해왔기 때문에, iOS는 "어떻게 빌드할지"가 한 줄도 안 적혀 있다. 애플 개발자 계정부터 새로 필요하다.
등장하는 것
- eas.json
- EAS Build(Expo의 클라우드 빌드 서비스)에 "이 프로필로 빌드해라"를 적어두는 파일. 맥 없이 iOS 앱을 만들 수 있게 해준다.
- 프로비저닝 프로파일 · 인증서
- 애플이 발급하는 서명 자격. 이게 있어야 실기에 설치되고 스토어에 올라간다. EAS가 대신 만들어줄 수 있다.
- APNs 인증키(.p8)
- 애플 푸시 서버에 접속하는 열쇠. 이 앱은 Expo Push를 쓰므로 이 키를 EAS credentials에 등록해야 푸시가 나간다.
- UDID 등록
- iOS는 아무 폰에나 테스트 앱을 못 깐다. 기기 고유번호를 미리 등록해야 한다(
eas device:create).
근거
eas.json build.development / preview / production → 전부 "android" 키만
submit.production → android(google-play-key.json) 만
필요한 준비물
- Apple Developer Program 멤버십(연 $99) — 없으면 실기 설치조차 불가
- App Store Connect에 앱 레코드 생성 + Bundle ID
kr.bedar.app등록 - 배포 인증서 / 프로비저닝 프로파일 (EAS 대행 가능)
- APNs 인증키(.p8)를 EAS credentials에 업로드
- 테스트할 iPhone의 UDID 등록
- 시뮬레이터로 확인하려면
"ios": { "simulator": true }프로필 별도
P1-1 앱이 꺼져 있을 때 푸시를 받는 스위치가 안 켜져 있다 iOS는 "백그라운드에서 푸시를 받겠다"고 앱 설정에 명시해야만 꺼진 앱을 깨운다. 그 항목이 빠져 있어 백그라운드 통화 수신 코드가 실행 자체를 안 한다.
등장하는 것
- Info.plist
- iOS 앱의 설정표. 권한 안내 문구, 백그라운드 동작 허용, 되돌아올 주소 등이 들어간다. 안드로이드의
AndroidManifest.xml에 해당한다. - UIBackgroundModes
- Info.plist 안의 항목. 앱이 화면에 없을 때도 뭘 할 수 있는지를 나열한다.
remote-notification은 "푸시로 앱을 깨울 수 있음",audio는 "소리를 계속 낼 수 있음"이다. - expo-notifications
- 푸시 권한 요청 + Expo Push 토큰 발급 + 안드로이드 알림 채널 등록을 담당. 서버는 이 토큰으로 알림을 쏜다.
- setBackgroundMessageHandler
- 앱이 백그라운드·종료 상태일 때 푸시가 오면 실행되는 함수. 이 앱은 여기서 통화 수신 알림을 띄운다(
src/tools/call-bg-handler.ts).index.js가 앱보다 먼저 등록할 만큼 중요한 경로다.
왜 iOS에서 깨지나
expo-notifications 플러그인은 옵션을 켜야만 UIBackgroundModes를 넣어준다. app.json:98–104에 그 옵션이 없어 항목이 통째로 빠진다. 그러면 iOS는 푸시가 와도 앱을 깨우지 않고, 위 핸들러는 한 번도 실행되지 않는다. 백그라운드·종료 상태의 call.invite(수신)·call.cancel(취소)이 전부 무동작.
근거
// expo-notifications/plugin/build/withNotificationsIOS.js:27 if (!enableBackgroundRemoteNotifications) { return config; // ← 그냥 리턴. UIBackgroundModes 를 안 넣는다 }
["expo-notifications", { ..., "enableBackgroundRemoteNotifications": true }]
다만 이걸 켜도 iOS 통화 수신은 여전히 안 된다 — 띄울 수신 화면 자체가 없기 때문이다(P1-2).P1-2 통화 수신 화면·알림·오버레이가 전부 Android 전용 — iOS엔 존재하지 않는다 고객이 상담 전화를 받는 UI 일체가 Kotlin으로 짠 안드로이드 전용 코드다. iOS에선 크래시도 안 나고 그냥 아무 일도 안 일어난다. 애플 방식(CallKit)으로 새로 만들어야 하고 서버 발송 경로까지 바뀐다.
등장하는 것
- plugins/with-call-notification
(자체 제작) - Kotlin 파일 4개를 안드로이드 프로젝트에 심고, 앱 시작 코드에 등록하는 플러그인. 수신 알림·잠금화면 표시·통화 중 오버레이가 전부 여기서 나온다.
- CallStyle 알림
- 안드로이드가 제공하는 전화 전용 알림 양식(큰 응답/거절 버튼). 카카오톡 통화 알림이 이 모양이다.
- fullScreenIntent
- 안드로이드에서 잠금화면 위에 전체화면 수신 화면을 강제로 띄우는 방법. iOS엔 같은 개념이 없다.
- SYSTEM_ALERT_WINDOW
- 안드로이드의 "다른 앱 위에 표시" 권한. 통화 중 상단에 떠 있는 바를 이걸로 그린다. iOS엔 대체 수단이 아예 없다.
- CallKit
- 애플이 제공하는 "진짜 전화처럼 보이는 수신 화면" 프레임워크. 서드파티 앱이 잠금화면 전화 UI를 쓰려면 이것뿐이다.
- PushKit / VoIP push
- CallKit 전용 특수 푸시. 일반 푸시보다 우선 전달되지만, 받으면 반드시 CallKit을 띄워야 하고 안 띄우면 OS가 이후 수신을 끊어버린다.
왜 iOS에서 깨지나
플러그인이 하는 일은 세 가지 — Kotlin 파일 복사, 앱 시작 코드에 등록, 액티비티에 잠금화면 옵션 추가. 전부 안드로이드 전용 API다. iOS 대응물은 0개.
JS 쪽은 옵셔널 체이닝(?.)으로 호출해서 크래시는 나지 않는다. 대신 조용히 아무 일도 안 일어난다 — 이게 더 나쁘다. 로그도 안 남는다.
근거
src/tools/call-notification.ts:17,29 NativeModules.CallNotification → undefined src/tools/call-overlay.ts:26 NativeModules.CallOverlay → undefined src/tools/call-permissions.ts:34,47 hasNative → false
결과적으로 iOS에서 사라지는 것: 수신 알림(응답/거절 버튼), 잠금화면 풀스크린 수신 화면, 통화 중 오버레이 바.
조치 — 이게 iOS 최대 공수다
같은 경험을 내려면 CallKit + PushKit을 새로 붙여야 한다. 그리고 이건 앱만의 일이 아니다 — VoIP push는 Expo Push로 못 보내고 APNs의 VoIP 토픽(kr.bedar.app.voip)에 별도 인증서로 직접 쏴야 하므로, bedar-api / bedar-admin의 통화 invite 발송 경로를 고쳐야 한다.
P1-3 통화 중 홈 버튼을 누르면 소리가 끊긴다 iOS는 "화면에서 내려가도 소리를 계속 내겠다"고 명시해야 한다. WebRTC 플러그인은 권한 문구만 넣어줄 뿐 그 항목을 안 넣는다.
등장하는 것
- WebRTC
- 브라우저·앱에서 쓰는 실시간 음성/영상 통화 엔진. 이 앱의 CS 상담 통화가 이걸로 돌아간다.
- @config-plugins/
react-native-webrtc - WebRTC를 iOS·Android 프로젝트에 설정해주는 공식 커뮤니티 플러그인.
- UIBackgroundModes: audio
- "화면에서 내려가도 소리를 계속 낼 수 있음" 표시. 음악 앱·통화 앱이 이걸 쓴다.
왜 iOS에서 깨지나
플러그인이 iOS에 하는 일은 두 가지뿐이다 — 카메라/마이크 권한 문구 넣기, bitcode 끄기. 백그라운드 오디오 항목은 넣지 않으므로, 통화 중 홈 버튼을 누르면 OS가 오디오 세션을 정지시킨다.
근거
// node_modules/@config-plugins/react-native-webrtc/build/withWebRTC.js config = withPermissions(config, _props); // iOS: 카메라/마이크 문구만 config = withBitcodeDisabled(config); // iOS: bitcode off // 나머지는 전부 Android 권한 목록
app.json의 ios.infoPlist에 UIBackgroundModes: ["audio"]를 추가한다. 수신까지 하려면 "voip"도 함께 넣어야 한다(P1-2와 같이 처리).P1-4 통화 오디오 엔진 초기화 코드가 Android에만 심어져 있다 안드로이드에선 이걸 빼먹어 통화 첫 시도에 크래시가 났던 이력이 있다. iOS엔 대응 코드가 없는데, iOS도 같은 초기화가 필요한지는 실기에서 확인해야 한다.
등장하는 것
- @livekit/react-native
- CS 상담 음성통화 자체. 통화방 접속, 마이크·스피커 연결을 담당한다(
src/tools/use-cs-call.ts). - plugins/with-livekit-setup
(자체 제작) - 앱 시작 지점에
LiveKitReactNative.setup()한 줄을 주입하는 플러그인. 안드로이드에선 이게 없으면 통화 시도 시 네이티브 크래시가 났다(2026-06-23 실측).
왜 확인이 필요한가
이 플러그인은 안드로이드의 MainApplication.onCreate만 건드린다. iOS엔 대응 mod가 없다. iOS 쪽에서도 별도 오디오 세션 설정이 필요한지는 문서만으로는 확답이 안 되고, 실기 통화 1회면 판별된다.
AudioSession 설정을 추가해야 한다.P1-5 푸시 관련 패키지 두 개가 iOS에서 서로 충돌할 수 있다 Expo 푸시와 Firebase 푸시가 둘 다 들어와 있다. iOS는 푸시 창구가 하나뿐이라 한쪽이 선점하면 다른 쪽이 토큰을 못 받는다. 게다가 실패해도 로그가 안 남는다.
등장하는 것
- APNs
- 애플 푸시 서버. iOS 푸시는 무슨 방식을 쓰든 결국 여기를 거친다.
- Expo Push 토큰
- Expo가 중계해주는 푸시 주소. 서버는 이 토큰 하나로 보내고 Expo가 iOS(APNs)·안드로이드(FCM)로 나눠 보낸다. 이 앱의 기본 방식이다.
- @react-native-firebase/
messaging - FCM을 직접 다루는 패키지. 이 앱에선 백그라운드 통화 수신 핸들러 하나 때문에 들어와 있다(P1-1).
- firebase.json
- RNFirebase의 iOS 세부 동작 설정 파일. 지금 레포에 없어서 전부 기본값으로 돈다.
왜 확인이 필요한가
- RNFirebase messaging 플러그인은 Android 설정만 건드린다 —
plugin/build/android/만 있고 iOS 디렉터리가 없다. - iOS에서 Expo Push 토큰을 받으려면 먼저 APNs 토큰을 받아야 하는데, RNFirebase가 그 창구를 선점하면 실패할 수 있다.
가장 큰 문제 — 실패가 안 보인다
// src/tools/push-token.ts:84-86 } catch { return null; // ← 로그도 안 남는다 }
catch에 임시 로그를 넣을 것. 안 넣으면 iOS에서 푸시가 통째로 안 와도 원인을 추적할 단서가 없다.P1-6 푸시 환경이 "개발용"으로 고정돼 있을 수 있다 iOS 푸시는 개발용·운영용 주소가 다르다. 설정을 안 하면 개발용이 기본값이라, 스토어 빌드에서 운영 푸시가 전부 실패하는 전형적인 함정이다.
등장하는 것
- entitlements
- 애플이 승인해야 쓸 수 있는 기능 목록 파일(푸시·CallKit 등). 서명 자격과 짝이 맞아야 앱이 실행된다.
- aps-environment
- entitlements 안의 항목.
development면 개발용(샌드박스) 푸시 서버,production이면 운영 서버를 쓴다. 둘은 토큰이 호환되지 않는다.
왜 확인이 필요한가
expo-notifications 플러그인의 기본값이 development이고, app.json에 별도 지정이 없다. 스토어·TestFlight 빌드에 이 값이 그대로 남으면 샌드박스 토큰이 발급돼 운영 푸시가 전부 실패한다.
다만 Xcode의 exportArchive 단계가 서명 자격 기준으로 이 값을 덮어쓰는 경우도 있어, 실제로 나온 .ipa의 entitlements를 직접 열어보는 것이 유일한 확답이다.
근거
// withNotificationsIOS.js:9 mode 기본값 = 'development'
config.modResults['aps-environment'] = mode;
["expo-notifications", { "mode": "production" }]로 덮어쓴다(app.config.js 분기 필요). 먼저 첫 TestFlight 빌드의 entitlements를 확인한 뒤 결정하면 된다.P1-7 통화 중 음성 읽어주기(TTS)와 통화 오디오가 서로 먹을 수 있다 iOS는 앱 전체가 오디오 설정을 하나만 가진다. 통화 엔진·벨소리 모듈·TTS 세 개가 그 하나를 서로 뺏는 구조라 Android처럼 그냥 돌아갈 가능성은 낮다.
등장하는 것
- AVAudioSession
- iOS의 오디오 설정 하나짜리 전역 자원. "지금 앱이 녹음 중인지, 재생 중인지, 스피커인지 수화기인지"를 한 곳에서 정한다. 안드로이드는 스트림이 나뉘어 있어 이 문제가 없다.
- react-native-tts
- 통화 중 번역된 상담원의 말을 음성으로 읽어주는 기능(
use-cs-call.ts). - react-native-incall-manager
- 통화 연결음·벨소리, 얼굴을 대면 화면 끄기(근접센서), 통화 중 화면 꺼짐 방지.
- LiveKit(WebRTC)
- 통화 본체. 마이크를 잡고 있는 주체다.
왜 확인이 필요한가
WebRTC가 오디오 세션을 playAndRecord로 점유한 상태에서 TTS를 돌리면, 한쪽이 무음이 되거나 통화 오디오가 끊기는 사례가 흔하다. 여기에 incall-manager도 같은 세션을 직접 만지므로 경합원이 하나 더 있다.
즉 iOS 통화는 LiveKit / InCallManager / TTS 3자의 오디오 세션 조율을 실기에서 맞춰야 한다. 코드 수정 없이 넘어갈 가능성은 낮다고 본다.
P1-8 LINE 로그인이 끝나고 앱으로 되돌아오는 경로를 iOS에서 재검증해야 한다 이미 한 번 크게 헤맸던 부분이다. 앱이 꺼져 있다 켜지는 경우와 떠 있는 경우의 처리가 다르고, iOS는 그 타이밍이 Android와 다르다.
등장하는 것
- 딥링크 (
bedar://) - 웹에서 앱으로 되돌아오는 주소. LINE 로그인은 네이티브 SDK 없이 웹 OAuth + 이 딥링크로 처리한다.
- warm / cold start
- 앱이 이미 떠 있을 때 돌아오는 경우(warm)와 꺼져 있다 새로 켜지며 돌아오는 경우(cold). 받는 코드가 서로 다르다.
왜 확인이 필요한가
이 앱은 딥링크 소비자를 반드시 하나만 둬야 한다는 제약이 있고(warm은 openAuthSessionAsync, cold는 _layout의 getInitialURL), 과거에 이걸 어겨 한참 헤맨 이력이 있다. iOS는 cold start 전달 타이밍이 Android와 달라 같은 코드로 같은 결과가 난다는 보장이 없다.
kr.bedar.app) 등록이 안 돼 있으면 로그인 자체가 거부된다. 실기 테스트 전에 먼저 등록할 것.P2-1 Apple 로그인이 없어서 심사에서 확정 리젝된다 구글·페이스북·LINE 로그인을 제공하는 앱은 Apple 로그인도 반드시 같이 제공해야 한다는 애플 규정(4.8). 지금 Apple 버튼은 "준비중"으로 숨겨져 있다.
등장하는 것
- 심사지침 4.8
- 애플의 App Store 심사 규정. 제3자 소셜 로그인을 제공하면 Sign in with Apple도 동등하게 제공해야 한다.
- expo-apple-authentication
- Apple 로그인을 붙이는 공식 패키지. 현재 설치돼 있지 않다.
현재 상태
제공 중: Google · Facebook · LINE (+ 전화·이메일). Apple은 목록에 정의만 돼 있고 comingSoon 플래그로 화면에서 걸러진다.
근거
// app/login/index.tsx:276-281 { key: 'apple', ..., comingSoon: true }, // :323 providers.filter((p) => !p.comingSoon) ← 화면에서 아예 숨김
회피책은 소셜 3종을 iOS에서 전부 숨기는 것뿐인데, 그러면 기존 소셜 가입자가 iOS에서 로그인할 방법이 없어진다.
P2-2 광고 식별자를 수집하면서 동의 팝업은 띄우지 않는 설정 iOS는 광고 추적에 동의 팝업이 의무다. 지금 설정은 수집은 켜고 팝업은 끈 상태라 심사에서 문제가 될 수 있다.
등장하는 것
- react-native-fbsdk-next
- 이 앱에서 두 가지 일을 한다 — 페이스북 로그인(
social-auth.ts)과 Meta 광고 전환 이벤트 전송(ads-events.ts, 주문 완료 시). - IDFA
- iOS의 광고 식별자. 광고 성과를 사용자 단위로 추적하는 데 쓴다.
- ATT (App Tracking Transparency)
- "이 앱이 다른 앱·웹의 활동을 추적하도록 허용할까요?" 동의 팝업. iOS 14.5부터 추적 전에 반드시 띄워야 한다.
왜 문제인가
수집 스위치는 켜져 있는데 ATT 문구가 false라 팝업이 뜨지 않는다. 동의 없이 추적 데이터를 수집하는 구성으로 읽히고, 개인정보 신고 내용과 어긋나면 리젝 사유가 된다.
근거
// app.json:109-119 "advertiserIDCollectionEnabled": true, ← IDFA 수집 ON "autoLogAppEventsEnabled": true, "iosUserTrackingPermission": false ← ATT 문구 없음 = 팝업 없음
iosUserTrackingPermission에 안내 문구를 넣고 ATT 팝업을 띄운다 — 동의율만큼만 추적된다.(b)
advertiserIDCollectionEnabled: false로 내린다 — 팝업이 필요 없어지지만 Meta 광고 성과 추적이 줄어든다. 마케팅 판단이 필요하다.P2-3 개인정보 신고 파일이 없는 서드파티 라이브러리가 7개 2024년부터 애플은 라이브러리마다 "무슨 정보를 쓰는지" 신고 파일을 요구한다. 없으면 업로드 후 경고 메일이 오고 항목에 따라 거절된다.
등장하는 것
- privacy manifest
(PrivacyInfo.xcprivacy) - 라이브러리가 어떤 개인정보와 시스템 API를 쓰는지 애플에 신고하는 파일. 라이브러리 제작자가 자기 패키지에 넣어야 한다.
- ITMS-91061
- 이 파일이 빠졌을 때 App Store Connect가 보내는 경고 코드.
- commonly used SDK
- 애플이 지정한 "특히 신고가 중요한 라이브러리" 목록. Facebook SDK가 여기 들어있어 서명된 신고 파일이 필수다.
근거 — 실제 node_modules 확인 결과
react-native-tts 4.1.1 없음 react-native-incall-manager 4.2.1 없음 @react-native-ml-kit/text-recognition 2.0.0 없음 react-native-fbsdk-next 13.4.3 없음 (※) @livekit/react-native 2.11.1 없음 @livekit/react-native-webrtc 144.1.1 없음 expo-speech-recognition 3.1.3 없음 ※ 네이티브 FBSDK pod 자체에 들어있을 수 있어 pod 설치 후 재확인 필요
PrivacyInfo.xcprivacy를 직접 넣는 config plugin을 만든다.P2-4 iPad 지원이 켜져 있어 iPad 스크린샷과 동작이 의무가 된다 UI는 폰 전용으로 만들었는데 설정만 iPad 지원으로 돼 있다. 심사자가 iPad에서 직접 돌려본다.
왜 문제인가
app.json:12의 supportsTablet: true를 켜두면 App Store에 iPad 스크린샷 등록이 필수가 되고, 심사자가 iPad에서 실제로 실행해본다. 레이아웃이 폰 기준이라 깨져 보이면 그 자체가 리젝 사유다.
false로 내리는 게 안전하다. 나중에 켜는 건 언제든 가능하다.P2-5 암호화 수출 규정 답변이 설정에 없어 업로드마다 막힌다 한 줄이면 끝나는 설정인데, 없으면 업로드할 때마다 질문이 뜨고 답하기 전까지 TestFlight 배포가 멈춰 있다.
등장하는 것
- ITSAppUsesNonExemptEncryption
- "이 앱이 규제 대상 암호화를 쓰는가"를 미리 답해두는 항목. HTTPS만 쓰는 앱은
false가 정답이다.
app.json의 ios에 "config": { "usesNonExemptEncryption": false }를 추가한다.P2-6 OCR 모델 5개 언어분이 전부 앱에 들어가 용량이 크게 는다 Android는 구글 서비스가 모델을 따로 내려받지만, iOS는 앱 파일 안에 통째로 들어간다. 한국어 하나만 쓰는데 중국어·일본어·데바나가리까지 강제로 딸려온다.
왜 문제인가
podspec이 5개 스크립트를 선택 불가로 전부 의존한다. 스크립트당 수십 MB 단위라 앱 용량이 크게 늘고, 셀룰러 다운로드 한도와 심사 대기시간에 영향을 준다. 실제 이 앱이 쓰는 건 한국어 하나다(src/tools/ocr.ts, TextRecognitionScript.KOREAN).
근거
# RNMLKitTextRecognition.podspec — 선택 불가, 5개 전부 의존 GoogleMLKit/TextRecognition 8.0.0 # Latin GoogleMLKit/TextRecognitionChinese 8.0.0 GoogleMLKit/TextRecognitionDevanagari 8.0.0 GoogleMLKit/TextRecognitionJapanese 8.0.0 GoogleMLKit/TextRecognitionKorean 8.0.0 # ← 실제로 쓰는 건 이것뿐
(b) iOS 내장
Vision 프레임워크로 교체 — 용량 0MB, 무료, 한국어 인식도 지원하지만 네이티브 모듈을 따로 만들어야 한다. src/tools/ocr.ts가 이미 교체 지점을 격리해둬서 호출부는 안 건드려도 된다.(c) iOS에선 OCR을 빼고 서버 비전 폴백만 쓴다 — 코드상 폴백 경로가 이미 있다(
recognizeAddressViaServer).정확한 증가분은 첫 아카이브의 App Thinning 리포트로 측정한다.
P3-1 ios.bitcode 설정이 이제 의미가 없다 Xcode 14부터 폐기된 기능이고, WebRTC 플러그인이 같은 처리를 이미 한다.
app.json:19. 무해하지만 중복이라 지워도 된다. (bitcode는 앱을 애플 서버에서 재컴파일하게 하던 옛 기능이다.)
P3-2 안드로이드 권한 목록 13개가 통째로 두 번 적혀 있다 iOS와는 무관하지만 설정 파일을 읽을 때 눈에 걸린다.
app.json:31–59에 같은 권한 13개가 정확히 두 번 나열돼 있다. 동작에는 영향이 없다(빌드 시 중복 제거됨).
P3-3 modules/digital-ink 가 빌드 찌꺼기만 남은 빈 폴더다 손글씨 인식을 시도했던 흔적으로 보인다. 소스도 없고 코드에서 참조하지도 않는다.
android/build/만 남아 있고 소스·expo-module.config.json·JS 참조가 전부 0이다. Expo가 자동 연결하는 조건(설정 파일)이 없어 빌드엔 영향 없다. 삭제 대상.
P3-4 음성 읽어주기(TTS) 라이브러리가 두 벌 들어있다 단어장은 expo-speech, 통화는 react-native-tts를 쓴다. iOS에선 둘 다 같은 애플 엔진을 부른다.
expo-speech는 단어장 발음 재생(WordbookCreatePane, wordbook/[folderId]), react-native-tts는 통화 중 번역 읽어주기(use-cs-call.ts)에 쓰인다. iOS에선 둘 다 AVSpeechSynthesizer를 부른다.
합칠 여지가 있지만, 통화 쪽은 오디오 세션 제어 때문에 지금 라이브러리가 필요할 수 있다 — P1-7 정리하면서 같이 판단할 것.
P3-5 KCP 결제 웹뷰가 외부 앱으로 넘어가는 처리를 안 한다 결제 중 PASS·은행 앱으로 넘어가야 하는 경우 iOS에선 그냥 멈춘다. 다만 이 경로가 아직 쓰이는지부터 확인이 필요하다.
components/kcp-webview-modal.tsx에 onShouldStartLoadWithRequest(웹뷰가 앱 주소로 이동하려 할 때 가로채는 훅)도 Linking.openURL(외부 앱 실행)도 없다. iOS에서 인증 앱으로 넘어가야 하는 경로면 동작하지 않는다.
먼저 확인할 것: 현행 결제는 내부 예치금 방식이라 이 웹뷰가 실제로 열리는 경로가 남아 있는지부터 봐야 한다. 안 쓰이면 P3-5는 자동 소멸이다.