ITMS-90338 Non-public API usage 해결 방법 (Expo)
글: Mehdi Azizi
게시, 9분
ITMS-90338은 Apple이 비공개로 보는 메서드, 함수, 클래스를 빌드가 참조한다는 뜻이에요. 원인 의존성을 찾아 업데이트한 뒤 다시 빌드하세요.
ITMS-90338은 Apple이 빌드를 대상으로 하는 자동 검사예요. 바이너리가 Apple이 비공개로 간주하는 API(Objective-C 메서드, 함수, 클래스)를 참조한다는 뜻이에요. 해결 방법은 이메일에 적힌 파일을 찾고, 그 이름이 들어 있는 의존성까지 거슬러 올라가서 업데이트하거나 내 메서드 이름을 바꾼 뒤 다시 빌드하는 거예요.
Expo에서는 이 이름이 거의 항상 네이티브 라이브러리에서 나오고, JavaScript에서 나오는 경우는 드물어요. 2026년 10월 15일에 저는 미니 게임 알람 앱 Solys(Expo SDK 57)의 시뮬레이터 빌드와 Xcode 26.2의 iOS SDK로 이 가이드의 분석 명령을 직접 실행해 봤어요. 이메일을 읽는 법, 원인을 찾는 법, 오탐을 알아보는 법을 정리했어요.
ITMS-90338 오류는 무슨 뜻인가요?
메시지 제목은 "ITMS-90338: Non-public API usage"예요. Apple이 바이너리에서 무엇을 찾았느냐에 따라 세 가지 변형이 있어요.
- "references non-public selectors":
boldFontOfSize:같은 Objective-C 메서드 이름 - "references non-public symbols":
__dyld_get_shared_cache_range처럼 앞에 밑줄이 붙은 함수 - "contains or inherits from non-public classes": 클래스 이름
각 변형에는 경로가 나오고, 그 뒤에 이름 목록이 이어져요. 본문에는 두 가지 조언도 있어요. 메서드 이름이 비공개 API와 같다면 이름을 바꾸고, 그 이름이 들어 있는 정적 라이브러리를 제거하라는 거예요. 2026년 9월 이메일에는 인코딩된 Swift 이름을 읽을 수 있게 바꿔 주는 xcrun swift-demangle도 안내돼 있었어요.
이 검사의 근거는 가이드라인 2.5.1이에요. 앱은 공개 API만 사용할 수 있어요. Apple Developer Program 라이선스 계약 3.3.1조도 같은 내용, 즉 비공개 API 호출 금지를 다시 명시해요. 이 규칙에 대한 사람의 심사는 가이드라인 2.5.1 가이드에서 자세히 다뤄요. 여기서는 업로드 오류를 다뤄요.
다른 ITMS 오류처럼 아무도 앱을 열어 보지 않았어요. Apple이 빌드를 처리하는 동안 바이너리를 분석한 거예요. Apple은 비공개 API 목록을 공개하지 않아요. 이메일에 적힌 이름이 유일하게 믿을 수 있는 목록이에요.
경로 읽기: SDK인가, 내 실행 파일인가?
목록 앞에 적힌 경로가 어느 파일을 찾아봐야 하는지 알려 줘요.
Frameworks/Name.framework/Name: 이름이 앱에 포함된 동적 프레임워크, 즉 특정 SDK 안에 있어요. Solys 빌드에서는 이 폴더에 프레임워크 14개가 있어요. Hermes, React Native, 미리 컴파일되어 제공되는 Expo 모듈, 이미지 라이브러리 SDWebImage예요.- 앱 이름만 있는 경우: 이름이 메인 실행 파일 안에 있어요. Expo에서는 내 네이티브 코드와 소스에서 컴파일된 pod가 기본적으로 정적 링크되어 이 파일에 모여요. 그래서 어떤 네이티브 라이브러리든 원인이 될 수 있어요.
판단 트리: 이름에서 해결책까지
위에서부터 순서대로 보세요. 내 상황에 맞는 첫 번째 줄이 단서예요.
| 보이는 것 | 찾아볼 곳 | 해결 |
|---|---|---|
$s 또는 _$s로 시작 | xcrun swift-demangle | 트리를 다시 따라가기 |
| IPA에서 찾을 수 없음 | 번들에 grep | 빌드 다시 제출 |
| iOS에서 공개된 이름 | iOS SDK 헤더 | 빌드 다시 제출 |
API_UNAVAILABLE(ios) | iOS SDK 헤더 | 패키지 업데이트 |
Frameworks/X 안 | X를 제공하는 SDK | SDK 업데이트 |
| 셀렉터, 내 앱 | pod와 네이티브 모듈 | 접두사 또는 업데이트 |
심볼 _name, 내 앱 | nm -m, 그다음 pod | 호출 교체 |
| 클래스, 내 앱 | 내 클래스와 pod | 클래스에 접두사 |
"빌드 다시 제출" 줄은 오탐을 뜻해요. 아래에서 다뤄요.
1단계: 제출한 빌드 열기
프로젝트가 아니라 Apple에 실제로 보낸 파일로 작업하세요. expo.dev의 빌드 페이지에서 IPA를 내려받고 Mac에서 실행하세요.
unzip -q MyApp.ipa -d ipa
ls ipa/Payload/MyApp.app/Frameworks
otool -L ipa/Payload/MyApp.app/MyApp | grep PrivateFrameworks
두 번째 줄은 포함된 프레임워크를 나열해요. 세 번째 줄은 Apple 비공개 프레임워크로의 직접 링크를 찾는데, 아무것도 출력되지 않아야 해요. Solys에서는 otool -L이 실행 파일에서도, 14개 프레임워크에서도 비공개 프레임워크를 찾지 못했어요.
2단계: 이름이 들어 있는 파일 찾기
넓게 시작하세요. 앞의 밑줄을 뺀 이름으로 번들 전체를 검색해요.
grep -rl "dyld_get_shared_cache_range" ipa/Payload/MyApp.app
이 명령은 해당 텍스트가 들어 있는 파일을 나열해요. 그다음 찾은 파일(Binary로 표기)에서 이름의 종류(셀렉터, 심볼, 클래스)에 따라 좁혀 보세요.
strings -a Binary | grep -x "boldFontOfSize:"
nm -m Binary | grep "_dyld_get_shared_cache_range"
nm -m Binary | grep '_OBJC_CLASS_$_ClassName'
셀렉터는 nm이 아니라 strings로 찾으세요. Solys의 React 프레임워크에서 nm은 -[RCTRootView initWithBridge:moduleName:initialProperties:]처럼 클래스와 함께 메서드를 보여 주고, initWithBridge: 셀렉터만 따로 보여 주지는 않아요. strings는 찾아내요.
심볼은 줄 끝을 읽으세요. Solys에서 nm -m은 예를 들어 (undefined) external __dyld_get_image_name (from libSystem)을 출력해요. 바이너리가 이 함수를 포함하지 않고 호출만 하며, libSystem이 제공한다는 뜻이에요. 다음 단계에서 보듯 이 함수는 공개 API예요.
3단계: 이름이 정말 비공개인지 확인하기
iOS SDK 헤더에는 공개 API가 정의돼 있어요. 몇 초면 이름이 거기 있는지, 어떤 플랫폼에서 쓸 수 있는지 알 수 있어요. 심볼이라면 컴파일 시 붙는 밑줄을 먼저 빼세요. _SCDynamicStoreCreate는 SCDynamicStoreCreate가 돼요.
SDK=$(xcrun --sdk iphoneos --show-sdk-path)
grep -rn "SCDynamicStoreCreate" "$SDK/usr/include" "$SDK"/System/Library/Frameworks/*/Headers
2026년 10월 15일, iOS SDK 26.2로 실제 ITMS-90338 이메일에 나온 이름 네 개를 확인한 결과예요.
| 언급된 이름 | iOS SDK에서 | 해석 |
|---|---|---|
| _dyld_get_shared_cache_range | 없음 | 비공개 API |
| boldFontOfSize: | 없음 | 비공개 또는 충돌 |
| SCDynamicStoreCreate | API_UNAVAILABLE(ios) | macOS 전용 |
| isMainFrame | WebKit 프로퍼티 | 공개 |
이름이 있고 iOS에서 사용 가능하다면 오탐일 가능성이 커요. 이름이 없다면 비공개 API이거나 이름 충돌이에요. 한계가 두 가지 있어요. Swift 전용 공개 API는 이 헤더에 나오지 않아요. 그리고 setDelegate: 같은 접근자는 delegate 프로퍼티로 검색하세요.
4단계: 패키지까지 거슬러 올라가기
경로에 프레임워크가 적혀 있다면, 그 이름으로 pod를 찾고 다시 npm 패키지를 찾아요.
grep -n "FrameworkName" ios/Podfile.lock
npm explain package-name
직접 추가한 적이 없는 패키지라면 npm explain이 어떤 의존성이 설치했는지 알려 줘요.
경로에 실행 파일이 적혀 있다면, 네이티브 소스와 pod에서 이름을 찾아요.
grep -rl --include='*.m' --include='*.mm' --include='*.swift' --include='*.h' \
"boldFontOfSize" node_modules modules
grep -rl "boldFontOfSize" ios/Pods
폴더 목록은 상황에 맞게 바꾸세요. modules는 로컬 Expo 모듈 폴더예요. ios/Pods 폴더는 npx expo prebuild -p ios를 실행하면 생기는데, 이 명령은 Mac에서 pod도 설치해요. 주의하세요. 이 명령은 ios 폴더를 다시 만들 수 있어요. 직접 수정한 적이 있다면 복사본에서 작업하세요.
ios/Pods 검색은 소스 검색만큼 중요해요. Solys에서는 이 폴더에 .xcframework 형식으로 미리 컴파일된 프레임워크 15개가 있어요. Expo 모듈, Hermes, React Native, 서드파티 SDK예요. 소스 파일만 검색하면 이 코드를 놓쳐요.
흔한 원인
너무 오래됐거나 너무 최신인 서드파티 SDK. 2026년 9월, 한 Flutter 개발자가 보안 SDK 때문에 거절당했다고 보고했어요. 8.2.3 버전이 __dyld_get_shared_cache_range를 호출했어요. 8.2.2 버전은 통과했고, 제작사는 수정이 나올 때까지 그 버전으로 돌아가라고 권했어요.
문서화되지 않은 API를 호출하는 네이티브 모듈. 내가 만든 모듈, 플러그인, 작은 라이브러리가 포럼에서 찾은 UIKit 내부 메서드를 쓸 수 있어요. 검사는 이름으로 이를 찾아내요.
이름 충돌. 내 메서드나 클래스가 우연히 Apple 비공개 API와 같은 이름을 가진 경우예요. 메시지도 이 경우를 언급하며 이름을 바꾸라고 해요. Apple은 자사 클래스에 추가하는 메서드에 접두사를 붙이라고 권장해요. 예를 들어 소문자 세 글자 뒤에 밑줄을 붙이는 식이에요.
macOS에서는 공개지만 iOS에서는 아닌 API. 2024년에 한 .NET MAUI 앱이 _SCDynamicStoreCreate 때문에 거절당했어요. Apple은 이 함수를 문서화하고 있지만 macOS 전용이고, iOS SDK 헤더는 이를 API_UNAVAILABLE(ios, tvos, watchos)로 표시해요.
우회하지 말고 고치기
- 의존성을 업데이트하세요. Expo CLI 문서에 따르면 Expo가 관리하는 패키지는
npx expo install --check가 SDK와 맞지 않는 패키지를 알려 줘요. 나머지는 변경 내역을 읽어 보세요. - 새 버전이 그 이름을 들여왔다면 이전 버전으로 되돌리고 제작사에 알리세요.
- 방치된 라이브러리는 교체하거나 제거하세요.
- 메서드나 클래스 이름을 접두사를 붙여 바꾸세요.
이름을 숨기지 마세요. 문자열을 쪼개거나 우회 경로로 메서드를 호출하면 사용 사실이 가려질 뿐 고쳐지지 않아요. 여전히 2.5.1 규칙과 계약에 위반되고, 그때는 내 계정이 위험해져요.
그다음 새 빌드 번호로 다시 빌드하세요. 원격 업데이트로는 부족해요. 이름이 컴파일된 네이티브 코드 안에 있기 때문이에요. 제출 전에 새 IPA로 2단계를 다시 실행하세요. 이름이 더 이상 나오지 않아야 해요.
오탐인 경우
Apple의 검사가 틀릴 때도 있어요. 2022년 6월 6일, React Native와 Flutter 앱들이 setNavigationDelegate:나 isMainFrame 같은 WebKit 공개 이름 때문에 ITMS-90338을 받았어요. 거의 바뀐 게 없는 업데이트도 마찬가지였어요. 다음 날 Apple 엔지니어가 검증 쪽 문제가 해결됐다며 빌드를 다시 제출하라고 했어요. 이 스레드는 Expo GitHub 저장소에 남아 있어요.
다음 세 가지가 모두 해당하면 오탐일 가능성이 커요.
- 빌드가 마지막으로 승인된 빌드와 비슷해요.
- 2단계에서 이름을 찾지 못하거나, 3단계에서 iOS 공개 API로 확인돼요.
- 같은 날 다른 개발자들도 Apple 포럼에서 같은 오류를 보고해요.
이럴 때는 Apple 서비스 상태를 확인한 뒤 새 번호로 빌드를 다시 제출하세요. 오류가 계속되면 이메일 전문을 첨부해 Apple 개발자 지원에 문의하세요. 단, 이름 충돌은 Apple 입장에서 오탐이 아니에요. 이름을 바꾸세요.
다음 제출 전에
다른 ITMS 오류도 가이드가 있어요. Xcode 버전은 ITMS-90725, 개인정보 매니페스트는 ITMS-91053, 서명은 ITMS-90034를 보세요. 빌드가 승인되면 심사 전에 TestFlight로 설치해 보세요.
리뷰어의 거절은 좀 더 넓게 봐야 해요. 대응 방법은 Apple 앱 거절 페이지에, 가장 흔한 사유는 Apple이 앱을 거절하는 이유에 있어요.
제출 전에 준비 상태를 점검하고 싶다면 Review Kit에 제 체크리스트와 프로젝트를 검토해 주는 Claude Code 스킬을 모아 두었어요. 제출이 막혀서 사람의 눈이 필요하다면 저와 1시간 상담을 예약하세요. 화상으로 화면을 보면서 프로젝트를 함께 살펴봐요. 첫 출시를 준비 중이라면 무료 진단을 받아 보세요. 단계를 순서대로 정리해 주고 Apple 거절 방지 체크리스트도 보내 드려요.
자주 묻는 질문
ITMS-90338은 App Review 거절인가요?
아니에요. 빌드 처리 중에 사람의 심사 전에 이뤄지는 자동 검사예요. 거부된 빌드는 TestFlight에도, 심사에도 올라가지 않아요. 나중에 리뷰어가 비공개 API를 발견하는 경우는 가이드라인 2.5.1에 해당해요.
원격 업데이트로 ITMS-90338을 고칠 수 있나요?
아니요. 언급된 이름은 바이너리의 컴파일된 네이티브 코드 안에 있어요. 원격 업데이트는 JavaScript와 리소스만 바꿔요. 의존성을 고치고 다시 빌드한 뒤 제출하세요.
왜 Apple이 제가 작성한 적 없는 메서드를 언급하나요?
앱에 포함된 라이브러리에서 왔거나 이름 충돌 때문이에요. 의존성이 비공개 API와 같은 이름의 메서드를 선언한 경우죠. Apple은 소스 코드가 아니라 바이너리에 있는 이름을 읽어요. grep과 npm explain으로 패키지까지 거슬러 올라가세요.
API가 iOS에서 공개인지 어떻게 알 수 있나요?
iOS SDK 헤더에서 이름을 검색하고 사용 가능 여부를 확인하세요. 헤더 경로는 xcrun --sdk iphoneos --show-sdk-path로 알 수 있어요. Apple 문서 페이지에도 나와 있어요. macOS 전용으로 문서화된 함수는 iPhone에서 공개 API가 아니에요.
수정 후 빌드 번호를 바꿔야 하나요?
네. EAS가 자동으로 올리게 두세요. 같은 버전에 이미 제출한 번호는 ITMS-90189로 거부될 수 있어요.

