Search

[WWDC] Efficiency awaits: Background tasks in SwiftUI

들어가며

이번 세션에서 알아볼 것은 새 SwiftUI API 입니다.
모든 Apple 플랫폼에서 Swift Concurrency를 이용해 백그라운드 작업을 일관적으로 처리합니다.
API의 백그라운드 작업 처리가 Swift Concurrency로 얼마나 쉬워졌는지 살펴봅시다!
새로운 API는 watchOS, iOS, tvOS, Mac Catalyst, 위젯 및 Mac에서 실행하는 iOS 앱까지 모두 공유합니다. 즉 백그라운드 작업을 처리할 때 한 플랫폼에서 배운 개념과 패턴을 다른 곳에도 적용할 수 있다는 뜻이죠.
Swift Concurrency를 활용하는 새 API는 깊이 중첩된 completion Handler와 콜백 및 종종 부작용을 일으켰던 가변 상태의 필요성을 줄일 수 있습니다.
Swift Concurrency의 네이티브 작업 취소는 앱이 시간에 맞춰 작업을 완료하도록 도움으로써 시스템이 백그라운드에서 앱을 종료하는 것을 피합니다.

‘Stormy’

폭풍우 치는 날 하늘 사진을 찍는 앱으로 백그라운드 작업을 이용합니다.
폭풍우가 치면 하늘 사진을 찍으라고 알려 주는 앱
도표를 보면서 폭풍우 치는 날에만 백그라운드 작업을 사용하여 알림을 전송하는 법을 자세히 알아보겠습니다.
왼쪽에 보이는 막대: foreground 앱의 실행 시간
중간에 있는 막대: background 앱의 실행 시간
오른쪽에 있는 막대: 시스템 앱 실행 시간
앱에서 나가서 앱이 일시 정지되면 시스템은 예약한 시간에 백그라운드에서 앱을 깨워야 한다는 것을 기억합니다.
정오에 앱을 예약했으므로 시스템은 정오에 백그라운드에서 앱을 깨우고 백그라운드 앱 새로고침 작업을 보냅니다
백그라운드 실행 시간에 우리는 폭풍우가 치는지 알아내야 합니다. 날이 흐리다면 사용자에게 알림을 보냅니다. 우선 날씨 서비스에 네트워크 요청을 보내서 현재 날씨를 확인해야 합니다.
백그라운드에 예약된 URLSession으로 앱은 일시 정지 상태로도 네트워크 요청이 완료될 때까지 기다릴 수 있습니다.
백그라운드 네트워크 요청으로 날씨 데이터를 받으면 새로운 URLSession 백그라운드 작업으로 앱에 백그라운드 실행 시간이 다시 주어집니다.
요청한 날씨 데이터를 입수하면 앱은 밖에 폭풍우가 치는지 판단하고 사진을 찍으라는 알림을 보낼 것인지 결정합니다.

Background on Background Task

단일 백그라운드 작업의 작동 방식을 더 자세히 알아보겠습니다.
단일 앱을 새로고침하는 백그라운드 작업의 수명 주기
먼저 시스템이 앱을 깨우고 앱 새로고침 백그라운드 작업을 보냅니다.
백그라운드 상태에서 네트워크 요청을 보내 폭풍우가 치는지 확인합니다.
이상적인 상황은 우리 네트워크 요청이 새로고침을 위해 앱에 할당된 실행 시간 안에 모두 완료되는 것입니다.
알림이 게시되면 앱 새로고침 중에 필요한 건 모두 마쳤으므로 시스템이 다시 앱을 일시 정지시킵니다.

날씨 데이터에 대한 네트워크 요청이 제때 안 끝나면 어떻게 될까요?

앱이 작업을 수행할 백그라운드 실행 시간을 모두 소비했다면 시스템이 앱에 시간이 부족하다고 알려서 상황에 대비할 기회를 줍니다.
실행 시간이 만료될 때까지 앱이 백그라운드 작업을 완료하지 못하면 앱은 시스템에 의해 중단되고 다음 백그라운드 작업을 위해 스로틀링을 일으킵니다.
이 경우에는 네트워크 요청이 백그라운드 네트워크 요청인지 확인해야 합니다.
그렇다면 앱 새로고침 작업을 즉시 완료하고 네트워크 요청이 들어오면 추가 백그라운드 실행 시간을 위해서 앱을 깨울 수 있습니다.
백그라운드 URLSession이 예약되면 다시 시스템은 앱을 일시 정지할 수 있습니다

기본 앱

다음 날 정오에 앱의 백그라운드 새로 고침을 예약하는 함수를 작성
1.
먼저 내일 정오를 나타내는 날짜를 만듦
2.
백그라운드에서 앱을 새로고침하는 요청을 만듦
3.
가장 빠른 시간을 다음 날 정오로 설정하고 스케줄러에 제출
 시스템은 이것을 받고 다음 날 정오에 앱을 깨우게 되죠.
