0Pricing
React Native Academy · 강의

첫 번째 구성 플러그인 작성하기

@expo/config-plugins 도우미를 사용하여 withMyPlugin 함수를 만들고 app.json의 plugins 배열에 추가한 뒤 expo prebuild를 실행하여 네이티브 출력을 확인합니다.

첫 번째 구성 플러그인 작성하기은(는) CoddyKit의 무료 React Native Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 React Native Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Project Setup for a Config Plugin

You can write a config plugin as a file inside your project or as a separate npm package. For project-specific plugins, create a plugins/ folder at the project root and add your plugin file there. Reference it in app.json using a relative path. This approach keeps the plugin co-located with the app and requires no publishing step.

# Project structure
your-expo-app/
  plugins/
    withCustomPermissions.js   <- your plugin
  app.json
  App.tsx

# app.json reference
{
  'expo': {
    'plugins': [
      ['./plugins/withCustomPermissions', { 'reason': 'Scan QR codes' }]
    ]
  }
}

The withMyPlugin Function Signature

Every config plugin is a synchronous function that takes two arguments: config (the full Expo config object) and an optional options object passed from app.json. It must return the (modified) config object. You chain Expo's built-in modifier helpers inside this function to make specific changes to native files, then return the result of the last modifier call.

// plugins/withCustomPermissions.js
const { withInfoPlist } = require('@expo/config-plugins');

/**
 * @param {import('@expo/config-plugins').ExpoConfig} config
 * @param {{ reason: string }} options
 */
function withCustomPermissions(config, options = {}) {
  const reason = options.reason || 'This app requires camera access.';

  return withInfoPlist(config, (iosConfig) => {
    iosConfig.modResults['NSCameraUsageDescription'] = reason;
    return iosConfig;
  });
}

module.exports = withCustomPermissions;

Understanding modResults

Inside a mod callback (like the one passed to withInfoPlist), you receive a config object where config.modResults contains the parsed native file as a JavaScript object. For withInfoPlist it is a plain object mirroring the plist dictionary. For withAndroidManifest it is a parsed XML document. You modify modResults in place and return the config — Expo serializes it back to the native file format automatically.

const { withInfoPlist } = require('@expo/config-plugins');

module.exports = function withAppTrackingPermission(config) {
  return withInfoPlist(config, (iosConfig) => {
    // modResults is the parsed Info.plist dictionary
    const plist = iosConfig.modResults;

    // Add multiple keys at once
    plist['NSUserTrackingUsageDescription'] =
      'We use tracking to improve your ad experience.';
    plist['ITSAppUsesNonExemptEncryption'] = false;

    return iosConfig;
  });
};

Modifying AndroidManifest.xml

Use withAndroidManifest to modify AndroidManifest.xml. The modResults is an XML object parsed by the xmldom library. Expo's AndroidConfig helpers provide utility functions like addUsesPermission, addMetaDataItemToMainActivity, and getMainApplication that operate on this XML structure, so you rarely need to manipulate the XML DOM directly.

const { withAndroidManifest, AndroidConfig } = require('@expo/config-plugins');
const { addUsesPermission } = AndroidConfig.Permissions;

module.exports = function withBluetoothPermissions(config) {
  return withAndroidManifest(config, (androidConfig) => {
    const manifest = androidConfig.modResults;

    // Add multiple permissions
    addUsesPermission(manifest, 'android.permission.BLUETOOTH');
    addUsesPermission(manifest, 'android.permission.BLUETOOTH_ADMIN');
    addUsesPermission(manifest, 'android.permission.BLUETOOTH_SCAN');

    return androidConfig;
  });
};

Testing Your Plugin with expo prebuild

Run npx expo prebuild --clean to test your plugin and inspect the generated native files. Use --clean to start fresh. After prebuild, open the generated files to verify your changes were applied. For plist changes check ios/YourApp/Info.plist. For manifest changes check android/app/src/main/AndroidManifest.xml. Fix any issues in the plugin file and re-run prebuild until the output is correct.

# Test the plugin
npx expo prebuild --clean --platform ios

# Verify the change in Info.plist
plutil -p ios/MyApp/Info.plist | grep -A1 Camera
# Output:
#   "NSCameraUsageDescription" => "Scan QR codes."

# For Android
cat android/app/src/main/AndroidManifest.xml | grep BLUETOOTH

Adding Files to the Native Project

Some plugins need to copy files into the native project — for example, a Google Services JSON file or a custom font. Use withDangerousMod for arbitrary filesystem operations during prebuild. This is the escape hatch for changes not covered by other helpers. Use it sparingly and document the side effects clearly.

const { withDangerousMod } = require('@expo/config-plugins');
const path = require('path');
const fs = require('fs');

module.exports = function withGoogleServicesJson(config) {
  return withDangerousMod(config, [
    'android',
    async (dangerousConfig) => {
      const projectRoot = dangerousConfig.modRequest.projectRoot;
      const src = path.join(projectRoot, 'google-services.json');
      const dest = path.join(projectRoot, 'android', 'app', 'google-services.json');

      if (!fs.existsSync(src)) {
        throw new Error('google-services.json not found in project root');
      }
      fs.copyFileSync(src, dest);
      return dangerousConfig;
    },
  ]);
};

Accessing Config Values Inside a Plugin

The config object passed to your plugin contains the full Expo configuration, including values from app.json. You can read config.name, config.version, config.ios.bundleIdentifier, config.android.package, and any extra fields you defined. This lets your plugin be data-driven based on the app config rather than hard-coded values.

