-
[RN 강의] Expo 환경의 배포와 업데이트강의노트/React Native 2025. 10. 3. 16:35
Expo Go 환경에서 개발하는 React Native의 마지막 강의 노트이다.
지금까지 개발한 앱을 배포하고 플랫폼의 심사 없이 업데이트 하는 방법을 배운다.
1. EAS에 대해서
본격적인 시작에 앞서서 EAS(Expo Application Services)의 좋은 점들이 많이 있어서 쭉 정리를 하고 넘어가도록 한다.
EAS Build
Expo Go 환경의 핵심이라고 볼 수 있다. 클라우드를 이용해서 프로젝트의 파일들을 전부 빌드를 수행한다. 클라우드 빌드의 장점은 클라우드 환경이기 때문에 Winodws OS를 사용하는 개발자가 iOS 빌드를 하는 등의 작업이 가능해진다. 하지만 EAS build의 단점은 팀 단위로 가면 사실 정기 결제를 해야 한다. 무료 유저는 한 달에 30번밖에 빌드를 못하기 때문이다. 하루에 한번 꼴인 것이며, 다시 빌드를 하기 위해서는 다음달까지 기다려야 한다. 그리고 이 빌드 자체도 무료 사용자는 유료 사용자들이 많은 경우에 대기를 해야 한다. 그래서 직접 로컬에서 빌드하는 명령어(npx expo run:android)를 배운 것이다. 이렇게 빌드를 하고 나면 그 빌드한 결과물을 내 기기나 팀원의 컴퓨터든 다 공유해서 사용할 수 있기 때문에 테스트를 수행할 때도 편리해진다. 빌드 후 생성되는 QR 코드 하나만 공유하면 전부 다 동일한 버전을 앱에 설치해서 테스트를 해볼 수 있는 것이다.
Expo.dev에 들어가면 그간 build 결과물들을 살펴볼 수 있다. 성공한 빌드의 경우 팀원들에게 공유하여 각자의 컴퓨터 또는 모바일에서 install 버튼으로 설치해볼 수 있다.


expo.dev에 쌓여 있는 build 결과물 EAS를 사용하면 push 알림을 보낸 로그 기록, 비밀키 관리를 할 수 있다.
Expo Github 연동으로 Updates 자동화
Github를 연동해서 업데이트 자동화 세팅을 해둘 수 있다. GitHub 탭의 Build triggers 설정을 해서 특정 브랜치 배포 시 빌드가 되도록 자동화를 세팅할 수 있다.
Build triggers 밑에 Updates를 활성화 해두면 어떤 브랜치든 커밋할 때마다 App Store 심사 없이 EAS Updates를 할 수 있다. 그래서 모바일을 재시작하면 바로 새로운 버전을 다운로드 받을 수 있게끔 자동화를 할 수 있다.

Expo와 Github 연동 Expo 환경 변수 설정
아무래도 클라우드에서 빌드를 하는 것이다 보니 클라우드에는 .env 파일이 없다. .env 파일 또는 내부 설정들을 환경설정 (Environment variables)에 올려주면 된다. 그러면 클라우드 빌드를 수행할 때 여기에 등록된 환경변수들을 참고해서 빌드를 수행한다.

Environment variables 탭 Hosting
호스팅 기능은 React Native 웹을 위한 기능이다. Next.js의 Vercel 처럼 Expo도 EAS 호스팅 서비스를 출시했다. 그래서 앱과 웹을 동시에 만드는 개발자는 여기서 호스팅까지 이용해볼 수 있다.

Hosting 탭 EAS Updates
Development나 Preview 채널에 AppStore 심사 없이 바로 업데이트 빌드를 배포할 수 있다. 이 EAS Updates를 사용하면 패치 됐을 때 얼마나 많이 업데이트 했는지, 에러로 인해 얼마나 많이 롤백 했는지 등의 통계 그래프도 확인할 수 있다.


Over-the-air updates 탭 Insight
Vercel의 Analytics 같은 기능으로 현재는 무료로 사용할 수 있으나, 추후에는 유료 기능으로 바뀔 것 같다.

