관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. Multipart, 멀티파트 - URLSession으로 구현 (uploadTask, URLSessionUploadTask, progress 처리) 본문

iOS 응용 (swift)

[iOS - swift] 2. Multipart, 멀티파트 - URLSession으로 구현 (uploadTask, URLSessionUploadTask, progress 처리)

jake-kim 2023. 7. 1. 01:41

1. Multipart, 멀티파트 - 개념 (메모리 효율성, 네트워크 대역폭, 개별적 재전송 유리)

2. Multipart, 멀티파트 - URLSession으로 구현 (uploadTask, URLSessionUploadTask, progress 처리)

3. Multipart, 멀티파트 - BackgroundSession 사용 방법 (background에서 업로드, suspended에서 업로드, not running에서 업로드)

Multipart 방식

  • 이전 포스팅 글에서 알아보았듯이, Multipart 방식은 대용량 파일을 업로드하기에 좋은 방식
    • 파일의 크기에 영향을 받지 않는 장점이 존재
    • 메모리 효율성 - 전체 파일을 한꺼번에 메모리에 올리지 않고 쪼개어서 전송
    • 네트워크 대역폭에 효율
    • 개별적으로 재전송이 쉬움

Multipart방식 구현 전, URLSessionUplaodTask 알아보기

  • URLSessionUploadTask를 사용하여 Multipart 방식 사용
  • URLSessionUploadTask은 URLSessionTask의 서브클래스
  • 일반적인 data task와는 다르게 background에서 파일들을 업로드 할 수 있는 Tsak 방식

https://developer.apple.com/documentation/foundation/urlsessionuploadtask

cf) URLSessionDataTask란?

  • URLSessionTask의 서브클래스
  • 데이터를 가져오는 작업을 나타내는 클래스
  • 일반적으로 HTTP GET 요청을 사용하여 서버에서 데이터를 가져오는 작업에 사용
  • 데이터를 서버로부터 받아오는 작업을 수행하며, 서버 응답 데이터는 메모리에 저장

1) Multipart 구현 - 함수 선언

  • 이미지 데이터를 넘겨주고, 네트워킹을 통해 얻어온 결과값을 completion으로 전달
func uploadImageUsingURLSession(imageData: Data, completion: @escaping (Error?) -> Void) {
}

2) Multipart 구현 - url과 HTTP 방식 작성

  • URLRequest에 url, HTTP 방식 정보 입력
let url = URL(string: "https://example.com/upload")

var request = URLRequest(url: url!)
request.httpMethod = "POST"

3) Multipart 구현 - 헤더 작성

  • 이전 포스팅 글에서 알아본 대로 multipart 방식은 body안에 구분자를 넣어서 form-data들을 구분해야 하므로 boundary라는 값을 content-type (mime-type)으로 작성
  • 필요한 두 개의 정보
    • multipart/form-data
    • boundary
let uniqString = UUID().uuidString
let contentType = "multipart/form-data; boundary=\(uniqString)"
request.setValue(contentType, forHTTPHeaderField: "Content-Type")

4) Multipart 구현 - form-data 작성

Multipart 형태

  • body부분을 작성하기 위해 빈 Data() 인스턴스 준비
var body = Data()
  • multipart 방식은 body부분에 구분자를 넣어서 여러개의 data를 넣을 수 있고 특정한 형식이 있으므로 구분자를 작성
// 멀티 파트 데이터의 구분자(boundary) 설정
body.append("--\(uniqString)\r\n".data(using: .utf8)!)

// TODO...

body.append("--\(uniqString)--\r\n".data(using: .utf8)!)
  • 위 TODO 부분에 multipart 방식에 사용되는 형태인 Content-Disposition, name, filename, Contnet-Type 등을 작성
