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

378 lines
9.3 KiB
Markdown

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