예약한 백그라운드 작업에 해당 핸들러를 등록하려면 새 백그라운드 작업 Scene 수정자를 이용해야 합니다.
앱이 백그라운드 작업을 받으면 백그라운드 작업과 일치하는 수정자에 등록된 모든 블록이 실행됩니다. 이 경우에는 appRefresh를 사용했습니다.
원하는 날짜에 백그라운드의 실행 시간을 미리 예약해서 앱에 제공할 수 있죠 백그라운드 작업 수정자에서 요청 및 핸들러에 같은 식별자를 사용하면 앱이 해당 작업을 받을 때 시스템이 호출할 핸들러를 식별할 수 있습니다.
내일로 다시 예약이 됐는지 확실히 확인하기 위해 방금 작성한 scheduleAppRefresh 함수로 백그라운드 실행 시간을 내일 정오로 다시 예약해서 백그라운드 작업을 시작하려 합니다. 정오의 백그라운드 실행 시간이 순환하므로 바깥 날씨를 확인하는 네트워크 요청을 만들고 Swift의 await 키워드로 결과를 기다립니다.
클로저의 본문이 반환되면 시스템이 앱에 할당한 기반 백그라운드 작업이 묵시적으로 완료된 것으로 표시됩니다.
여기에서 Swift Concurrency를 사용하면 작업이 완료됐을 때 명시적인 콜백 없이 백그라운드에서 장기 실행 작업을 수행할 수 있습니다.
Apple 플랫폼 전반에서 알림 추가를 비롯한 대다수 API는 이미 비동기 작업에서 Swift Concurrency를 지원합니다.
notifyForPhoto() async 함수를 활용하면 UserNotificationCenter에서 찾을 수 있는 비동기 알림 추가 메소드로 간단하게 수행할 수 있습니다.

Swift concurrency

Swift Concurrency의 asyncawait가 백그라운드 작업 처리에서 우리에게 얼마나 편리한지 알아보겠습니다.
그동안 참조한 비동기 isStormy 함수를 작성해 보죠!
async 함수는 바깥 날씨를 확인하는 네트워크 요청을 해야 합니다.
가장 먼저 공유 URLSession을 쓰고 날씨 데이터 요청을 인스턴스화합니다.
Swift Concurrency를 채택한 URLSession은 비동기 컨텍스트로 대기할 수 있는 네트워크에서 데이터를 다운로드하는 메소드를 갖췄습니다.
네트워크 응답이 들어오면 날씨 데이터를 읽고 결과를 반환합니다.
그러나 실행 시간이 만료되기 전에 앱이 네트워크 요청을 완료하지 못하면 어떻게 될까요?
이 경우에는 URLSession을 백그라운드 세션으로 설정했는지 확인하고 URLSession 세션 백그라운드 작업을 이용하여 앱에 시작 이벤트를 보내는지 살펴봐야 합니다.
Shared URLSession을 사용했습니다.
URLSession을 만들 때 백그라운드 설정에서 sessionSendsLaunchEvents 속성을 true로 놓아야 합니다.
이 코드는 앱이 일시 중지된 경우에도 네트워크 요청을 실행하고 요청이 완료되면 앱을 깨워서 URLSession 백그라운드 작업을 수행하라고 시스템에 알립니다.
이것은 watchOS에서 특히 중요합니다.
watchOS 백그라운드에서 실행되는 앱의 네트워크 요청은 백그라운드 URLSessions으로 요청돼야 합니다.
백그라운드 작업의 실행 시간이 만료되면 백그라운드 작업 수정자에 제공된 클로저를 실행하는 비동기 작업을 시스템에서 취소합니다.
 이것은 백그라운드 실행 시간이 만료될 때 여기서 이뤄진 네트워크 요청도 취소된다는 뜻입니다
이 취소에 응답하고 처리하려면 Swift Concurrency에 내장된 함수인 withTaskCancellationHandler를 사용하세요!
결과를 직접 대기하기보다는 withTaskCancellationHandler 호출에 다운로드를 넣고 그 결과도 함께 대기하세요.
withTaskCancellationHandler에 전달된 첫 번째 블록 - 우리가 실행하려고 대기하는 비동기 프로시저
두 번째 후행 클로저 - 작업이 취소될 때 실행되는 코드
여기에서 즉각적인 네트워크 요청이 실행 시간 만료로 취소되면 백그라운드 다운로드 작업으로 네트워크 요청을 승격하여 재개를 호출합니다.
앱이 일시 정지된 상태에서도 백그라운드 다운로드를 계속 진행
 이 코드는 양쪽 모두 같은 URLSession을 사용해서 기반 네트워크 요청을 두 번 보내지 않으며 URLSession은 진행 중인 모든 중복 요청을 제거합니다.
우리가 조금 전에 만든 백그라운드 URLSession과 동일한 식별자를 사용해서 특정 URLSession이 백그라운드 작업을 생성할 때만 이 블록이 호출되도록 설정할 수 있습니다.