[iOS] Background Mode, Background Task

Hyunndy·2023년 2월 21일
3

iOS-App-Structure

목록 보기
8/8

🐸

오늘은 BackgroundMode와 BackgroundTask에 대해 정리해보겠습니다.


배경

iOS앱은 기본적으로 포그라운드, 즉 사용자가 앱을 열어 활성화한 경우에만 작동합니다.
하지만 사용자가 홈으로 나가거나 App Switcher로 앱을 전환하면 앱은 백그라운드 상태로 실행됩니다.

iOS는 퍼포먼스의 이슈로 백그라운드에서 실행되는 앱의 시간을 제한합니다.
이 제한된 시간안에 작업을 수행하려면 BackgroundTask API를 사용하여 시간 제한을 늘릴 수 있습니다.
예를 들어 파일 다운로드, 데이터 동기화, 알림 처리, 위치 추적, 오디오 재생 등을 백그라운드에서 처리할 수 있습니다.
(그래도 제한 시간이 지나면 강제로 종료 시킵니다.)

그리고! BackgroundTask를 구현하려면 XCode에서 Background Mode도 설정해주어야 합니다.
Background Mode는 iOS 앱이 백그라운드에서 실행 가능한 기능을 설정하는 옵션입니다.

정리하자면, Background Mode로 부터 승인된 기능을 Background Task API로 구현하는거네요~


Background Mode

백그라운드 모드는 iOS앱이 백그라운드에서 실행 가능한 기능을 설정하는 옵션 입니다.

XCode -> 타겟 -> Capabilities -> Background Mode
에서 설정할 수 있습니다.

많은 설정 값들이 있는데,
이 글에서는 BackgroundTask와 관련되어 보이는

  • Background fetch
  • Remote Notifications
  • Background Processing
    을 보겠습니다!

Background Task

백그라운드에서 실행되는 Task를 스케쥴링하기 위해, Background Mode를 설정하고 Task를 BGTaskScheduler 객체에 등록해야 합니다.

종류

Background Task엔 2가지 유형이 있습니다.
1. BGAppRefeshTask

  • 가벼운 작업. 단순 API 호출 및 저장, 사용자가 기기를 사용하는 시간에도 실행이 가능합니다.

2.BGProcessingTask

  • DB등 크고 무거운 작업. 옵션 중 추가 배터리를 사용하는가, 네트워크를 사용하는가 등의 요소도 지정해줄 수 있습니다. 무거운 작업이기 때문에 보통 앱이 충전 중이고, 아무것도 안할 때 실행시켜줍니다.


Background Fetch는 BGAppRefresh Task를 사용합니다.
Background Processing은 BGProcessing Task를 사용합니다.

기본 설정

  1. Background Mode를 설정한다.
  2. Background에 실행시킬 Task를 Info.plist의Permitted background task scheduler identifiers 에 추가한다! (BGTaskSchedulerPermittedIdentifiers 배열인)

Info.plist


여기에 Task의 공인된 식별자 String을 넣어준다.

👩‍💻 iOS13부터 여기다 key를 추가하면 application(_:performFetchWithCompletionHandler:) and setMinimumBackgroundFetchInterval(_:) 이 함수가 안불린다고 합니다.

실제 구현

Background Task의 실제 동작은 1. 등록 2. 스케줄링 3. 실행 4. 완료 입니다.

1. 등록

각 Task는 launch Handler를 갖는 BGTaskScheduler 객체, 고유한 식별자를 갖습니다.
모든 Task를 app launch sequence가 끝나기 전에 BGTaskScheduler에 등록 해야 합니다.

👩‍💻 그럼 launch 가 끝나는 시점인 appDelegate의 didFinishLaunchingWithOptions 함수에서 호출하면 되겠네요.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        registerBackgroundTasks()
        
        return true
    }
    /// 앱의 launch sequence가 끝나기 전에 Background Task를 Scheduler에 "등록"해야 합니다.
    /// Info.plist에 등록한 키 값으로 등록해야 합니다.
    private func registerBackgroundTasks() {
        
        print("Background Task 등록!")
        // RefreshTask
        // 1. Refresh Task 등록
        let taskIdentifier = ["com.example.apple-samplecode.ColorFeed.refresh"]
        BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier[0], using: nil, launchHandler: { task in
        
            // 2. 실제로 수행할 Background 동작 구현
            self.handleBackgroundTask(task: task as! BGAppRefreshTask)
        })
    }

예시에서는 RefreshTask를 등록합니다.
등록 할 때 정의하는 클로저에서는 Task가 시작되면 실제 수행 할 함수를 구현합니다.

2. 스케줄링

앱이 Background 상태에 들어갈 때 BGTaskScheduler에 Task를 submit 해줍니다.
이 때, TaskRequest를 만들어서 submit할 수 있는데

  • (processingTask)의 경우 배터리 사용 여부
  • (processingTask)의 경우 네트워크를 사용할 것인지 여부
  • 백그라운드 진입 하고 Task를 실행할 때 까지의 최소 대기 시간

을 설정할 수 있습니다.

