AndroidManifest 및 Info.plist 수정하기
withAndroidManifest로 권한과 메타데이터 요소를 추가하고, withInfoPlist로 NSCameraUsageDescription과 같은 키를 추가한 다음 두 플랫폼에서 테스트합니다.
AndroidManifest 및 Info.plist 수정하기은(는) CoddyKit의 무료 React Native Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 React Native Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why These Files Matter
AndroidManifest.xml and Info.plist are the central configuration files for Android and iOS apps respectively. They declare permissions, features, app components, URL schemes, capabilities, and usage descriptions. Third-party native libraries almost always require additions to these files. Without the correct entries, features crash, permissions are denied, and App Store submission fails review.
Anatomy of AndroidManifest.xml
The AndroidManifest.xml has three main sections: uses-permission elements at the top level (declare what the app needs), the application element (app-wide settings like icon, theme, backup), and inside it, activity, service, receiver, and provider elements. Config plugins typically add permissions and meta-data elements to the application or activity.
<!-- Typical AndroidManifest.xml structure -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions at top level -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".MainApplication"
android:label="@string/app_name">
<!-- meta-data items here -->
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="YOUR_KEY" />
<activity android:name=".MainActivity">
<!-- intent filters here -->
</activity>
</application>
</manifest>Adding Permissions to AndroidManifest
Use AndroidConfig.Permissions.addUsesPermission to safely add a permission without duplicating it. This helper checks whether the permission already exists before inserting, so running prebuild multiple times is safe. Always add both dangerous runtime permissions (like CAMERA) and the uses-feature declaration when requiring specific hardware to prevent your app from appearing in the Play Store on incompatible devices.
const { withAndroidManifest, AndroidConfig } = require('@expo/config-plugins');
module.exports = function withCameraAndroid(config) {
return withAndroidManifest(config, (androidConfig) => {
const manifest = androidConfig.modResults;
AndroidConfig.Permissions.addUsesPermission(
manifest, 'android.permission.CAMERA'
);
AndroidConfig.Permissions.addUsesPermission(
manifest, 'android.permission.READ_MEDIA_IMAGES'
);
// Declare hardware requirement
if (!manifest['uses-feature']) manifest['uses-feature'] = [];
manifest['uses-feature'].push({
'$': { 'android:name': 'android.hardware.camera', 'android:required': 'false' }
});
return androidConfig;
});
};Adding meta-data to the Application Element
Many SDKs (Google Maps, Firebase, Facebook) require a meta-data element inside the application tag with an API key or app ID. Use AndroidConfig.Manifest.getMainApplication to safely locate the application element, then push a new meta-data object into its 'meta-data' array. Always check the array exists before pushing to avoid a null reference error.
const { withAndroidManifest, AndroidConfig } = require('@expo/config-plugins');
module.exports = function withGoogleMapsAndroid(config, { apiKey }) {
return withAndroidManifest(config, (androidConfig) => {
const manifest = androidConfig.modResults;
const mainApp = AndroidConfig.Manifest.getMainApplication(manifest);
if (!mainApp['meta-data']) mainApp['meta-data'] = [];
// Remove existing key to avoid duplicates
mainApp['meta-data'] = mainApp['meta-data'].filter(
(item) => item.$['android:name'] !== 'com.google.android.geo.API_KEY'
);
mainApp['meta-data'].push({
'$': {
'android:name': 'com.google.android.geo.API_KEY',
'android:value': apiKey,
}
});
return androidConfig;
});
};Intent Filters for Deep Links
To register a custom URL scheme or Android App Link on Android, you add an intent filter to the main activity. The intent filter declares what URL patterns your app handles. Config plugins can add these programmatically by finding the MainActivity element and appending to its intent-filter array.
const { withAndroidManifest, AndroidConfig } = require('@expo/config-plugins');
module.exports = function withDeepLinks(config, { scheme }) {
return withAndroidManifest(config, (androidConfig) => {
const manifest = androidConfig.modResults;
const mainActivity = AndroidConfig.Manifest.getMainActivity(manifest);
if (!mainActivity['intent-filter']) mainActivity['intent-filter'] = [];
mainActivity['intent-filter'].push({
action: [{ '$': { 'android:name': 'android.intent.action.VIEW' } }],
category: [
{ '$': { 'android:name': 'android.intent.category.DEFAULT' } },
{ '$': { 'android:name': 'android.intent.category.BROWSABLE' } },
],
data: [{ '$': { 'android:scheme': scheme } }],
});
return androidConfig;
});
};Info.plist Usage Description Keys
iOS requires a usage description string in Info.plist for every sensitive permission your app requests. If the string is missing, the app crashes when it calls requestPermission. Common keys include NSCameraUsageDescription, NSLocationWhenInUseUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription, and NSContactsUsageDescription. The string appears in the system permission dialog shown to the user.
const { withInfoPlist } = require('@expo/config-plugins');
module.exports = function withAllPermissions(config, opts = {}) {
return withInfoPlist(config, (iosConfig) => {
const plist = iosConfig.modResults;
plist['NSCameraUsageDescription'] =
opts.camera || 'Scan QR codes and take profile photos.';
plist['NSMicrophoneUsageDescription'] =
opts.microphone || 'Record voice messages.';
plist['NSLocationWhenInUseUsageDescription'] =
opts.location || 'Show nearby restaurants.';
plist['NSPhotoLibraryUsageDescription'] =
opts.photoLibrary || 'Upload photos from your library.';
return iosConfig;
});
};URL Types for Custom Schemes on iOS
To handle custom URL schemes on iOS (e.g., myapp://), add a CFBundleURLTypes entry to Info.plist. Each entry has a role and an array of scheme strings. Expo sets this automatically for the app's own scheme, but if you need an additional scheme (e.g., for OAuth redirect), you must add it via a config plugin.
const { withInfoPlist } = require('@expo/config-plugins');
module.exports = function withOAuthScheme(config, { scheme }) {
return withInfoPlist(config, (iosConfig) => {
const plist = iosConfig.modResults;
if (!plist['CFBundleURLTypes']) plist['CFBundleURLTypes'] = [];
// Avoid duplicate
const existing = plist['CFBundleURLTypes'].find(
(t) => t.CFBundleURLName === scheme
);
if (!existing) {
plist['CFBundleURLTypes'].push({
CFBundleURLName: scheme,
CFBundleURLSchemes: [scheme],
});
}
return iosConfig;
});
};Info.plist Boolean and Number Values
Info.plist supports several value types beyond strings: booleans, numbers, arrays, and dictionaries. In the JavaScript modResults object, use plain JS booleans and numbers — Expo handles the correct plist XML encoding. Setting ITSAppUsesNonExemptEncryption to false (a boolean) avoids App Store export compliance questions for most apps.
const { withInfoPlist } = require('@expo/config-plugins');
module.exports = function withExportCompliance(config) {
return withInfoPlist(config, (iosConfig) => {
const plist = iosConfig.modResults;
// Boolean value
plist['ITSAppUsesNonExemptEncryption'] = false;
// Number value
plist['UIRequiresFullScreen'] = false;
// Nested dictionary
plist['NSAppTransportSecurity'] = {
NSAllowsArbitraryLoads: false,
NSExceptionDomains: {
'localhost': { NSExceptionAllowsInsecureHTTPLoads: true }
}
};
return iosConfig;
});
};Background Modes on iOS
iOS apps that need to run code while backgrounded must declare UIBackgroundModes in Info.plist. Supported modes include audio, location, fetch, remote-notification, processing, and voip. Without the correct background mode declared, iOS suspends the app when it leaves the foreground and your background task is silently killed.
const { withInfoPlist } = require('@expo/config-plugins');
module.exports = function withBackgroundModes(config, { modes }) {
return withInfoPlist(config, (iosConfig) => {
const plist = iosConfig.modResults;
if (!plist['UIBackgroundModes']) plist['UIBackgroundModes'] = [];
for (const mode of modes) {
if (!plist['UIBackgroundModes'].includes(mode)) {
plist['UIBackgroundModes'].push(mode);
}
}
return iosConfig;
});
};
// app.json usage
// ["./plugins/withBackgroundModes", { "modes": ["audio", "fetch"] }]Verifying Plugin Changes with plutil and grep
After running expo prebuild, always verify your changes landed correctly before building. On macOS, use plutil -p ios/YourApp/Info.plist to pretty-print the plist. Use grep to search AndroidManifest for specific strings. Add this verification as part of your CI pipeline so a broken plugin is caught before wasted build minutes.
# Verify iOS Info.plist
npx expo prebuild --clean --platform ios
plutil -p ios/MyApp/Info.plist | grep NSCamera
# -> "NSCameraUsageDescription" => "Scan QR codes."
# Verify Android manifest permissions
grep 'CAMERA' android/app/src/main/AndroidManifest.xml
# -> <uses-permission android:name="android.permission.CAMERA"/>
# Verify meta-data
grep 'geo.API_KEY' android/app/src/main/AndroidManifest.xmlIdempotency: Safe to Run Multiple Times
A well-written config plugin is idempotent — running it multiple times produces the same result as running it once. Guard every insertion with a check for existing values before adding them. Use filter to remove old values before inserting the new one (replace pattern) rather than blindly pushing, which would accumulate duplicates across repeated prebuild runs.
// Bad - adds duplicate permissions on every prebuild
manifest['uses-permission'].push({
'$': { 'android:name': 'android.permission.CAMERA' }
});
// Good - idempotent check first
const hasCameraPermission = manifest['uses-permission']?.some(
(p) => p.$['android:name'] === 'android.permission.CAMERA'
);
if (!hasCameraPermission) {
AndroidConfig.Permissions.addUsesPermission(
manifest, 'android.permission.CAMERA'
);
}Quick Check
Test your understanding of React Native Mobile Development concepts from this lesson.
Lesson Recap
In this lesson you learned: how AndroidManifest.xml and Info.plist control permissions, features, and OS integrations, how to use withAndroidManifest to add permissions and meta-data elements, and how to use withInfoPlist to set usage descriptions, URL types, and background modes. You also learned why idempotency matters for plugins that run on every prebuild. Next up we learn how to distribute config plugins as npm packages.
자주 묻는 질문
“AndroidManifest 및 Info.plist 수정하기” 강의는 무료인가요?
네 — “AndroidManifest 및 Info.plist 수정하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 React Native Academy 강의 전체를 잠금 해제할 수 있습니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“AndroidManifest 및 Info.plist 수정하기”에서 뭘 배우나요?
withAndroidManifest로 권한과 메타데이터 요소를 추가하고, withInfoPlist로 NSCameraUsageDescription과 같은 키를 추가한 다음 두 플랫폼에서 테스트합니다. 브라우저에서 직접 실행하는 실습 코드로 React Native Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
React Native Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 React Native Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“AndroidManifest 및 Info.plist 수정하기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 React Native Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 React Native Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 구성 플러그인이란 무엇이며 언제 사용할까요
- 첫 번째 구성 플러그인 작성하기
- AndroidManifest 및 Info.plist 수정하기
- 구성 플러그인을 npm 패키지로 배포하기