body.append("Content-Disposition: form-data; name=\"image\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!)
body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
  • 실제 이미지 데이터를 추가하고 해당 폼 데이터의 마지막이라는 구분자도 추가하면 body 형태 완료
body.append(imageData)
body.append("\r\n".data(using: .utf8)!)

(body를 채우는 전체 코드 )

/// 폼 데이터 생성
var body = Data()

/// 멀티 파트 데이터의 구분자(boundary)
body.append("--\(uniqString)\r\n".data(using: .utf8)!)

/// 멀티 파트 데이터의 파트에 대한 헤더를 추가: name 파라미터를 "image"로 설정하고, 업로드된 파일의 원래 이름을 "image.jpg"로 설정
body.append("Content-Disposition: form-data; name=\"image\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!)

/// 업로드된 파일의 MIME 타입을 명시
body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)

/// 실제 이미지 바이너리 데이터를 담고있는 값 추가
body.append(imageData)

///  각 파트 사이에 빈 줄을 추가하여 파트를 구분하며 멀티파트 데이터의 각 파트를 구분하는 구분자 역할
body.append("\r\n".data(using: .utf8)!)

/// 멀티파트 데이터의 끝을 나타내는 경계값을 추가하며 멀티파트 데이터의 마지막을 나타내고 파트들을 닫는 역할
body.append("--\(uniqString)--\r\n".data(using: .utf8)!)

5) URLSession 만들어서 네트워킹

  • 파일 업로드는 URLSession.shared와는 다른 timeout 정책을 가져가서 언제까지 업로드 할건지 타임아웃을 정하는것이 좋으므로 별도의 URLSession을 만들어서 사용
let session = URLSession(configuration: .default)
session.configuration.timeoutIntervalForRequest = TimeInterval(20)
session.configuration.timeoutIntervalForResource = TimeInterval(20)

let task = session.uploadTask(with: request, from: body) { (data, response, error) in
    DispatchQueue.main.async {
        if let error = error {
            completion(error)
            return
        }
        // 응답 처리
        
        completion(nil)
    }
}

task.resume()
  • task를 바로 resume()하기 전에 delegate를 지정하여 파일들의 업로드 progress 파악도 가능
... 

}
    
task.delegate = self // <- 추가

task.resume()

extension ViewController: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        print("progress = ", Double(bytesSent) / Double(totalBytesSent))
    }
}

보너스) 파일들 전송에 개별적인 progress가 필요한 경우 구현 방법

  • multipart를 사용하면 여러개의 파일들을 동시에 업로드할때 사용하기 때문에 이때는 각각의 이미지를 upload하도록 구현 필요
  • ImageUploader라는 별도의 클래스를 만들고 이 안에서 URLSessionTask를 배열로 관리하도록 설정
  • task를 기록해놓는 이유
    • 각 task마다 progress를 URLSessionTaskDelegate로 알 수 있기 때문에 이를 활용하기가 용이
import Foundation
import UIKit

class ImageUploader: NSObject {
    private var session: URLSession!
    private var uploadTasks: [URLSessionTask] = []
    
    func uploadImages(images: [UIImage]) {
        let configuration = URLSessionConfiguration.default
        session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        
        for image in images {
            guard let imageData = image.jpegData(compressionQuality: 0.8) else {
                print("Failed to convert image to data")
                continue
            }
            
            let url = URL(string: "https://example.com/upload")!
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            
            let task = session.uploadTask(with: request, from: imageData)
            task.delegate = self
            uploadTasks.append(task)
            
            task.resume()
        }
    }
}

extension ImageUploader: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        guard let index = uploadTasks.firstIndex(of: task) else { return }
        let progress = Double(totalBytesExpectedToSend) / Double(totalBytesSent)
        print("image \(index + 1): \(progress)")
    }
}

 

* 전체 코드: https://github.com/JK0369/ExMultipart

* 참고

https://m.boostcourse.org/web326/lecture/59008

https://developer.apple.com/documentation/foundation/urlsessionuploadtask

https://developer.apple.com/documentation/foundation/urlsessionuploadtask

Comments