Background 상태에 돌입할 때이니 AppDelegate와 SceneDelegate에 알맞은 함수에 넣어줘야겠죠?

    func applicationDidEnterBackground(_ application: UIApplication) {
        
        scheduleBackgroundTask()
        print(#function)
    }
    private func scheduleBackgroundTask() {
        let task = BGAppRefreshTaskRequest(identifier: "com.example.apple-samplecode.ColorFeed.refresh")
        /// (Processing Task 였다면)
        /*
         task.requiresExternalPower = false // 배터리를 사용할 것인지 여부
        task.requiresNetworkConnectivity = false // 네트워크를 사용할 것인지 여부
         */
        
        // 백그라운드 작업을 실행할 때까지의 최소 대기 시간
        task.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        
        do {
            print("Background Task submit!")
            // Background Task 등록!!
            try BGTaskScheduler.shared.submit(task)
        } catch {
            print("Could not schedule app refesh")
        }
    }

BGTaskScheduler에 submit하면 Task가 스케줄러에 등록되고 시스템이 판단했을 때 earliestBeginDate가 지나지 않은 적절한 시기에 실행됩니다. (안..할지도?)

실행 & 완료

BackgroundTask를 실제 구현하는 곳에서는 2가지만 기억합니다.

  • task.expirationHandler 구현
  • task.setTaskCompleted를 호출할 것
    expirationHandler는 Background Task가 갑자기 종료 되거나 TimeOut 될 때를 대비해서 정의하는 핸들러 입니다.
    setTaskCompletled()는 Task가 완료되었음을 알려줍니다. 꼭 해줘야겠죠 우린 제한된 백그라운드의 자원을 이용해 굳이 억지로 이 함수를 실행 시키고 있는거니까요.

👩‍💻 예시1) 파일 다운로드

    private func handleBackgroundTask(task: BGAppRefreshTask) {
        
        task.expirationHandler = {
            task.setTaskCompleted(success: false)
        }
        
        let config = URLSessionConfiguration.background(withIdentifier: "com.example.apple-samplecode.ColorFeed.refresh")
        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
        let downloadTask = session.downloadTask(with:URL(string: "https://example.com/my-file.zip")!)
        downloadTask.resume()
    }

BackgroundTask에서 URlSession을 이용해서 파일을 다운로드 한다면, urlSessionDelegate를 이용해서 완료되거나/에러 등으로 인해 종료될 때 task를 명시적으로 Complete 시켜줘야 합니다.

    private func endBackgroundTask() {
        if let backgroundTask = self.backgroundTask {
            backgroundTask.setTaskCompleted(success: true)
            self.backgroundTask = nil
        }
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        
        // 파일 다운로드 완료
        print("Background Task 완료!")
        
        // BackgroundTask 종료
        endBackgroundTask()
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error {
            // 오류 처리..
            print("Background Task 에러!")
            print(error)
        }
        
        // BackgroundTask 종료
        endBackgroundTask()
    }

이런식으로요!

👩‍💻 예시2) async/await 사용

    private func handleBackgroundTask(task: BGAppRefreshTask) {
        
        task.expirationHandler = {
            task.setTaskCompleted(success: false)
        }
        
        Task {
            do {
                let url = URL(string: "https://baconipsum.com/api/?type=all-meat&paras=1&start-with-lorem=1")!
                let request = URLRequest(url: url)
                async let (data, response) = URLSession.shared.data(for: request)
                guard try await (response as? HTTPURLResponse)?.statusCode == 200 else {
                    throw HyunndyError.badNetwork
                }
                
                let paragraph = try JSONDecoder().decode([String].self, from: try await data)
                print("BackgroundTask 성공!! \n\(paragraph[0])")
                task.setTaskCompleted(success: true)
            } catch {
                task.setTaskCompleted(success: false)
            }

        }
    }

한 곳에서 관리하니 확실히 편하네요.

테스트

이 블로그를 참고해주세요!

주의 사항

  • 배터리 수명이 단축될 수 있으니 Background Task에서는 리소스 사용을 최소화 할 것
  • 최대 실행 시간이 정해져있고 지키지 않으면 크래쉬 나니 조심할 것
  • Task가 끝나면 즉시 메모리 해제할 것
  • UI 조작 당연히 안됨 (메인스레드 아니니깐)

Remote Notifications

위에 Background Task 관련 Background Mode가 Remote Notifications가 있다고 했는데요.
iOS 13 이상에서는 UNUserNotificationCenterDelegate 프로토콜을 통해 원격 알림을 처리할 수 있으나, iOS 12 이하 버전에서는 Background Task를 사용해야 한다고 합니다.


마무리

Background Task를 아주 간소하게 알아보았습니다!
Advanced in Background Task
요게 심화 과정 WWDC 이네요.

요건 천천히 보는걸로 하고~
Background Task를 학습해보니 티맵이나 이런 네비게이션 어플의 Background Task 구현이 궁금해지네요.

profile
https://hyunndyblog.tistory.com/163 티스토리에서 이사 중

0개의 댓글