module.exports = function withAppMetadata(config) {
  const appName = config.name;
  const bundleId = config.ios?.bundleIdentifier;
  const version = config.version;

  return withInfoPlist(config, (iosConfig) => {
    iosConfig.modResults['CFBundleDisplayName'] = appName;
    // Log for debugging during prebuild
    console.log(
      '[withAppMetadata] Setting display name to:', appName,
      'for bundle:', bundleId, 'v' + version
    );
    return iosConfig;
  });
};

Modifying build.gradle with withAppBuildGradle

To modify the Android app-level build.gradle file, use withAppBuildGradle. The modResults.contents property contains the file as a raw string, and you use string manipulation or regex to insert your changes. This approach is necessary for adding Gradle dependencies, plugin declarations, or custom build configurations that the higher-level helpers do not cover.

const { withAppBuildGradle } = require('@expo/config-plugins');

module.exports = function withGooglePlayServicesPlugin(config) {
  return withAppBuildGradle(config, (gradleConfig) => {
    const contents = gradleConfig.modResults.contents;

    // Add google-services plugin if not already present
    if (!contents.includes('com.google.gms.google-services')) {
      gradleConfig.modResults.contents = contents.replace(
        'apply plugin: "com.android.application"',
        'apply plugin: "com.android.application"\napply plugin: "com.google.gms.google-services"'
      );
    }
    return gradleConfig;
  });
};

Modifying the Xcode Project with withXcodeProject

For changes to the Xcode project file itself — like adding a build phase, linking a native library, or adding a capability — use withXcodeProject. The modResults is an XcodeProject object from the xcode npm package. This is the most complex mod and requires familiarity with the Xcode project format. Prefer higher-level helpers when they exist.

const { withXcodeProject } = require('@expo/config-plugins');

module.exports = function withRunScript(config) {
  return withXcodeProject(config, (xcode) => {
    const project = xcode.modResults;
    const target = project.getFirstTarget().uuid;

    // Add a run script build phase
    project.addBuildPhase(
      [],
      'PBXShellScriptBuildPhase',
      'Bundle React Native code and images',
      target,
      { shellPath: '/bin/sh', shellScript: 'echo Running custom script' }
    );

    return xcode;
  });
};

Entitlements with withEntitlementsPlist

iOS entitlements are capability declarations in a .entitlements file that must match your provisioning profile. Use withEntitlementsPlist to add entitlements like Push Notifications, iCloud, HealthKit, or App Groups. Without the correct entitlements, features silently fail at runtime on device even if the code looks correct.

const { withEntitlementsPlist } = require('@expo/config-plugins');

module.exports = function withPushNotifications(config) {
  return withEntitlementsPlist(config, (iosConfig) => {
    const entitlements = iosConfig.modResults;

    // Enable push notifications capability
    entitlements['aps-environment'] = 'production';

    // App Groups for sharing data with extensions
    if (!entitlements['com.apple.security.application-groups']) {
      entitlements['com.apple.security.application-groups'] = [];
    }
    entitlements['com.apple.security.application-groups'].push(
      'group.com.yourapp.shared'
    );

    return iosConfig;
  });
};

Composing a Complete Plugin

Real-world plugins often need to modify multiple native files across both platforms. Compose helpers in sequence, passing the result of each into the next. Add clear input validation at the top and log progress messages so developers can trace what the plugin changed during prebuild. Return the final config after all modifications.

const {
  withInfoPlist,
  withAndroidManifest,
  withEntitlementsPlist,
  AndroidConfig,
} = require('@expo/config-plugins');

module.exports = function withPaymentSDK(config, opts = {}) {
  if (!opts.merchantId) {
    throw new Error('withPaymentSDK requires a merchantId option.');
  }

  // iOS
  config = withInfoPlist(config, (c) => {
    c.modResults['PaymentMerchantId'] = opts.merchantId;
    return c;
  });

  config = withEntitlementsPlist(config, (c) => {
    c.modResults['com.apple.developer.in-app-payments'] = [opts.merchantId];
    return c;
  });

  // Android
  config = withAndroidManifest(config, (c) => {
    AndroidConfig.Permissions.addUsesPermission(
      c.modResults, 'android.permission.INTERNET'
    );
    return c;
  });

  return config;
};

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: how to write a config plugin function that takes config and options and returns the modified config, how to use withInfoPlist, withAndroidManifest, withEntitlementsPlist, and withDangerousMod, and how to test your plugin with expo prebuild --clean. You also saw how to compose multiple mods into a complete cross-platform plugin. Next up we explore modifying AndroidManifest and Info.plist in detail.

자주 묻는 질문

“첫 번째 구성 플러그인 작성하기” 강의는 무료인가요?

네 — “첫 번째 구성 플러그인 작성하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 React Native Academy 강의 전체를 잠금 해제할 수 있습니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“첫 번째 구성 플러그인 작성하기”에서 뭘 배우나요?

@expo/config-plugins 도우미를 사용하여 withMyPlugin 함수를 만들고 app.json의 plugins 배열에 추가한 뒤 expo prebuild를 실행하여 네이티브 출력을 확인합니다. 브라우저에서 직접 실행하는 실습 코드로 React Native Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

React Native Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 React Native Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“첫 번째 구성 플러그인 작성하기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 React Native Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 React Native Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 구성 플러그인이란 무엇이며 언제 사용할까요
  2. 첫 번째 구성 플러그인 작성하기
  3. AndroidManifest 및 Info.plist 수정하기
  4. 구성 플러그인을 npm 패키지로 배포하기
← React Native Academy(으)로 돌아가기