RSS듀오랩스
모바일 앱

Expo config plugin이 반영되지 않는 이유: prebuild와 mod 순서

작성자
듀오랩스 대표·9분 읽기

생명시계 앱에 Face ID 잠금을 붙였습니다. expo-local-authentication 을 설치하고 app.json 에 플러그인과 권한 문구를 적었습니다.

[
  "expo-local-authentication",
  { "faceIDPermission": "저장해 둔 생년월일을 열 때 Face ID로 잠금을 해제합니다." }
]

npx expo run:ios --device 를 돌렸고 Build Succeeded, 오류 0건으로 끝났습니다. 앱은 실기기에 설치됐습니다. 여기까지는 아무 문제가 없어 보였습니다.

권한 문구가 빠진 채 설치된 앱

습관처럼 설치된 앱의 Info.plist 를 열어 봤습니다.

plutil -extract NSFaceIDUsageDescription raw .../Debug-iphoneos/Dev.app/Info.plist
Could not extract value, error: No value at that key path or invalid key path: NSFaceIDUsageDescription

권한 문구가 없었습니다. iOS 에서 NSFaceIDUsageDescription 없이 생체 인증을 부르면 앱이 그 자리에서 죽습니다. 빌드는 성공했고, 설치도 됐고, 실행도 됩니다. 잠금을 켜는 순간에만 죽습니다.

이 확인을 건너뛰었다면 폰에서 크래시로 만났을 겁니다. 그리고 크래시 시점에는 원인이 app.json 이라는 단서가 화면에 하나도 없습니다.

prebuild 를 건너뛰는 조건

이유는 단순했습니다. ios/ 디렉터리가 이미 있었습니다.

expo run:ios 는 네이티브 프로젝트가 없을 때만 prebuild 를 돌립니다. 이미 있으면 그대로 컴파일합니다. 그런데 config plugin 은 prebuild 때만 적용됩니다. app.json 은 컴파일러가 읽지 않고, prebuild 가 그것을 Info.plist.entitlements 로 펼쳐 놓아야 비로소 빌드에 들어갑니다.

그래서 플러그인을 추가한 뒤 expo run:ios 만 돌리면, 새 플러그인이 적용되지 않은 예전 ios/ 를 그대로 씁니다. 빌드는 아무 불평 없이 성공합니다. 넣은 설정이 어디에도 없을 뿐입니다.

이 구조 자체는 Expo 가 CNG 라고 부르는 방식이고 의도된 것입니다. ios/ 는 소스가 아니라 산출물입니다. 산출물을 다시 만들지 않으면 새 입력이 반영되지 않는 것은 당연한데, 빌드가 성공한다는 사실이 그 당연함을 가려 줍니다.

npx expo prebuild --platform ios 를 직접 돌리니 문구가 들어갔습니다.

쓰지도 않는 원격 푸시가 만든 빌드 실패

그 상태로 다시 빌드했더니 이번엔 실패했습니다.

❌ Provisioning Profile "iOS Team Provisioning Profile: *" does not support the Push Notifications capability.
❌ Entitlements file defines the value "aps-environment" which is not registered for profile.
CommandError: "xcodebuild" exited with error code 65.

aps-environment 는 원격 푸시용 엔타이틀먼트입니다. 그런데 이 앱은 로컬 알림만 씁니다. 서버가 없어서 푸시를 보낼 주체가 아예 없고, 개인정보 문서에도 원격 푸시를 받지 않는다고 적어 두었습니다.

붙인 것은 expo-notifications 였습니다. 플러그인 소스를 열어 보면 조건 없이 넣습니다.

config = withEntitlementsPlist(config, (config) => {
  if (!config.modResults['aps-environment']) {
    config.modResults['aps-environment'] = mode;
  }
  return config;
});

옵트아웃 옵션이 없습니다. 값이 비어 있으면 넣습니다.

앞의 빌드가 통과했던 이유도 여기서 설명됩니다. 그때는 prebuild 가 안 돌아서 .entitlements 가 예전 것이었고, 거기엔 이 값이 없었습니다. prebuild 를 제대로 돌리자 값이 들어왔고, 그제서야 프로비저닝 검사에 걸렸습니다. 첫 번째 문제를 고친 것이 두 번째 문제를 꺼낸 셈입니다.

