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

내가 이미 확인한 내용

  1. 업데이트가 Expo Dashboard의 production 채널에 표시됩니다.
  2. 앱을 강제 종료하고 다시 실행했으며, 기기에서 여러 번 수행했습니다.

목표

대시보드에 업데이트가 게시된 것으로 표시되는데도 TestFlight에 설치된 기기에서 OTA 업데이트가 적용되지 않는 이유를 파악하고 싶습니다. 누락된 설정, 캐시 문제, 혹은 필요한 네이티브 변경 사항에 대한 통찰을 제공해 주시면 감사하겠습니다.

다른 구성 파일이나 특정 로그가 필요하면 알려 주세요. 즉시 게시물을 업데이트하겠습니다.

Back to Blog

관련 글

더 보기 »