iOS Privacy Manifest와 앱 스토어 규정 대응

4분 분량 #iOS #PrivacyManifest #Expo #ReactNative #AppStore 이 글의 방문자 수

배경

2024년부터 Apple은 앱에서 사용하는 Required Reason API를 PrivacyInfo.xcprivacy 파일에 선언하도록 요구한다. 이를 누락하면 앱 심사에서 리젝된다. React Native(Expo) 앱에서 이 요구사항을 어떻게 처리했는지 정리한다.

PrivacyInfo.xcprivacy

iOS 프로젝트의 ios/app/ 디렉토리에 plist 형식의 Privacy Manifest 파일을 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>C617.1</string>
        <string>0A2A.1</string>
        <string>3B52.1</string>
      </array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>CA92.1</string>
        <string>1C8F.1</string>
        <string>C56D.1</string>
      </array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>35F9.1</string>
      </array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryDiskSpace</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>E174.1</string>
        <string>85F4.1</string>
      </array>
    </dict>
  </array>
  <key>NSPrivacyCollectedDataTypes</key>
  <array/>
  <key>NSPrivacyTracking</key>
  <false/>
</dict>
</plist>

각 API 카테고리와 사용 이유 코드의 의미:

FileTimestamp (파일 타임스탬프)

  • C617.1: 파일 타임스탬프로 DRM이나 라이선스 관리에서 사용
  • 0A2A.1: 앱 자체 파일의 생성/수정 시간 접근
  • 3B52.1: 파일 시스템의 파일 수정 날짜를 사용

React Native와 Expo가 번들 파일, 캐시, OTA 업데이트 등의 파일 작업에서 타임스탬프를 참조한다.

UserDefaults

  • CA92.1: 앱 자체의 UserDefaults 데이터 접근
  • 1C8F.1: 동일 앱 그룹 내 앱 간 데이터 공유
  • C56D.1: 서드파티 SDK의 UserDefaults 접근

React Native의 AsyncStorage, Expo의 SecureStore, Supabase의 세션 저장 등이 내부적으로 UserDefaults를 사용한다.

SystemBootTime (시스템 부팅 시간)

  • 35F9.1: 앱의 기능 시간 측정에 사용

react-native-reanimated, 성능 모니터링 등에서 부팅 시간을 참조한다.

DiskSpace (디스크 공간)

  • E174.1: 파일 다운로드 전 디스크 공간 확인
  • 85F4.1: 앱의 스토리지 관리 기능

이미지 캐싱, OTA 업데이트 다운로드 시 여유 공간을 확인한다.

Expo app.config.js 설정

Expo 프로젝트에서는 app.config.js에서 권한 관련 설정을 관리한다.

module.exports = {
  ios: {
    bundleIdentifier: 'com.example.app',
    infoPlist: {
      NSLocationWhenInUseUsageDescription: '위치 기반 혜택 정보를 제공하기 위해 사용됩니다',
      NSLocationTemporaryUsageDescriptionDictionary: {
        'benefit-location': '가까운 혜택 정보를 보여드리기 위해 현재 위치가 필요합니다',
      },
      NSCameraUsageDescription: '프로필 사진 및 인증 서류 촬영에 사용됩니다',
      NSPhotoLibraryUsageDescription: '프로필 사진 및 인증 서류 업로드에 사용됩니다',
      NSMicrophoneUsageDescription: '동영상 촬영 시 오디오 녹음에 사용됩니다',
      ITSAppUsesNonExemptEncryption: false,
      LSApplicationQueriesSchemes: [
        'supertoss', 'tosspayments', 'kb-acp', 'liivbank',
        'nhappcardansimclick', 'lottesmartpay', 'lotteappcard',
      ],
    },
  },
  android: {
    permissions: [
      'ACCESS_FINE_LOCATION',
      'ACCESS_COARSE_LOCATION',
      'CAMERA',
      'READ_MEDIA_IMAGES',
      'READ_MEDIA_VIDEO',
      'RECORD_AUDIO',
    ],
  },
  plugins: [
    ['expo-location', {
      locationAlwaysAndWhenInUsePermission: '위치 기반 혜택 정보를 제공하기 위해 사용됩니다',
      isIosBackgroundLocationEnabled: false,
      isAndroidBackgroundLocationEnabled: false,
    }],
  ],
};

주요 설정:

  • ITSAppUsesNonExemptEncryption: false: HTTPS만 사용하고 자체 암호화를 하지 않으면 수출 규정 대상이 아님을 선언. 이걸 누락하면 앱을 제출할 때마다 수출 규정 질문이 뜬다.
  • LSApplicationQueriesSchemes: 결제 SDK가 다른 앱(토스, 카카오뱅크 등)을 열 때 필요한 URL 스킴. iOS 9부터 canOpenURL 호출 시 이 목록에 없는 스킴은 false를 반환한다.
  • locationAlwaysAndWhenInUsePermission: Expo Location 플러그인이 빌드 시 Info.plist에 자동 삽입한다. 백그라운드 위치 추적은 불필요하므로 비활성화했다.

Android 딥링크

Android에서 앱 링크를 통해 웹에서 앱으로 전환하려면 assetlinks.json을 웹 서버에 배치해야 한다.

[{
  "relation": [
    "delegate_permission/common.handle_all_urls",
    "delegate_permission/common.get_login_creds"
  ],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.app",
    "sha256_cert_fingerprints": ["D6:52:EC:76:..."]
  }
}]

public/.well-known/assetlinks.json에 배치하면 Android가 이 파일을 확인하고 해당 도메인의 링크를 앱에서 직접 열 수 있다. handle_all_urls는 URL 처리 권한, get_login_creds는 자격 증명 공유 권한이다.

심사 대응 체크리스트

  1. PrivacyInfo.xcprivacy에 앱과 SDK가 사용하는 모든 Required Reason API를 선언했는지 확인
  2. 각 API 카테고리의 사용 이유 코드가 실제 사용 목적과 일치하는지 검증
  3. NSPrivacyCollectedDataTypes에 수집하는 데이터 유형을 정직하게 기재 (서버로 전송하지 않으면 빈 배열)
  4. NSPrivacyTracking을 정확히 설정 (ATT 팝업을 띄우지 않으면 false)
  5. 서드파티 SDK(Firebase, Supabase 등)가 자체 Privacy Manifest를 포함하는지 확인
  6. ITSAppUsesNonExemptEncryption을 false로 설정하거나, 암호화를 사용하면 수출 규정 문서 제출

Reference

연결문서

키보드 단축키