--- id: mobile-background-sync title: Background Sync — iOS / Android 비교 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [mobile, background, sync, vibe-coding] tech_stack: { language: "Swift / Kotlin", applicable_to: ["iOS", "Android"] } applied_in: [] aliases: [background fetch, BGTaskScheduler, WorkManager, periodic sync, Doze mode] --- # Background Sync > App 가 background 일 때 sync. **iOS = BGTaskScheduler (제약 강함). Android = WorkManager (더 유연)**. Battery + 데이터 절약 OS 가 throttle. ## 📖 핵심 개념 - iOS: OS 가 사용 패턴 학습 → 자유 결정. - Android: WorkManager + 제약 (battery, network). - Doze (Android) / Low Power (iOS): 추가 throttle. - Push: 가장 reliable trigger. ## 💻 코드 패턴 ### iOS — BGTaskScheduler (modern) ```swift import BackgroundTasks // Info.plist // BGTaskSchedulerPermittedIdentifiers: ["com.acme.refresh"] // UIBackgroundModes: [fetch, processing] ``` ```swift // Register (App init) BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.acme.refresh", using: nil) { task in handleRefresh(task as! BGAppRefreshTask) } func handleRefresh(_ task: BGAppRefreshTask) { scheduleNextRefresh() let op = SyncOperation() task.expirationHandler = { op.cancel() } op.completionBlock = { task.setTaskCompleted(success: !op.isCancelled) } OperationQueue().addOperation(op) } func scheduleNextRefresh() { let request = BGAppRefreshTaskRequest(identifier: "com.acme.refresh") request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15분 후+ try? BGTaskScheduler.shared.submit(request) } ``` → OS 가 사용자 패턴 학습 — "보통 9시 사용" 시간 가까이 트리거. ### iOS — Long task (BGProcessingTask) ```swift BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.acme.cleanup", using: nil) { task in handleCleanup(task as! BGProcessingTask) } let request = BGProcessingTaskRequest(identifier: "com.acme.cleanup") request.requiresNetworkConnectivity = false request.requiresExternalPower = true // 충전 중 만 try? BGTaskScheduler.shared.submit(request) ``` → 1-30 min. 충전 중 / 야간. ### iOS — silent push (background) ```ts // Server { aps: { 'content-available': 1 }, syncKey: '...' } ``` ```swift // AppDelegate func application(_ app: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completion: @escaping (UIBackgroundFetchResult) -> Void) { Task { await syncData(key: userInfo["syncKey"] as? String ?? "") completion(.newData) } } ``` → Push 가 trigger. Server-driven. ⚠️ iOS 가 silent push throttle (1-2 / hour). Reliable 가정 X. ### Android — WorkManager ```kotlin implementation("androidx.work:work-runtime-ktx:2.9.0") ``` ```kotlin class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result { return try { val data = api.fetchUpdates() db.update(data) Result.success() } catch (e: Exception) { if (runAttemptCount < 3) Result.retry() else Result.failure() } } } // Schedule val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) .build() WorkManager.getInstance(ctx).enqueueUniquePeriodicWork("sync", ExistingPeriodicWorkPolicy.KEEP, request) ``` → 15 min minimum. OS 가 정확 시점 결정. ### Android — Expedited (즉시 + 짧은) ```kotlin val request = OneTimeWorkRequestBuilder() .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() WorkManager.getInstance(ctx).enqueue(request) ``` → Foreground priority. 10 min limit. ### Android — Periodic vs One-time ``` Periodic: 15 min minimum. Repeats. One-time: 한 번. (즉시 또는 delay) ``` ### Android — FCM data message ```ts // Server { data: { syncKey: '...' }, android: { priority: 'high' } } ``` ```kotlin class MyFcmService : FirebaseMessagingService() { override fun onMessageReceived(msg: RemoteMessage) { val key = msg.data["syncKey"] ?: return // Schedule WorkManager (즉시) val request = OneTimeWorkRequestBuilder() .setInputData(workDataOf("key" to key)) .build() WorkManager.getInstance(this).enqueue(request) } } ``` → Push trigger + WorkManager 처리. ### Common 패턴 — 동기화 strategy ``` 1. Pull (period): - WorkManager / BGTask - 매 15-60 min - Battery / data 비싸지만 simple 2. Push-driven: - Server send notification - App 가 fetch - Reliable + efficient 3. WebSocket / SSE (foreground 만): - Real-time - Background = X (suspend) 4. CDC / sync: - Cursor / version - Delta only ``` → Push + delta sync = best. ### Delta sync ```ts // Server GET /sync?since= → { items: [...changed], deleted: [...ids], cursor: 'new-cursor' } // Client const response = await api.sync({ since: lastSync }); db.applyDelta(response.items, response.deleted); lastSync = response.cursor; ``` → 적은 bandwidth + battery. ### iOS Doze / Low Power ```swift import UIKit if ProcessInfo.processInfo.isLowPowerModeEnabled { // Reduce sync frequency } NotificationCenter.default.addObserver(forName: .NSProcessInfoPowerStateDidChange, object: nil, queue: .main) { _ in // Re-evaluate } ``` ### Android Doze ```kotlin // Idle 상태 — WorkManager 가 throttle (15분 이하 안 됨). // FCM high-priority 가 wake up 가능. if (powerManager.isIgnoringBatteryOptimizations(packageName)) { // 사용자가 unrestricted 허용 } ``` ### Background 권한 (사용자 friendly) ``` Android 12+: - Background activity 차단 강 - Foreground service type 명시 (위 [[Android_Foreground_Service_Patterns]]) iOS: - Background mode 일부만 - 권한 자동 X — Apple 가 사용 패턴 결정 ``` ### Sync conflict ```ts // Server 와 client 가 둘 다 변경 - Last-write-wins (간단) - Merge (CRDT) - Conflict UI (사용자 해결) - Three-way merge (base + client + server) ``` ### Test (Android WorkManager) ```kotlin @Test fun testSyncWorker() = runTest { val context = ApplicationProvider.getApplicationContext() val worker = TestListenableWorkerBuilder(context).build() val result = worker.startWork().get() assertEquals(Result.success(), result) } ``` ### Test (iOS BGTaskScheduler) ``` 디버깅: - Xcode → Debug → Simulate Background Fetch - Physical device 권장 (정확한 throttle) ``` ### Battery / data 모니터링 ```kotlin val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) val isCharging = batteryManager.isCharging val isMetered = ConnectivityManager.isActiveNetworkMetered if (batteryLevel < 20 && !isCharging) skipSync() if (isMetered) lightSyncOnly() ``` ### iOS NetworkPathMonitor ```swift import Network let monitor = NWPathMonitor() monitor.pathUpdateHandler = { path in if path.usesInterfaceType(.cellular) { // Cellular — 작게 } else if path.usesInterfaceType(.wifi) { // Wifi OK } } monitor.start(queue: .global()) ``` ### Cross-platform abstraction (RN / Flutter) ```ts // react-native-background-fetch import BackgroundFetch from 'react-native-background-fetch'; BackgroundFetch.configure({ minimumFetchInterval: 15, enableHeadless: true, }, async (taskId) => { await syncData(); BackgroundFetch.finish(taskId); }); ``` ### Sync UI feedback ``` 사용자에 sync 결과 명시: - "Last synced 5 min ago" - "Sync failed — tap to retry" - "Pending changes: 3" → Trust + control. ``` ### Recurring vs one-time ``` Daily report: PeriodicWork / BGAppRefresh Specific event: OneTimeWork On data change: Push trigger On wifi: Constraint required ``` ### Best practices ``` 1. Minimize battery / data. 2. Push 가 reliable, periodic 는 best-effort. 3. Sync UI 보임 (last synced time). 4. Conflict resolution 명시. 5. Failure 알람 (사용자에). 6. Cellular 시 작게. 7. Test on real device. ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 일반 sync (15 min+) | iOS BGTask / Android WorkManager | | Real-time | Push + sync trigger | | 큰 작업 | iOS BGProcessingTask / Android Foreground Service | | 배터리 절약 | Constraint (charging, wifi) | | Reliable | Push primary | | Cross-platform | RN background-fetch / capacitor | ## ❌ 안티패턴 - **iOS background guarantee 가정**: Apple 가 결정. Best-effort. - **Periodic 너무 자주 (1 min)**: throttle. - **모든 사용자 push**: opt-out 무. - **Sync 매번 모든 데이터**: delta 만. - **Conflict 무시**: data loss. - **Battery / data 무관심**: 사용자 uninstall. - **Foreground service 없는 long task**: kill. ## 🤖 LLM 활용 힌트 - Push trigger + WorkManager / BGTask 처리. - Delta sync + cursor. - Constraint (battery, wifi). - iOS = best-effort. Android = 더 reliable. ## 🔗 관련 문서 - [[Android_WorkManager_Patterns]] - [[iOS_Background_Tasks]] - [[Mobile_Push_Deep]]