선택지는 둘이었습니다. Apple 개발자 포털에서 App ID 에 Push Notifications 를 켜거나, 쓰지 않는 엔타이틀먼트를 빼거나. 쓰지 않는 기능 때문에 capability 를 켜는 것은 앞뒤가 맞지 않고, 바이너리가 푸시를 받겠다고 선언하는데 문서는 안 받는다고 적혀 있는 상태가 됩니다. 그래서 빼기로 했습니다.

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

module.exports = function withoutPushEntitlement(config) {
  return withEntitlementsPlist(config, (mod) => {
    delete mod.modResults['aps-environment'];
    return mod;
  });
};

app.jsonplugins 맨 뒤에 넣었습니다.

지운 값이 되살아난 이유

값이 그대로 남아 있었습니다.

ios.entitlements = {"aps-environment": "development"}

처음에는 플러그인이 로드되지 않았다고 생각했습니다. 경로를 잘못 적었거나 module.exports 형태가 틀렸을 것이라고 봤습니다. 확인해 보니 아니었습니다. 지우는 대신 표식을 하나 넣어 봤습니다.

mod.modResults['zz-plugin-ran'] = 'yes';
delete mod.modResults['aps-environment'];
{"zz-plugin-ran": "yes", "aps-environment": "development"}

표식은 들어갔고 aps-environment 도 남아 있습니다. 플러그인은 돌았습니다. 지운 값을 누군가 다시 넣은 것입니다.

넣을 수 있는 것은 expo-notifications 하나뿐입니다. 저는 그것을 제 플러그인보다 앞에 등록했으니 먼저 돌 것이라고 생각했습니다. 반대였습니다. Expo 의 mod 는 등록 순서의 역순으로 실행됩니다. 뒤에 등록한 것이 먼저 돌고, 앞에 등록한 것이 나중에 돕니다.

app.json 에서 순서를 바꿔 제 플러그인을 expo-notifications 앞으로 옮겼습니다.

ios.entitlements = {}

introspect 로 드러난 실행 순서

이 확인을 빌드로 했다면 왕복 한 번에 몇 분씩 걸렸을 것이고, 실패 메시지도 프로비저닝 오류라서 원인이 순서라는 힌트를 주지 않습니다.

expo config --type introspect 가 플러그인을 전부 적용한 최종 설정을 파일에 쓰지 않고 보여 줍니다.

npx expo config --type introspect --json | jq '.ios.entitlements'

prebuild 도 빌드도 없이 결과만 나옵니다. 표식을 넣어 실행 여부를 확인한 것도 이 명령 위에서였습니다. 플러그인을 건드릴 때는 이쪽을 먼저 보는 편이 빠릅니다.

정리하면 이렇습니다.

확인할 것 명령
플러그인이 최종 설정에 반영됐나 expo config --type introspect
생성된 네이티브 프로젝트에 들어갔나 expo prebuildInfo.plist, .entitlements 직접 열기
설치된 앱에 실제로 들어갔나 plutil -extract, codesign -d --entitlements -

세 번째까지 봐야 끝입니다. 첫 번째 문제는 두 번째 단계에서 갈렸고, 두 번째 문제는 첫 번째 단계에서 갈렸습니다.

위젯에 같은 문제가 기다리는 자리

다음으로 홈 화면 위젯을 검토하고 있습니다. 위젯은 React Native 가 아니라 SwiftUI 로 만들고, 앱과 다른 프로세스에서 돌기 때문에 앱 샌드박스의 데이터를 못 읽습니다. 데이터를 넘기려면 App Group 엔타이틀먼트가 필요합니다.

aps-environment 와 같은 계열입니다. 엔타이틀먼트를 하나 더 붙이는 일이고, 프로비저닝 프로파일이 그것을 지원해야 하며, config plugin 이 그것을 .entitlements 에 써 넣어야 합니다. 위젯 코드는 숫자 하나를 크게 그리는 정도라 짧을 텐데, 막히는 자리는 코드가 아닐 가능성이 높습니다.

아직 시작하지 않았습니다. 다만 이번에 배운 것 덕분에 어디서 시간이 갈지는 대충 압니다. Swift 를 쓰는 날이 아니라 Apple 개발자 포털과 introspect 를 오가는 날이 될 겁니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.