---
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<SyncWorker>(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<SyncWorker>()
    .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<SyncWorker>()
            .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=<lastSyncTimestamp>
→ { 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<Context>()
    val worker = TestListenableWorkerBuilder<SyncWorker>(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]]
