이 글은 누구를 위한 것인가
- 앱이 백그라운드에서 데이터를 미리 가져오게 하고 싶은 팀
- 대용량 파일 다운로드를 백그라운드에서 안정적으로 처리하려는 개발자
- iOS 백그라운드 실행 정책과 제약을 이해하려는 iOS 개발자
들어가며
iOS는 배터리와 성능을 위해 백그라운드 실행을 엄격히 제한한다. BackgroundTasks 프레임워크의 두 가지 작업 유형을 올바르게 사용하면 앱이 포그라운드에 없어도 콘텐츠를 최신 상태로 유지할 수 있다.
이 글은 bluefoxdev.kr의 iOS 백그라운드 처리 가이드 를 참고하여 작성했습니다.
1. iOS 백그라운드 실행 유형
[BackgroundTasks 프레임워크]
BGAppRefreshTask:
목적: 짧은 콘텐츠 업데이트
실행 시간: ~30초
실행 조건: 시스템 재량 (배터리, 사용 패턴 기반)
사용 예: 뉴스 피드 미리 가져오기, 메시지 확인
Info.plist: BGTaskSchedulerPermittedIdentifiers
BGProcessingTask:
목적: 장시간 처리 작업
실행 시간: 수분 ~ 수십분
실행 조건: 충전 중, Wi-Fi 연결 시 (옵션)
사용 예: DB 마이그레이션, 머신러닝 모델 업데이트, 대용량 동기화
requiresNetworkConnectivity: true/false
requiresExternalPower: true/false
URLSession 백그라운드 다운로드:
실행 시간: 무제한 (시스템 관리)
목적: 대용량 파일 다운로드/업로드
앱 종료 후에도 계속 실행
완료 시 앱 깨워서 알림
사일런트 푸시 (content-available: 1):
실행 시간: ~30초
목적: 서버 이벤트에 반응해 콘텐츠 업데이트
제약: 과도한 사용 시 시스템이 무시
2. BackgroundTasks 구현
import BackgroundTasks
import UIKit
// AppDelegate에서 등록
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerBackgroundTasks()
return true
}
private func registerBackgroundTasks() {
// BGAppRefreshTask 등록
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.refresh",
using: nil
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
// BGProcessingTask 등록
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.processing",
using: nil
) { task in
self.handleProcessingTask(task: task as! BGProcessingTask)
}
}
// 앱이 백그라운드로 갈 때 스케줄 등록
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
scheduleProcessingTask()
}
}
// BGAppRefreshTask 처리
extension AppDelegate {
func handleAppRefresh(task: BGAppRefreshTask) {
// 다음 refresh 즉시 스케줄 (연쇄 실행)
scheduleAppRefresh()
let operation = ContentRefreshOperation()
task.expirationHandler = {
// 시간 초과 시 작업 중지
operation.cancel()
}
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}
OperationQueue.main.addOperation(operation)
}
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15분 후 최소
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("BGAppRefreshTask 스케줄 실패: \(error)")
}
}
}
// BGProcessingTask 처리
extension AppDelegate {
func handleProcessingTask(task: BGProcessingTask) {
scheduleProcessingTask() // 다음 실행 예약
let syncTask = Task {
do {
try await performHeavySync()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
syncTask.cancel()
}
}
func scheduleProcessingTask() {
let request = BGProcessingTaskRequest(identifier: "com.app.processing")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = true // 충전 중에만
request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // 1시간 후
try? BGTaskScheduler.shared.submit(request)
}
private func performHeavySync() async throws {
// 대용량 데이터 동기화
let products = try await APIClient.fetchAllProducts()
try await DatabaseManager.shared.bulkUpsert(products)
}
}
// URLSession 백그라운드 다운로드
class BackgroundDownloadManager: NSObject {
static let shared = BackgroundDownloadManager()
private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "com.app.download")
config.isDiscretionary = false // 즉시 시작 (true면 시스템 재량)
config.sessionSendsLaunchEvents = true // 완료 시 앱 깨움
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
var completionHandler: (() -> Void)? // AppDelegate에서 받은 핸들러
var progressHandlers: [URL: (Double) -> Void] = [:]
var completionHandlers: [URL: (URL?, Error?) -> Void] = [:]
func downloadFile(url: URL,
onProgress: @escaping (Double) -> Void,
onComplete: @escaping (URL?, Error?) -> Void) {
progressHandlers[url] = onProgress
completionHandlers[url] = onComplete
let task = session.downloadTask(with: url)
task.resume()
}
}
extension BackgroundDownloadManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
guard let url = downloadTask.originalRequest?.url else { return }
// 임시 파일을 앱 디렉토리로 이동
let destURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent(url.lastPathComponent)
try? FileManager.default.moveItem(at: location, to: destURL)
DispatchQueue.main.async {
self.completionHandlers[url]?(destURL, nil)
self.completionHandlers.removeValue(forKey: url)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
guard let url = downloadTask.originalRequest?.url else { return }
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
DispatchQueue.main.async {
self.progressHandlers[url]?(progress)
}
}
// 백그라운드 세션 완료 시 호출
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
self.completionHandler?()
self.completionHandler = nil
}
}
}
// AppDelegate에서 백그라운드 세션 완료 처리
extension AppDelegate {
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
BackgroundDownloadManager.shared.completionHandler = completionHandler
}
}
// 사일런트 푸시 처리
extension AppDelegate {
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// content-available: 1 인 사일런트 푸시
guard userInfo["content-available"] as? Int == 1 else {
completionHandler(.noData)
return
}
Task {
do {
let hasNew = try await fetchNewContent()
completionHandler(hasNew ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
private func fetchNewContent() async throws -> Bool {
// 최신 데이터 확인 및 저장
let latestVersion = try await APIClient.checkVersion()
guard latestVersion > LocalCache.currentVersion else { return false }
let data = try await APIClient.fetchUpdates()
try await LocalCache.update(with: data)
return true
}
}
class ContentRefreshOperation: Operation {}
class DatabaseManager { static let shared = DatabaseManager()
func bulkUpsert(_ items: [Any]) async throws {} }
class APIClient { static func fetchAllProducts() async throws -> [Any] { return [] }
static func checkVersion() async throws -> Int { return 1 }
static func fetchUpdates() async throws -> Data { return Data() } }
class LocalCache { static var currentVersion = 0
static func update(with data: Data) async throws {} }
마무리
iOS 백그라운드 작업의 핵심은 "시스템이 언제 실행할지 모른다"는 전제다. BGAppRefreshTask는 30초 이내에 완료해야 하고, BGProcessingTask는 충전 중/Wi-Fi 환경에서 더 자주 실행된다. 대용량 다운로드는 반드시 URLSession 백그라운드 세션을 사용해야 앱 종료 후에도 완료된다. 사일런트 푸시는 과도하게 사용하면 시스템이 무시하므로 중요한 이벤트에만 사용하라.