Files
2nd/10_Wiki/Topics/Coding/Mobile_Background_Sync.md
T
2026-05-09 22:47:42 +09:00

9.3 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
mobile-background-sync Background Sync — iOS / Android 비교 Coding draft B conceptual 2026-05-09 2026-05-09
mobile
background
sync
vibe-coding
language applicable_to
Swift / Kotlin
iOS
Android
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)

import BackgroundTasks

// Info.plist
// BGTaskSchedulerPermittedIdentifiers: ["com.acme.refresh"]
// UIBackgroundModes: [fetch, processing]
// 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)

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)

// Server
{
  aps: {
    'content-available': 1
  },
  syncKey: '...'
}
// 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

implementation("androidx.work:work-runtime-ktx:2.9.0")
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 (즉시 + 짧은)

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

// Server
{
  data: { syncKey: '...' },
  android: { priority: 'high' }
}
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

// 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

import UIKit

if ProcessInfo.processInfo.isLowPowerModeEnabled {
    // Reduce sync frequency
}

NotificationCenter.default.addObserver(forName: .NSProcessInfoPowerStateDidChange, object: nil, queue: .main) { _ in
    // Re-evaluate
}

Android Doze

// 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

// Server 와 client 가 둘 다 변경
- Last-write-wins (간단)
- Merge (CRDT)
- Conflict UI (사용자 해결)
- Three-way merge (base + client + server)

Test (Android WorkManager)

@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 모니터링

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

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)

// 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.

🔗 관련 문서