insight 탭 WorkFlow
워크플로우는 CI/CD 파이프라인이라고 보면 된다. GitHub 액션이랑 비슷하다. 예를들어, "main 브랜치에 누군가 push를 했다면 빌드를 돌리고, 빌드 결과물을 제출한다"는 과정을 자동화 해 놓는 것을 workflow라고 한다.
# .eas/workflows/create-production-builds.yml name: Create Production Builds jobs: build_android: type: build # This job type creates a production build for Android params: platform: android build_ios: type: build # This job type creates a production build for iOS params: platform: iosEAS Submit
Expo를 활용하면 정말 간단하게 애플리케이션을 만들 수 있다. 그런데 화룡정점이 바로 EAS Submit 기능이다. 이 기능은 Sumbit to Google Play Store 또는 App Store을 할 수 있다. 명령어도 정말 간단한다. eas.json에 필요한 키나 세팅값을 미리 설정해두어야 하지만, 한 번 제출한 다음부터는 아래 명령어로 편리하게 제출할 수 있다.
# eas.json을 참조하여 Store에 제출 eas submit --platform android eas submit --platform ios # 빌드와 Store 제출을 한꺼번에 수행 eas build --platform android --auto-submit이전까지는 Google Play Store나 Apple App Store에 메타 데이터를 아래 처럼 직접 넣어야 했었다. 이런 정보도 Expo 내에서 관리할 수 있도록 EAS 메타데이터 서비스도 곧 출시 예정이다. 지금은 베타 테스트 버전으로 제공하고 있다.
{ "configVersion": 0, "apple": { "info": { "en-US": { "title": "Awesome App", "subtitle": "Your self-made awesome app", "description": "The most awesome app you have ever seen", "keywords": ["awesome", "app"], "marketingUrl": "https://example.com/en/promo", "supportUrl": "https://example.com/en/support", "privacyPolicyUrl": "https://example.com/en/privacy" } } } }Expo Orbit
Expo에서 빌드를 하다보면 빌드 기록들이 많이 쌓게 된다. 그리고 실제 모바일, 에뮬레이터 또는 시뮬레이터, 다양한 기기들까지 다양하다. 이들의 조합을 따져가면서 구분해서 개발하면 복잡하기 때문에 만들어진 서비스가 Expo Orbit이다. 즉, 다양한 빌드를 기기/에뮬레이터에서 실행 및 관리할 수 있다.
Expo Orbit을 설치하면 빠르게 빌드 결과물과 실행 기기를 각각 선택할 수 있다. 빌드의 경우 preview/production으로 빌드한 로컬 파일을 선택할 수도 있고, EAS 빌드 결과물을 선택할 수도 있다. 빌드를 선택하고 나면 실행할 기기를 선택하여 빠르게 실행해 볼 수 있다. 편리하게 빌드와 실행 매체의 조합들을 선택할 수 있기 때문에 Orbit도 깔아두는 것을 추천한다.

Expo Orbit 실행 화면 Logcat: preview/production 빌드 이후 앱이 바로 꺼지는 현상
preview 또는 production 빌드를 하면 Metro Server가 없기 때문에 로그 같은 것을 볼 수 없다. 그런데 앱을 빌드 한 후에 실행하자마자 꺼져버리는 현상이 발생할 수 있다. 분명 문제가 있어서 꺼지는 것일텐데 Metro도 없고, Development builds도 아니라서 엑스포 서버 연결도 안되니 디버깅할 방법이 없다. 이럴 때 안드로이드나 아이폰이나 둘 다 원래 기본 애플리케이션 개발 IDE (Android Studio, Xcode)를 사용하면 된다.
Android Studio 기준으로 아무런 프로젝트를 열고 IDE 왼쪽 하단에 Logcat이라는 고양이 아이콘이 있다. Logcat를 열고 에뮬레이터에 대한 모든 시스템 로그가 기록이 된다. 앱 코드상에서 찍는 console.log()도 다 기록된다. 거기서 내 앱을 실행시켜서 같은 현상이 나오도록 하고, 내 앱과 관련된 단어(threads)를 검색해보면 해당 Error가 보일 것이다.

