스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
탄탄한 재현형 파일 전송 구축하기
URLSession이 어떻게 앱에서 대용량 파일을 전송하고 네트워크 중단에서 복구하는 것에 도움을 주는지 확인해 보세요. HTTP 파일 전송을 정지했다 재개하는 법, 재현현 업로드를 지원하는 법을 알아보고, 앱이 백그라운드에서 정지된 경우에도 URLSession을 사용해 파일을 전송하는 모범 사례를 탐색합니다.
챕터
- 0:00 - Welcome
- 2:42 - Explore the resumable downloads protocol in HTTP
- 4:23 - Pause and resume downloads with URLSession
- 7:50 - Pause and resume uploads with URLSession
- 9:45 - Explore the resumable uploads protocol in HTTP
- 12:46 - Add resumable uploads to SwiftNIO
- 16:11 - Use background URLSession
리소스
관련 비디오
WWDC19
-
다운로드
♪ ♪
안녕하세요 저는 Jonathan입니다 저는 Internet Technologies 팀 엔지니어인데요 오늘 저는 URLSession을 활용해 재현형 파일 전송 방식에 대해 알려드리려고 합니다 대용량 파일 전송은 부담되는 일이죠 아주 잠깐만 중단돼도 진행 상태가 전부 날아가 처음부터 다시 시작할 수 밖에 없을 수도 있으니까요 또한 전송 용량이 클수록 시간은 더 오래 걸리고 시간이 오래 걸릴수록 무언가 잘못될 가능성이 더 커집니다
사용자가 앱 사용을 중단하거나 Wi-Fi 범위에서 벗어나거나 제어 불가한 네트워크 문제를 경험할 수가 있습니다 이번 세션에서는 이 문제들의 해결법을 알아보고 사용자에게 탄탄한 네트워크를 제공할 수 있는 법도 알아보죠 '재현형 HTTP 프로토콜' 이건 사용자가 연결이 중단되었을 때에도 진행 상태를 유지할 수 있도록 해주는 프로토콜입니다 시간과 대역폭 낭비를 막아 대용량 데이터 전송 시 프로토콜을 강력한 툴로 만들죠 URLSession에서의 업/다운로드를 보여드릴게요 업로드 재현을 위한 새로운 API도 소개해 드리고요 이 API의 메커니즘을 이해하는 건 앱 디버깅이나 서버 지원 구축에도 도움이 될 겁니다 그래서 재개 가능 프로토콜도 살펴보고 HTTP를 통해 앱과 서버가 작동하는 법도 알아보죠
그다음 서버와 관련해서 SwiftNIO를 사용하는 서버에 업로드 재현을 지원해 볼게요
백그라운드 URLSessions가 사용자와 네트워크 중단 처리를 시스템 자원을 충분히 활용하면서 어떻게 하는지도 알아보도록 하겠습니다
URLSession에서의 업/다운로드 재현을 더 보죠 지금 최신 Xcode를 다운로드 중인데요 7GB 정도 다운로드가 진행된 상태입니다 그런데 그 때 Wi-Fi 연결이 끊겼습니다 다운로드가 정지됐죠 하지만 Wi-Fi가 다시 연결되면 정지된 지점부터 다시 다운로드할 수 있습니다
그럼 엄청난 양의 대역폭과 시간이 절약되죠 다운로드 재현은 가히 놀랍습니다 그런데 이건 도대체 어떻게 작동하는 걸까요? 먼저, 클라이언트는 GET 요청을 보냅니다
이에 대해 서버는 Accept-Ranges 헤더를 사용해 다운로드 재현 지원을 광고하죠 Accept-Ranges: bytes란 이 자원의 특정 바이트에 대해 서버가 Range 요청을 지원한다는 겁니다 서버 응답에는 ETag라는 게 포함되는데 이건 곧바로 특이하게 자원을 식별합니다 서버 콘텐츠가 변경되면 ETag도 변경되고요
다운로드가 중단되면 어떻게 될까요?
클라이언트는 다운로드 데이터 일부를 저장했기 때문에 남은 부분만 필요하게 됩니다 이를 위해 다운로드에서 놓친 바이트를 불러오려고 Range 명령을 보낼 수 있죠 이 요청은 Range 필드를 사용 중인 바이트를 나타냅니다 하지만 클라이언트도 자원이 변하지 않았음을 알아야 하죠 아니면 새로운 자원에서 저장되어 있는 기존 자원으로 데이터를 첨부해야 합니다 이를 방지하기 위해 If-Range 필드는 이전 응답에서 얻은 ETag를 포함하고 있고 ETag가 같다면 남은 데이터만 보내라고 서버에게 말하죠
만약 ETag가 같다면 서버는 206 Partial Content로 응답합니다 여기서 Content-Range 필드는 이 응답에 포함된 바이트의 범위를 의미하고 다운로드를 완료합니다
처음부터 URLSession은 Range 요청을 사용해 다운로드를 정지 및 재개하는 API를 제공했습니다 이제 업로드도 정지와 재개가 가능해졌죠 이를 통해 진행 중인 작업을 직접 멈출 수도 있고 오류 처리를 수행하여 연결 문제에서 복구하여 정지된 지점부터 전송 재현이 가능합니다 이 다운로드 작동 원리부터 함께 살펴보죠 사용자가 다운로드를 직접 정지 및 재현할 수 있는 UI를 만들고 있다고 가정해 봅시다 Safari와 같이 앱은 이 UI를 가지지만 엔진룸에 있는 URLSession을 사용해 부분 다운로드 데이터나 ETag, 요청 헤더 추적 등 모든 디테일 처리가 가능하죠 다운로드를 시작하기 위해 평소처럼 태스크를 만들고 resume을 호출하여 시작하면 됩니다 사용자가 정지 버튼을 누를 때 다운로드가 정지되려면 cancelByProducingResumeData를 호출할 수 있습니다 나중에 재현하려면 부분 다운로드에 대한 정보가 필요할 거고요 ETag나 현재 용량 디스크에서의 위치 같은 것들요 이것과 다른 메타데이터는 이 함수에서 반환된 재현 데이터 객체에 저장됩니다 이 재현 데이터가 부분 다운로드 데이터가 아님에 주목하는 것이 중요합니다 재현 데이터가 0일 경우 다운로드 재현 요건이 하나 이상 미충족이라는 거죠 이에 대해서는 곧 살펴보겠습니다 반면 재현 데이터가 0이 아닐 경우 나중에 사용하기 위해 저장해 놓아야 합니다 그래서 다운로드 재현을 위해서는 사용자가 재개 버튼을 누를 때와 같이 이 저장된 데이터를 downloadTask withResumeData 메서드로 통과시키죠 정말 간편하죠? 이 패턴은 직접 다운로드를 정지할 때는 아주 좋습니다 URLSession은 예상치 못한 연결 중단 복구도 가능하고요
다운로드 태스크가 네트워크 문제로 실패하면 오류 자체에서 데이터 재현을 점검할 수 있죠 다운로드가 재현될 수 있다면 오류의 userInfo 딕셔너리는 그 데이터를 포함할 겁니다 URLError의 downloadTaskResumeData를 사용해 이 데이터에 아주 편리하게 접근할 수가 있고요 URLSession에는 다운로드 재현 요건이 몇 가지 있습니다 다운로드는 기본적으로 데이터를 가져오고 반복에 안전해야 하죠 그래서 이 태스크가 HTTP GET 요청을 하도록 하죠 다른 계획이나 메서드는 지원되지 않고요 다음으로 서버는 byte-range 요청을 지원하고 Accept-Ranges 헤더를 사용해 이를 광고해야 합니다 서버는 응답 시 자원으로 ETag나 Last-Modified 필드를 제공해야 하는데 여기서는 전자가 더 선호됩니다 마지막으로 임시 다운로드 파일은 디스크 공간 압박에 응답해 시스템에 의해 지워져서는 안 됩니다
이 요건만 충족되면 직접 다운로드를 정지 및 재개할 수 있습니다 연결 중단 상태에서 복구도 가능하고요 재현 프로토콜이 없다면 아주 잠깐 중단되더라도 데이터 전송을 맨 처음부터 시작할 수 밖에 없을 수도 있습니다 이건 업로드에 있어서는 훨씬 더 큰 문제죠 업로드는 다운로드보다 보통 훨씬 더 느립니다 그래서 재시작은 훨씬 더 많은 시간과 자원의 손실을 뜻하죠 iOS 17에서는 새로운 재현 업로드 태스크 지원이 도입됐고 저는 이 지원 내용들이 정말 맘에 듭니다 이제 업로드 태스크는 서버가 최신 프로토콜 규격을 지원하기만 한다면 자동으로 재현 가능해졌거든요 새로운 API부터 먼저 탐색해 보고 재현형 업로드 프로토콜을 자세하게 살펴보도록 하죠 다운로드 태스크와 같이 resume을 호출해 업로드 태스크를 만들고 시작할게요 업로드 태스크는 이제 정지를 위해 동일한 cancelByProducingResumeData 메서드를 다운로드 태스크로서 지원합니다 이 태스크는 서버가 최신 재현형 업로드 프로토콜이 지원되는지 바로 감지하죠 해당 서버에서 지원하는 경우 추후 사용을 위해 재현 데이터를 저장합니다
마지막으로 정지된 업로드를 재개하기 위해서는 uploadTask withResumeData라는 새로운 메서드를 사용하세요 다운로드 태스크와 비슷한 패턴임을 아실 겁니다 이미 앱에서 다운로드 정지 및 재개에 있어 멋진 경험을 선사해 보았다면 업로드에 대해서도 똑같이 쉽게 선사할 수 있다는 거죠 순간적인 네트워크 중단이 발생하더라도 서버에는 여전히 도달 가능하고 URLSession은 자동으로 업로드 재현을 시도할 겁니다 추가 코드가 필요하지 않죠 하지만 네트워크나 서버가 완전히 다운된 것처럼 더 광범위한 연결 문제가 발생한 경우에는 다운로드 태스크처럼 재현 데이터에 대한 오류 점검이 가능합니다 URLSession에서도 재현형 업로드를 보게 되다니 여러분도 저처럼 잘 활용해 보시길 바랍니다 하지만 이 특징을 이용할 수 있으려면 최신 업로드 프로토콜을 서버에서 지원해야 합니다
이 프로토콜은 현재 개발 중이며 IETF 내 표준화를 위한 노력들이 활발히 진행 중입니다 해당 프로토콜에서 클라이언트는 서버 지원을 자동 발견하는데요 이건 URLSession이 최초 요청에서부터 모든 업로드 재현을 시도할 수도 있다는 겁니다 서버가 업로드 재현을 지원하지 않는다면 요청은 정규 업로드로 계속될 겁니다 작동 원리를 살펴보죠
클라이언트는 업로드 종점에 요청을 하나 보냅니다 Upload-Incomplete 필드는 이 클라이언트가 업로드 재현을 지원한다는 걸 의미합니다 ?0은 구조화 필드 불리언으로 알려져 있는데요 그 값이 거짓임을 나타내죠 이건 업로드 데이터 전체가 요청 본문에 포함됐다는 거고요
서버가 업로드 재현을 지원하는 경우 클라이언트의 헤더를 감지하고 104 정보성 응답을 사용해 지원 내용을 광고합니다 104 응답에는 재현 URL과 로케이션 필드가 포함되죠 재현 URL은 특히 업로드 식별에 사용되기에 클라이언트는 연결이 중단되면 어디서 재현해야 할지를 압니다 서버는 수신한 업로드 데이터를 이 URL과 연결시키고요
업로드가 중단 없이 끝난다면 아주 좋겠죠 서버가 201을 송신하고 끝납니다 하지만 업로드가 중단될 경우 클라이언트와 서버는 업로드 재현을 수행합니다
서버는 재현 URL에 대해 업로드 내용 일부를 저장하지만 클라이언트는 서버가 가진 실제 데이터 양을 알아야 하죠 그래서 클라이언트는 URL에 HEAD 요청을 송신합니다 서버에 업로드 오프셋을 요청하는 거죠 이 오프셋은 서버가 수신한 실제 바이트 갯수입니다
그럼 서버는 클라이언트의 특정 업로드 오프셋으로 응답하죠
마지막으로 클라이언트가 해당 오프셋을 인정하고 남은 데이터를 송신해야 합니다 이를 위해 클라이언트는 재현 URL에 매칭 업로드 오프셋과 함께 PATCH 요청을 보내죠 이 요청의 본문에는 주어진 오프셋에서 시작된 업로드 데이터가 포함됩니다
이에 클라이언트는 모든 데이터를 서버로 보내고 업로드를 완료하죠 이 모든 게 URLSession에서 무료로 가능합니다 이제 서버 측을 간략하게 살펴보고 SwiftNIO를 통한 재현형 업로드 서버 구축 방식을 알아봅시다
이미 서버에서 SwiftNIO를 사용하고 계신 분들이 집중하시면 딱 좋을 내용입니다 업로드 재현은 어느 서버에서나 구현할 수 있지만 이미 SwiftNIO를 사용 중인 서버라면 새로운 패키지를 통해 훨씬 쉽게 추가할 수 있습니다 간략한 예시를 보죠 SwiftNIO는 앱과 서버에서 작동하는 비동기성 네트워크 앱 프레임워크입니다 이 샘플 코드에서는 HTTP/2를 서버로 설정하고 있죠 우리는 서버에 처리기 두 개를 추가했습니다 이 코덱은 HTTP/2 프레임을 우리 예시 처리기가 이해할 수 있는 요청으로 번역하죠 또 다르게는 예시 처리기에서 응답을 취해 HTTP/2 프레임에 코딩하고요 ExampleChannelHandler는 서버에 대해 기본 라우팅과 논리를 수행합니다 처음에는 정규 업로드만 지원하는데요 서버에서 업로드 지원을 추가하는 건 쉽습니다
먼저 NIOResumableUpload 프로젝트를 다운로드해서 종속성으로 추가해 코드에 들여옵니다 그다음 업로드 재현 콘텍스트를 정의하죠 이건 처리기에게 재현 URL을 생성할 때 어떤 업로드 종점을 사용할 건지 말해 줍니다
마지막으로 HTTPResumableUploadHandler에서 현재 처리기를 래핑하죠 이건 현재 논리 최상위에서 업로드 재현 절차를 수행합니다 각 업로드마다 안전한 무작위 재현 URL을 생성하여 업로드 데이터와 연결합니다 처리기는 연결이 중단될 경우 부분 데이터를 보관하고 모든 업로드 재현 절차에 응답합니다 놀랍죠! 단 코드 몇 줄만으로 서버에 변화를 주어 업로드 재현 지원을 이끌어 낼 수 있다니 서버에서 Swift를 사용 중이라면 시도해 보세요! 어쨌든 여러분 모두 설명에 있는 링크에서 오픈 소스 샘플 코드를 확인해 보시기 바랍니다 샘플 코드 또한 새로운 HTTP 타입을 사용해 Server 프로젝트에서 앱과 Swift 전반에 걸쳐 동일한 타입을 사용합니다 이 데이터 타입은 SwiftNIO와 함께 오픈 소스 패키지로 배포됐죠 Swift 블로그에서 확인 후 피드백을 제공해 주세요
재현형 업로드 프로토콜이 104 상태 코드를 사용하는 정보성 응답을 사용한다는 걸 알게 되셨을 겁니다 새 HTTP 타입을 통해 서버 측 응답 지원이 쉬워지죠 URLSession은 앱에서 업로드 재현을 위한 104 응답을 자동 처리하고요 이외에도 URLSession은 새 delegate 메서드 didReceiveInformationalResponse도 제공합니다 이를 통해 102 Processing이나 103 Early Hints 같은 즉각적인 응답들도 처리할 수가 있고요
재현형 프로토콜은 네트워크 중단을 완화하고 대역폭을 절약할 수 있는 최고의 방법입니다 Background URLSession도 대용량 파일 전송에 유용한데요 스키를 타며 찍은 4K 영상을 업로드한다면 연결이 중단되더라도 가능하면 업로드 재현을 보장하고 싶을 겁니다 여기서 모든 오류를 스스로 처리할 수 있습니다 아니면 백그라운드 세션이 하도록 냅둘 수 있죠
실제로 백그라운드 세션은 재현을 자동 처리합니다 서버가 지원한다면 업로드, 다운로드 모두요
시스템은 태스크가 중단되면 점점 늘어나는 간격을 두고 태스크 재현을 시도할 겁니다
태스크가 재현될 수 없다면 시스템은 자동으로 태스크를 처음부터 재시도할 거고요
스키를 타면서 셀 커버리지를 잃거나 폭풍우가 Wi-Fi 연결을 끊을 수 있는데요 백그라운드 세션은 항상 연결을 기다립니다 기기가 다시 인터넷에 연결된 어느 시점에 예정대로 진행되도록 말이죠
사용자가 영상을 업로드 중일 때 앱을 그대로 두거나 기기를 치워둘 수 있는데요 아마 스키를 탈 준비를 하면서 여전히 업로드가 계속될 거라고 예상할 겁니다
여기서 백그라운드 세션이 더욱 필요해집니다 백그라운드 태스크 스케줄은 시스템이 정하는데요 그래서 앱의 프로세스 밖에서 실행됩니다 즉, 앱이 시스템에 의해 중단되거나 종료되더라도 네트워크 태스크는 믿음직하게 계속된다는 겁니다 오랜 시간이 걸릴 수 있고 사용자가 그대로 둬도 지속되는 대용량 파일 전송의 경우에는 백그라운드 세션을 사용하세요
마지막으로 사용자가 앱에서 최고의 경험을 할 수 있으려면 덜 급한 태스크 스케줄이 더 나중에 있어야 합니다 백그라운드 세션에서는 사용자를 위해 네트워크 활동 일정을 효율적으로 설정하고 자원을 절약할 방법이 많습니다
바로 발생할 필요가 없는 태스크의 경우 백그라운드 설정에서 isDiscretionary 프로퍼티가 참이 되도록 설정해 보세요 시스템은 Wi-Fi 연결 기기 전력 연결 네트워크 제한 여부 등 다양한 요인을 고려해 똑똑하게 태스크 일정을 정할 겁니다 이건 추후 사용을 위해 에셋을 다운로드할 때나 매일 하는 백업 자료 업로드와 데이터 분석에 특히 좋습니다
Low Data Mode에서 과한 대역폭 사용 방지를 위해 allowsConstrainedNetworkAccess 프로퍼티 값을 거짓으로 설정해 주세요 아래 세션을 확인하시면 Low Data Mode 지원에 관해 더 많은 팁을 얻을 수 있고요
사용자가 시스템 자원을 덜 사용하는 경우라면 백그라운드 태스크 일정도 나중에 시작하게 할 수 있죠 늦은 밤이 대용량 백업과 같은 태스크 일정에 딱 좋습니다
시스템 스케줄링을 추가 지원하기 위해서는 countOfBytesClientExpectsToSend와 Receive 프로퍼티를 설정할 수 있고요 이 프로퍼티들을 사용해 시스템이 자원을 가장 잘 할당하고 그런 혜택을 사용자에게 넘기도록 할 수 있습니다 백그라운드 세션은 즉시 발생하지 않아도 되는 대용량 파일 전송이나 앱이 중단돼도 계속되어야 하는 파일 전송에 딱 좋은 툴입니다 더 작은 태스크나 바로 발생해야 하는 태스크에는 표준 URLSession을 사용할 수 있죠 재현의 힘을 앱에 가져와서 사용자들이 신뢰할 수 있는 네트워크를 만들어 보세요 SwiftNIO와 HTTP 타입을 확인해 보시고 Swift에서 최고의 HTTP 경험을 만들어 봅시다 대용량 또는 재량 파일 전송엔 백그라운드 세션을 사용하시고요 가장 필요할 때를 위해 자원을 남겨둘 수 있을 겁니다 시청해 주셔서 감사합니다 이와 관련된 다른 세션들도 확인해 보시고요 이만 마치겠습니다! ♪ ♪
-
-
4:53 - Pausing and resuming a URLSessionDownloadTask
let downloadTask = session.downloadTask(with: request) downloadTask.resume()
-
5:21 - Pausing and resuming a URLSessionDownloadTask
let downloadTask = session.downloadTask(with: request) downloadTask.resume() guard let resumeData = await downloadTask.cancelByProducingResumeData() else { // Download cannot be resumed return }
-
6:11 - Pausing and resuming a URLSessionDownloadTask
let downloadTask = session.downloadTask(with: request) downloadTask.resume() guard let resumeData = await downloadTask.cancelByProducingResumeData() else { // Download cannot be resumed return } let newDownloadTask = session.downloadTask(withResumeData: resumeData) newDownloadTask.resume()
-
6:34 - Retrieving resume data on error
do { let (url, response) = try await session.download(for: request) } catch let error as URLError { guard let resumeData = error.downloadTaskResumeData else { // Download cannot be resumed return } }
-
8:29 - Pausing and resuming a URLSessionUploadTask
let uploadTask = session.uploadTask(with: request, fromFile: fileURL) uploadTask.resume()
-
8:37 - Pausing and resuming a URLSessionUploadTask
let uploadTask = session.uploadTask(with: request, fromFile: fileURL) uploadTask.resume() guard let resumeData = await uploadTask.cancelByProducingResumeData() else { // Upload cannot be resumed return }
-
8:57 - Pausing and resuming a URLSessionUploadTask
let uploadTask = session.uploadTask(with: request, fromFile: fileURL) uploadTask.resume() guard let resumeData = await uploadTask.cancelByProducingResumeData() else { // Upload cannot be resumed return } let newUploadTask = session.uploadTask(withResumeData: resumeData) newUploadTask.resume()
-
9:22 - Retrieving resume data on error
do { let (data, response) = try await session.upload(for: request, fromFile: fileURL) } catch let error as URLError { guard let resumeData = error.uploadTaskResumeData else { // Upload cannot be resumed return } }
-
13:15 - Before resumable uploads in Swift NIO
NIOTSListenerBootstrap(group: NIOTSEventLoopGroup()) .childChannelInitializer { channel in channel.configureHTTP2Pipeline(mode: .server) { channel in channel.pipeline.addHandlers([ HTTP2FramePayloadToHTTPServerCodec(), ExampleChannelHandler() ]) }.map { _ in () } } .tlsOptions(tlsOptions)
-
14:06 - Add resumable uploads in Swift NIO
import NIOResumableUpload let uploadContext = HTTPResumableUploadContext(origin: "https://example.com") NIOTSListenerBootstrap(group: NIOTSEventLoopGroup()) .childChannelInitializer { channel in channel.configureHTTP2Pipeline(mode: .server) { channel in channel.pipeline.addHandlers([ HTTP2FramePayloadToHTTPServerCodec(), HTTPResumableUploadHandler(context: uploadContext, handlers: [ ExampleChannelHandler() ]) ]) }.map { _ in () } } .tlsOptions(tlsOptions)
-
15:48 - Informational responses in URLSession
protocol URLSessionTaskDelegate : URLSessionDelegate { optional func urlSession(_ session: URLSession, task: URLSessionTask, didReceiveInformationalResponse response: HTTPURLResponse) }
-
18:19 - Using background URLSession
// Configuring your background session let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app") configuration.isDiscretionary = true configuration.allowsConstrainedNetworkAccess = false let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) // Configuring your background task let backgroundTask = session.uploadTask(with: url, fromFile: fileURL) backgroundTask.earliestBeginDate = .now.addingTimeInterval(60 * 60) backgroundTask.countOfBytesClientExpectsToSend = 500 * 1024 backgroundTask.countOfBytesClientExpectsToReceive = 200
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.