[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user