EAS Update가 채널이 일치하고 성공적으로 배포했음에도 불구하고 프로덕션 빌드에서 다운로드되지 않음
발행: (2026년 2월 9일 오전 11:26 GMT+9)
5 분 소요
원문: Dev.to
Source: Dev.to
위에 제공된 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역을 원하는 본문을 알려주시면 한국어로 번역해 드리겠습니다.
문제 요약
저는 Managed workflow가 아닌 bare React Native 프로젝트에서 EAS Update를 사용하고 있습니다. 업데이트는 정상적으로 게시되어 Expo Dashboard에 표시되지만, TestFlight를 통해 설치한 실제 디바이스에서는 변경 사항이 전혀 반영되지 않습니다.
- 업데이트가 대시보드에서 올바른 브랜치/채널에 표시됩니다.
- 앱을 여러 번 재시작해 보았습니다.
프로젝트 구성
app.config.ts
import { ExpoConfig, ConfigContext } from 'expo/config';
import * as dotenv from 'dotenv';
import path from 'path';
import pkg from './package.json';
const APP_VARIANT = process.env.APP_VARIANT || 'prod';
dotenv.config({ path: path.resolve(__dirname, `.env.${APP_VARIANT}`) });
interface CustomExpoConfig extends ExpoConfig {
'react-native-google-mobile-ads'?: {
android_app_id?: string;
ios_app_id?: string;
};
}
const convertVersionToNumber = (version: string) => {
const [major, minor, patch] = version.split('.').map(Number);
return major * 1_000_000 + minor * 1_000 + patch + 2;
};
export default ({ config }: ConfigContext): CustomExpoConfig => ({
...config,
name: 'GoalWith',
slug: 'goalwith',
version: pkg.version,
runtimeVersion: pkg.version,
ios: {
...config.ios,
bundleIdentifier: 'com.goalwith.goalwith',
buildNumber: convertVersionToNumber(pkg.version).toString(),
googleServicesFile: './ios/GoogleService-Info.plist',
},
android: {
package: 'com.goalwith',
versionCode: convertVersionToNumber(pkg.version),
},
extra: {
env: process.env.ENV,
apiUrl: process.env.API_URL,
kakaoAppKey: process.env.KAKAO_APP_KEY,
googleWebClientId: process.env.GOOGLE_WEB_CLIENT_ID,
admobIdAndroid: process.env.ADMOB_ID_ANDROID,
admobIdIos: process.env.ADMOB_ID_IOS,
eas: {
projectId: '8475b304-e536-458b-aa6a-6aea6e3e6939',
},
},
'react-native-google-mobile-ads': {
android_app_id: process.env.ADMOB_ID_ANDROID,
ios_app_id: process.env.ADMOB_ID_IOS,
},
updates: {
url: 'https://u.expo.dev/8475b304-e536-458b-aa6a-6aea6e3e6939',
requestHeaders: {
'expo-channel-name': 'production',
},
},
});
eas.json
{
"build": {
"development": {
"channel": "dev",
"env": {
"APP_VARIANT": "dev"
}
},
"production": {
"channel": "production",
"env": {
"APP_VARIANT": "prod"
}
}
}
}
AppDelegate.swift
import UIKit
import Expo
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
import GoogleSignIn
import RNBootSplash
import KakaoSDKCommon
import KakaoSDKAuth
import FirebaseCore
import EXUpdates
@main
class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ReactNativeDelegate?
var reactNativeFactory: RCTReactNativeFactory?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
AppController.initializeWithoutStarting()
FirebaseApp.configure()
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
self.window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: self.window,
launchOptions: launchOptions
)
if let rootView = self.window?.rootViewController?.view {
RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView)
}
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
// Show the app's signed-out state.
} else {
// Show the app's signed-in state.
}
}
if let kakaoAppKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_APP_KEY") as? String {
KakaoSDK.initSDK(appKey: kakaoAppKey)
} else {
print("Warning: KAKAO_APP_KEY not found in Info.plist")
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
var handled = false
handled = GIDSignIn.sharedInstance.handle(url)
if handled { return true }
if AuthApi.isKakaoTalkLoginUrl(url) {
handled = AuthController.handleOpenUrl(url: url)
if handled { return true }
}
return super.application(app, open: url, options: options)
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
if let updatesUrl = AppController.sharedInstance.launchAssetUrl() {
return updatesUrl
}
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}
Expo.plist
EXUpdatesEnabled
EXUpdatesCheckOnLaunch
ALWAYS
EXUpdatesLaunchWaitMs
0
EXUpdatesRequestHeaders
expo-channel-name
production
EXUpdatesURL
https://u.expo.dev/8475b304-e536-458b-aa6a-6aea6e3e6939
EXUpdatesRuntimeVersion
1.0.5
내가 이미 확인한 내용
- 업데이트가 Expo Dashboard의 production 채널에 표시됩니다.
- 앱을 강제 종료하고 다시 실행했으며, 기기에서 여러 번 수행했습니다.
목표
대시보드에 업데이트가 게시된 것으로 표시되는데도 TestFlight에 설치된 기기에서 OTA 업데이트가 적용되지 않는 이유를 파악하고 싶습니다. 누락된 설정, 캐시 문제, 혹은 필요한 네이티브 변경 사항에 대한 통찰을 제공해 주시면 감사하겠습니다.
다른 구성 파일이나 특정 로그가 필요하면 알려 주세요. 즉시 게시물을 업데이트하겠습니다.