android studio logcat 모니터링 툴
실무에 가서 앱을 배포했는데 개발자가 버그를 못 잡아 내고, 가끔식 사용자가 잡아내는 경우가 있다. 사용자는 앱이 꺼질 것이다. 그런데 대부분의 고객은 그런 현상을 제보해주지 않는다. 그럴 때 어떻게 해야 할까? 개발자가 잡지 못하거나 재현하지 못하는 버그들은 다른 서비스를 사용해야 한다. 예를들어 Firebase의 Crashlytics이 있다. Crashlytics 는 앱에서 충돌이 발생할 때마다 에러 메시지를 수집해서 보내줄 수 있다. 또 다른 대안으로는 Sentry나 Datadog이 있다. Datadog는 Android Crash Reporting and Error Tracking 서비스를 제공한다. 문제는 Datadog은 비싸다는 점이다. 고객들에게 처음 발생한 에러는 이러한 모니터링 툴을 사용해서 잡아내야 한다.
2. Expo Updates
Expo Updates 제약사항
배포를 하지 않아도 앱을 재실행 했을 때 앱에서 수정된 부분이 있나 확인하고 있다면 업데이트를 할 수 있다. 제약사항은 존재하는데, 모든 걸 다 업데이트 할 수는 없고 JavaScript와 Style 부분이나 정적 파일(이미지, 동영상)들에 해당된다. 하지만 안드로이드나 iOS Native 쪽은 App Store 또는 Play Store 심사를 받아야 한다. 대부분의 경우 지금까지 JavaScript 코딩을 했다. 이런 경우 대부분의 마이너한 수정사항들은 전부 EAS Updates로 커버가 가능하다.
npm 라이브러리 쓸 때 주의해야 하는 부분이 있다. 안드로드/iOS 기능을 건들고 있고 있는지 확인을 해야 한다. 어떤 라이브러리가 JavaScript 코드 밖에 없다면 다운받아서 사용할 수 있지만, 어떤 라이브러리가 iOS Native 단을 건든다면 그런 건 EAS Update로 올려도 Native 쪽이 수정이 안 돼서 에러가 난다. 그래서 라이브러리를 새로 설치하고 EAS Updates를 사용하는 경우에는 Native 단을 건드리는 지 확인하고 조심해야 한다.
EAS Updates는 Code Push 라는 기능으로 불렸다. Code Push는 규모에 따라 다르지만 마이크로소프트에서 무료로 해주던 기능이었다. 하지만 Expo는 EAS Updates로 유료화 해서 사업화를 했다. Expo 가격정책은 한 달에 접속한 사용자들이 1,000명일 때 무료로 제공해주다가, 사용자의 수가 올라갈 수록 월 청구 비용이 올라간다. 100만명 이상의 액티브 사용자일 경우 매월 100만원씩 납부해야 한다. 사용자가 늘어날 수록 부담이 커지는 것이 사실이다.
Expo Updates 기능은 우리가 자체 서버를 구축해서 구현할 수 있는 기능이다. 한 달 액티브 유저가 100만명 정도 되면 자체 서버를 구축할만한 수익이 발생할 것이다. 한국 개발자가 Updates 자체 서버를 쉽게 구축할 수 있도록 패키지를 만들어 두었다. 자체 서버를 구축해서 Supabase/AWS/Firebase 등등에 연결해서 사용하면 된다. 이렇게 되면 서버 비용만 청구될 뿐이지 서비스 정기 구독료가 발생하지는 않는다. 그래서 100만 액티브 유저 이상의 규모의 서비스에서는 자체 서버 구축이 훨씬 저렴한 선택이지이다.
프로젝트 설정
공식 문서에 있는 명령어 하나로 프로젝트 설정을 쉽게 끝낼 수 있다. EAS Update 설정 명령어를 실행하면 채널이 생성된다. 크게는 빌드 이름과 똑같은 Development, Preview, Production 이 생긴다. Development 빌드를 했다면 development build 만 최신 업데이트를 받을 수 있는 형태를 가진다. 어떤 빌드를 할때 어떤 채널을 업데이트 할지 타겟 설정은 eas.json에 추가가 된다.
# Initialize your project with EAS Update eas update:configure// eas.json { ..., "build": { "development": { "devlopmentClient": true, "distribution": "internal", "channel": "development" }, "preview": { "distribuion": "internal", "channel": "preview" }, "production": { "autoIncrement": true, "channel": "production" } }, "submit": { "production": {} } }app.json에는 expo 속성으로 updates가 추가된다. 자체 구축한 Updates 커스텀 서버가 있다면 이 url 주소로 바꿔주면 된다. 그리고 iOS의 경우 runtimeVersion 내부에 policy로 되어 있고, Android의 경우 일반적인 버전 표기로 되어 있다. 이러한 iOS와 Android의 차이점은 Prebuild를 통해서 프로젝트 내부에 android 또는 ios 폴더가 생성되었다면, 그때부터는 android에 작성된 것처럼 정식 버전을 적어주어야 한다. 아직 Prebuild를 수행 안해서 ios 폴더가 없다면, 알아서 앱 버전을 따라가도록 세팅이 된다. ios 부분도 Prebuild를 통해서 ios 폴더가 생성되면 android와 동일하게 버전 정보가 추가될 것이다.
// app.json { "expo": { "newArchEnabled": true, "ios": { ... "runtimeVersion": { "policy": "appVersion" } }, "android": { ... "package": "com.morgankim.threads", "googleServicesFile": "./google-services.json", "runtimeVersion": "1.0.0" }, ..., "runtimeVersion": { "policy": "appVersion" }, ..., "plugins": [ ..., "expo-apple-authentication" ], "experiments": { "typedRoutes": true }, "extra": { "apiUrl": "https://threads.nodebird.com", "router": {}, "eas": { "projectId": "ddslksd-sdlk-sds-asds-sdlksdlsdk" } }, "updates": { "url": "https://u.expo.dev//ddaf49bd-3a77-4asd-ade-83csdsklds" } } }EAS Update 명령어
채널 이름에 development, preview, production 중에 하나를 넣어주면 되고, message에는 GitHub 커밋처럼 작업 내용을 작성해주면 된다. 그러면 자동으로 업데이트 빌드가 되고 변경 또는 추가된 기능을 이제 앱을 껐다가 재시작하면 다운로드 받을 수 있다. 이것도 배포의 일종이기 때문에 오남용 할 수가 있게 된다. 그래서 항상 development build에서 먼저 체크를 해보는 것이 좋다. development build에서는 Updates API가 동작하지 않는다. 이 경우에는 Preview Updates in development builds 문서를 참고하여 EAS 업데이트를 일단 development 채널에 한 뒤, Dev Client 앱(development build 홈)에서 Extension을 누르고 최신 development 채널을 선택(open)하면 업데이트가 적용된 빌드를 체험해볼 수 있다. 이렇게 development 빌드에서 업데이트를 체험을 해본 뒤에 성공적일 경우 EAS 업데이트를 preview로 넘겨서 다시 검증해보는 것을 추천한다.
eas update --channel [channel-name] --message "[message]"Updates 동의 구하기
업데이트가 있을 경우 앱을 열었을 때 무작적 업데이트를 하는 것이 아닌, 사용자에게 미리 동의를 구하는 것이 좋다. API를 사용해서 사용자에게 동의를 구하고 업데이트를 할 수 있도록 기능을 추가해 본다. Updates는 Dev. build에서는 동작하지 않고, preview/production에서만 동작하기 때문에 조건문을 넣어준다. development 환경에서는 Preview Updates API (Dev Client Extension 탭)를 사용해서 확인하자.
_layout.tsx에 추가한 새로운 onFetchUpdateAsync() 함수는 먼저 업데이트가 있는지 확인을 한다. 업데이트가 있다면 업데이트를 가져오고 지금 다시 로딩을 할 것인지 Alert으로 사용자에게 동의를 구하는 동작을 한다. 물론 Alert를 사용하지 않고 View를 활용해서 커스텀 모달을 사용할 수 있다.
npx expo install expo-updates// _laytou.tsx import * as Updates from "expo-updates"; ... function AnimatedSplashScreen({ ... const onImageLoaded = async () => { try { // 데이터 준비 await Promise.all([ AsyncStorage.getItem("user").then((user) => { updateUser?.(user ? JSON.parse(user) : null); }), onFetchUpdateAsync(), // 실행 ]) ... } catch (error) { console.error(error); } finally { setIsAppReady(true); } }; ... async function onFetchUpdateAsync() { try { if (!__DEV__) { const update = await Updates.checkForUpdateAsync(); if (update.isAvailable) { await Updates.fetchUpdateAsync(); Alert.alert("Update available", "Please update your app", [ { text: "Update", onPress: () => Updates.reloadAsync(), }, { text: "Cancel", style: "cancel"} ]); } } } catch (error) { console.error(error); } } })참고로 updates 관련 훅도 지원하고 있다.
const { currentlyRunning, isUpdateAvailable, isUpdatePending } = Updates.useUpdates(); console.log("currentlyRunning", currentlyRunning); console.log("isUpdateAvailable", isUpdateAvailable); console.log("isUpdatePending", isUpdatePending);업데이트 되돌리기
혹시 업데이트를 잘못한 것 같다고 생각될 때 되돌릴 수 있는 명령어다. 명령어를 실행하면 어떤 것으로 되돌릴 지 두가지 선택지가 나오게 된다. Published Update는 EAS Updates로 빌드한 결과이고, Embedded Update는 EAS Build로 빌드한 결과물이다. 이 rollback 기능은 Git과 다르게 엉성한 부분이 많다. 모든 update나 build 기록으로 되돌아 갈 수 있는 것도 아니며, 어떤 update나 build로 되돌아가면 그 이전의 버전으로 되돌아갈 수 없다. 추정하기로는 EAS Update 할때마다 커밋이 하나 자동으로 생기는데 그 커밋 때문에 그 전으로 되돌아가는 것이 안되는 상황으로 보인다. 그래서 rollback 기능은 추천하지 않는 기능이다.
결국 대안으로 preview 채널로 EAS updates를 실행하면서 불편하지만 수동으로 코드를 수정하고 message에 rollback이라고 남기는 것이다.
eas update:rollback ? Witch type of update would you like to roll back to? Published Update # EAS Updates > Embedded Update # EAS Builds3. EAS Submit
배포하는 과정이라는 것이 앱을 위한 정보들을 다 넣고 빌드 결과물을 업로드하면 된다. 그 업로드를 기존의 방식은 Android Studio를 통해 Play Store에 업로드를 따로 Xcode를 통해 App Store 따로 업로드를 해주었다. 그런데 Expo 에서는 그런 과정들을 간단하게 줄여놨다. 다만 서비스 주체의 정보 입력(앱 프로필 사진, 앱 소개 등)은 직접 수동으로 입력해야 한다. 그걸 해결하기 위해서 EAS 메타 데이터 기능도 나왔지만 아직 Preview 단계이기 때문에 정식 서비스가 시작되면 그때 적극적으로 활용하도록 하자.
Submit을 하기 위해서는 유료 계정이 필요하다. 구글(play store)은 $25를 한번 결제하면 평생 쓸 수 있는 계정이 필요하고, 애플(app store)는 매년 $99를 지불해야 한다.
Expo 공식문서를 따라서 Submit을 진행해보자.
Submit: android
(1) 구글은 구글 플레이 콘솔에서 회원가입 후 결제를 해준다.
(2) 구글 플레이 콘솔에 앱을 등록해준다.
(3) Google Service Account 만들기 (FCM push 알림할 때 만들었음)
- 기존에 만든걸 재사용해도 되고, 따로 Google Cloud에서 따로 만들어도 된다.
- push 알림과 서비스 계정을 분리해도 좋다. (보안에 좋음)
- 권한별로 Service Account를 분리하는 것은 나쁜 것이 아니다. (유출 시 하나만 폐기하면 되기 때문)
(4) eas-cli 에 로그인한다. (없을 시 설치)
npm install -g eas-cli && eas login(5) app.json에 패키지 이름을 등록(추가)한다.
{ "android": { "package": "com.morgankim.threads" } }(6) Production 빌드
profile을 이제 production으로 설정하여 eas build를 수행해준다.
eas build --platform android --profile production(7) 빌드 결과물 업로드 (aab 파일)
안드로이드는 처음 한 번만 앱을 aab 파일로 올려주면 된다. 구글 플레이 콘솔에서 Create App으로 집적 등록을 하고 나서, 내부 테스트에 테스터들을 추가하고 EAS build를 하면 생성되는 .aab 파일을 App bundles에 업로드 해주면 된다. (참고문서)
앱 버전 관리
버전관리는 Expo의 App version management에 따라서 지정을 해주면 된다. app.json 또는 app.config.js에 있는 version이 사용자들에게 보여지는 버전이다. 이 버전은 1.0.0 식으로 세 자리 숫자로 표기하는 시맨틱 버전 표기 방식을 사용한다. 앱 심사 없이 EAS Updates로 바로 업데이트를 한 경우에는 패치를 올려서 1.0.1 식으로 버전을 올려준다. 앱 심사에 제출하는 네이티브 단이 수정되면 1.1.0 아니면 2.0.0으로 올리거나 마이너나 메이저를 올린다.
⚠️주의할 점은 배포를 할 때마다 버전을 꼭 올려줘야 한다.
(8) EAS Submit
// 안드로이드 앱 제출 eas submit --platform android // 빌드와 앱 제출을 함께 수행 eas build --platform android --auto-submitApp versions
- version: 시멘틱 버저닝
- android.versionCode: (type: integer) Play Store에 제출할 때마다 1, 2, 3, 4 식으로 올려준다.
- ios.buildNumber: App 버전과 동일하게 시멘틱 버전 표기 방식으로 한다. (Major, Minor, Patch)
// 앱 버전 정보 설정 (Kotlin) android { namespace = "com.example.testapp" compileSdk = 33 defaultConfig { applicationId = "com.example.testapp" minSdk = 24 targetSdk = 33 versionCode = 1 // 1, 2, 3, ... versionName = "1.0" ... } ... } ...Submit: ios
iOS 앱을 Submit 하는 과정도 공식문서에 자세히 설명되어 있다.
(1) 애플 개발자 계정을 생성하고 결제한다.
(2) app.json 또는 app.config.js에 bundleIdentifier를 등록한다.
- ios bundleIdentifier는 웬만하면 안드로이드 패키지명과 동일하게 가져가는 것이 좋다.
// app.json { "expo": { ..., "ios": { "supportsTablet": true, "usesAppleSignIn": true, "bundleIdentifier": "com.morgankim.threads", "infoPlist": { "ITSAppUsesNonExemptncryption": flase, }, }, ..., } }(3) eas-cli 로그인 (필요시 설치)
npm install -g eas-cli && eas login(4) Production Build
iOS의 경우 앱 정보는 App Store Connect에 앱 정보를 적어두고, eas.json에 설정된 acsAppId로 나의 앱과 연결해두면 Submit 할 때 알아서 앱 정보에 빌드한 파일을 업로드 해준다. iOS는 빌드한 파일을 수동으로 업로드하는 과정은 없다.
eas build --platform ios --profile production// eas.json { "submit": { "production": { "ios": { "ascAppId": "your-app-store-connect-app-id" } } } }
ascAppId 값인 Apple ID 정보 위치 EAS Submit 자체를 사용하지 않는다 하더라도 공식문서를 읽어보면 대충 어떤 식으로 앱 빌드를 해서 올려야 되는지 나오기 때문에 EAS Submit 공식문서를 읽어 보기를 추천한다.
자동 버저닝 설정
마지막으로 eas.json에서 체크해줘야 할 사항이 있다. EAS 서버는 앱의 개발자용 빌드 버전 (android.versionCode와 ios.buildNumber)를 원격으로 저장하고 관리할 수 있다. 그래서 공식문서처럼 아래 JSON을 기본값으로 설정해주면 알아서 버전 관리가 된다. 이를 활성화 해주기 위해 "appVersionSource": "remote"와 "autoIncrement": true 설정을 잘 확인해주자. 이 설정을 통해서 내가 신경쓰지 않아도 앱을 제출할 때마다 빌드 넘버가 알아서 1씩 올라가게 된다.
// eas.json { "cli": { "version": ">= 16.12.0", "appVersionSource": "remote" }, "build": { "development": { "developmentClient": true, "distribution": "internal" }, "preview": { "distribution": "internal" }, "production": { "autoIncrement": true } }, "submit": { "production": {} } }4. Expo에서 Native 모듈을 사용하는 방법
Expo의 Native 모듈을 사용하기 위해 Expo Modules API를 사용할 것이다. 공식 문서만 잘 따라해도 큰 어려움은 없을 것이다. Expo 환겨에서 Native 모듈을 사용하기 가장 간단한 방법이며, --local을 붙이는 또는 안 붙이냐에 따른 2 가지 모드가 있다. --local을 붙이는 것은 외부 공유 없이 프로젝트에서만 사용할 때 붙이는 것이다. 반대로 붙이지 않는 것은 Expo 라이브러리를 만들거나 NPM 같은 곳에 직접 업로드하는 것처럼 공유를 하는 경우에 해당한다.
// 프로젝트에서만 사용하는 모듈 생성 (내부용) npx create-expo-module@latest --local // Expo lib.를 만들 때 사용하는 모듈 생성 (공유용) npx create-expo-module@latest내부용 Native 모듈 개발
해당 기능을 사용해보기 위해서 내부적으로 사용할 Native 모듈을 개발해본다. 로컬 모듈 이름은 대문자 작성이 안되어 background-uploader로 지어주고, Native 모듈 이름은 카멜케이스로 BackgroundUploader로 해준다. Android 패키지 이름도 자동완성 값을 그대로 사용해준다. 이름은 자유롭게 작성해주면 된다.
$ npx create-expo-module@latest --local Need to install the following packages: create-expo-module@1.0.10 Ok to proceed? (y) y The local module will be created in the modules directory in the root of your project. Learn more: https://expo.fyi/expo-module-local-autolinking.md √ What is the name of the local module? ... background-uploader √ What is the native module name? ... BackgroundUploader √ What is the Android package name? ... expo.modules.backgrounduploader ⠸ Downloading module template from npm Couldn't download the versioned template from npm, falling back to the latest version. ✔ Downloaded module template from npm registry. ✔ Created the module from template files ✅ Successfully created Expo module in modules/background-uploader ...modules 폴더
create-expo-module 명령어의 실행이 끝나면 moduels 폴더가 생성된다. 로컬 모듈명과 동일한 폴더 내부에는 android, ios, src 폴더와 함께 expo-module.config.json과 index.ts 파일이 존재한다. 크게 보면 View를 담당하는 곳, module을 담당하는 곳으로 구별하면 된다. module 단은 담당하는 단은 함수나 메서드를 불러와서 사용하는 기능 위주들이다. (expo-location에서 GPS 좌표 얻기) View 단은 화면에 그릴 WebView 컴포넌트라던가 하는 컴포넌트를 추가하려면 View 파일을 사용하면 된다.
참고로 create-expo-module 명령어에 --local을 붙이지 않으면 프로젝트에 modules 폴더가 생기는 것이 아니라 아예 전체 프로젝트가 하나 생기게 된다. 완전히 다르다. 그 전체 프로젝트 안에는 package.json 이나 tsconfig.json도 따로 존재하는 완전히 하나의 라이브러리가 되는 것이다.

생성된 modules 폴더 이번에는 View 단은 사용하지 않고 Module 단만 사용하기 때문에 src 폴더에서 View파일은 제거해 준다. 그리고 web.ts 파일이 있는데, 이 파일은 React Native 웹용이다. 웹용은 Native 모듈을 할 때 완전히 다르다. 우리가 생각하는 Native는 Android/iOS인데 웹은 그 기반 환경이 다르고 기능 제약도 많을 수 밖에 없기 때문에 따로 나온다. web.ts 파일도 이번에 사용되지 않으므로 지워준다.
공식문서의 tutorial에서도 native module과 native 컴포넌트를 만드는 native view로 나뉘어져 있다. 기본적으로 Modules API를 사용하기 위해서는 Kotlin과 Swift를 전부 알아야 한다. 그리고 만든 것을 requireNativeViewManager를 통해서 View단과 연결을 해주면 된다.
공식문서의 Module API를 보면 Swift와 Kotlin을 간단하게 어느정도 어떻게 작성해줘야 하는지 나온다. 두 언어는 유사한 형태가 많고 조그만 공부하면 코드를 작성할 수는 있겠지만 시간은 오래 걸릴 것이다. 이번는 Event를 보낼 수 있는 기능을 만들어 본다.
참고로 Expo Modules가 아닌 Turbo Native Modules이라는 것이 있다. Turbo Native Modules는 보통 Expo가 아닌 React Native 에서 Native를 사용하는 방법이다. 보통 Expo에서는 Turbo Native Modules를 C++ 같은 로우 레벨에 접근할 때만 사용한다. 그래서 Expo를 사용하는 개발자는 Expo Modules API를 사용하는 것이 가장 간단한 방법이다.
Expo SDK 버전과 호환되도록 추가적인 라이브러리를 설치해준다.
npx expo install expo-modules-corebackground-uploader의 기능은 게시글 업로드하는 기능을 modal.tsx에서 만들었었다. 그런데 중간에 앱을 중간에 꺼버리면 업로드가 중단돼 버리기 때문에 이미지나 용량이 너무 큰 파일의 경우 문제가 될 수 있다. 이런 업로드를 백그라운드에서 하는 코드이다.
예전에는 전부 다 공부하고 했어야 됐는데 요즘은 AI가 워낙 잘 되어 있어서 질문도 하기 쉽고, AI 에이전트를 사용해서 바이브코딩을 할 수도 있다. 그렇기 때문에 Native 모듈단을 개발하는 부담이 매우 줄어들었다. 그래서 Expo가 더 좋은 점도 있는 것 같다. 이제는 Native (Switft/Kotlin) 이게 또 기존의 Objective-C, Java 보다 문법도 훨씬 간단하다. 이제 이를 AI와 함께 코드를 작성하고 고칠 수 있다 보니 훨씬 환경이 나아졌다.
안드로이드 패키지 설치는 build.gradle 설정을 package.json 처럼 사용한다. build.gradle에서 dependencies 설정을 넣어서 의존성을 꼭 설치해 주어야 문제없이 코드가 동작한다. 여기서 작업하는 코드들은 Native 단의 코드더라도 안드로이드 스튜디오에서 실행되지 않는다. 왜냐하면 로컬 모듈이기 때문에 독립적으로 돌아가게 설정은 안 되어 있다. 이 모듈은 현재 프로젝트 안에 들어있어야지만 실행이 되게 되어 있다. 뭔가 일부 기능이 누락되어 있고 현재 프로젝트의 안드로이드 폴더에 의존하는 것이라고 생각하면 된다.
안드로이드에서처럼 iOS도 똑같이 비슷한 기능을 구현해서 Expo Modules Core를 사용하여 TypeScript에 연결해주면 된다. expo-module.config.json은 폴더 안에 들어있어야지만 해당 폴더가 모듈로 인식이 된다. 여기에 어떤 플랫폼을 지원할 것이지 설정할 수 있는데, apple은 iOS, macOS, tvOS 3개를 다 포함하는 개념이다. 어떤 앱들은 ios만 적혀있는 경우도 있고, 웹이 없는 경우도 있다.
// modules/background-uploader/src/BackgroundUploaderModule.ts import { requireNativeModule } from 'expo-modules-core'; type BackgroundUploaderType = { startUpload(params: string): void; }; export default requireNativeModule<BackgroundUploaderType>( 'BackgroundUploader' );// expo-module.config.json { "platforms": ["apple", "android"], "apple": { "modules": ["BackgroundUploaderModule"] }, "android": { "modules": ["expo.modules.backgrounduploader.BackgroundUploaderModule"] } }이제 만들어진 Native Module을 model.tsx에서 사용하기만 하면 된다. 로컬 모듈은 이렇게 간단하게 사용할 수 있다. 외부 모듈이라면 당연히 NPM에 올리거나 파일 기반의 파일 경로를 package.json에 적어주면 로컬 모듈처럼 상대 경로가 아닌, npm에서 설치한 모듈처럼 사용할 수 있다.
에러 메시지 같은 것들이 필요하다면 Native 모듈 단에서 에러 메시지까지 담아서 sendEvent를 보내주면 된다. 결과 자체는 useEffect와 addListener()를 사용해줘야 한다. 이벤트 리스너는 구독제이기 때문에 컴포넌트가 언마운트 될 때 클리어 함수에 remove()도 해줘야 한다. 이 부분을 빠뜨리면 게시글 하나 등록할 때 emitter가 서너번 호출될 수 있다.
... import { EventEmitter } from "expo-modules-core"; import BackgrouondUploaderModule from "../modules/background-uploader"; import { AuthContext } from './_layout'; ... export default function Modal() { ... const { user } = useContext(AuthContext); useEffect(() => { const emitter = new EventEmitter<Record<string, any>>(); const sub = emitter.addListener( 'uploadFinished', (data: { threadId: string; success: boolean; id: string }) => { console.log('uploadFinished', data); if (data.success) { console.log('uploadFinished', data); Toast.hide(); Toast.show({ text1: 'Post posted', type: 'customToast', visibilityTime: 5000, position: 'bottom', bottomOffset: 20, onPress: () => { console.log('post pressed', data); router.replace(`/@${user?.id}/post/${data.threadId}`); Toast.hide(); }, }); } else { Toast.hide(); Toast.show({ text1: 'Post failed', type: 'customToast', visibilityTime: 5000, position: 'bottom', bottomOffset: 20, }); } } ); return () => { sub.remove(); }; }, []); ...handlePost에 있던 코드의 기능을 네이티브 단으로 보내버렸기 때문에 코드가 매우 간결해진다. 그리고 반대로 결과를 useEffect에서 받게 된다.
const handlePost = () => { console.log('handlePost', threads); Toast.show({ text1: 'Posting...', type: 'customToast', visibilityTime: 5000, position: 'bottom', bottomOffset: 20, }); BackgroundUploaderModule.startUpload(JSON.stringify(threads)); };백그라운드 업로드 기능 개발이 완료가 되고 나면 빌드를 해준다. 빌드를 하다보면 문제가 생길 수가 있다. 실습을 하면서 그리고 기존의 배포과정 때문에 android/ios 폴더가 생겼을 것이다. 이 Native module의 build 과정은 android/ios 폴더 둘다 존재해야 되고, 없다면 prebuild를 먼저해서 폴더를 생성해줘야 한다. 그래야지만 제대로 Native module이 동작할 수 있다. 그리고 Native 단을 수정하면 무조건 다시 빌드를 해야 한다.
npx expo run:androidNative module가 빌드 과정에서 에러가 발생하면 전부 Logcat이나 Xcode 콘솔 로그를 보면 디버깅을 된다. 네이티브 단에서 발생하는 에러는 Native 개발환경에만 기록되기 때문이다. modules API 빌드를 하다가 발생한 에러는 w:는 warning으로 경고이기 때문에 넘어가도 되고, e:로 시작되는 error를 보아야 한다. 에러들을 복사해서 AI 에이전트와 함께 해결해 나가면 된다. Android 빌드는 한번 빌드한 부분은 다시 진행할 때 매우 빠르게 처리 되기 때문에 에러가 났다고 해서 다시 빌드하느라 매우 오래 걸리는 건 아니다.
로컬 모듈을 사용할 때는 android/ios 폴더에 Native 소스코드를 입력하고, src 폴더의 *Module.ts에 타입을 적용해주면 된다. 굳이 기본으로 생성되는 *.types.ts 파일을 사용하지 않아도 된다.
modules API 자체는 로컬 모듈을 만들고 나서 상대 경로로 불러오면 되서 간단한 편이다. 문제는 코틀린과 스위프트 코드를 작성할 줄 알아야 된다. 이 부분은 어쩔 수 없이 배워야 되는 부분이기 때문에 감수하도록 하자. 그래도 JavaScript에 연결해서 사요하는 부분은 정말 간단해졌다고 할 수 있다. EventEmitter를 통해서 이벤트 기반으로 소통을 하면 편하다. 내가 호출할 때는 함수로 호출을 하고, 결과값을 받을 때는 콜백함수를 복잡하게 생각하기 보다는 간단하게 EventEmitter를 통해서 받으면 된다. (문법도 좀 더 복잡한 콜백함수 안 쓰는 걸 추천한다.)
.
.
.
.
.
.
관련 코드들
GitHub - redcontroller/threads-clone: 제로초 React native 학습 레포
제로초 React native 학습 레포. Contribute to redcontroller/threads-clone development by creating an account on GitHub.
github.com
출처: React Native with Expo: 제로초에게 제대로 배우기
React Native with Expo: 제로초에게 제대로 배우기| 제로초(조현영) - 인프런 강의
현재 평점 4.9점 수강생 553명인 강의를 만나보세요. 웹 개발자가(특히 React 개발자라면) 정말 손쉽게 앱을 출시할 수 있는 시대가 되었습니다. Expo와 함께라면 더더욱 빠르게 Android와 iOS 앱 모두를
www.inflearn.com
'강의노트 > React Native' 카테고리의 다른 글
[RN 강의] Expo Go 그 이상의 기능들 (development builds) (0) 2025.09.29 [RN 강의] 게시글관련 기능 고도화하기 (0) 2025.09.20 [RN 강의] 편리한 Expo 라이브러리로 기능 구현 (0) 2025.09.08 [RN 강의] Mission: 게시글의 주제(topic) 선정 기능 (0) 2025.09.07 [RN 강의] 웹 개발과 유사한 Expo 앱 개발 (3) 2025.09.06