POST Request를 보낼 때 body의 data를 인코딩하는 방식이다.
multi-part의 의미는 data가 다수의 부분으로 나뉘어져 서버로 업로드된다는 것이다.
그래서 보통 용량이 큰 파일을 업로드할때 사용한다.
일반적으로 json 데이터를 보낼때는 Content-Type이 application/json인데, multipart/form-data도 네트워크 요청 방식은 똑같다.
URLRequest에서 url과 method를 명시해준 후,
header, parameter, 그리고 body를 넣어준다.
그리고 나서 urlSession의 dataTask 메서드에 앞서 만든 request를 넣어주면 response가 클로저에 담겨서 돌아온다.
다른 점은 body를 구성하기가 정말로 까다롭다는 것이다.
자 그럼 이제 어떤식으로 구성되어있는지 살펴보자
Content-Type과 Boundary에 대한 정보가 있음
Content-Type: multipart/form-data; boundary=3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Type header에 Boundary가 필요한 이유는 서버로 데이터를 보낼 때 데이터가 한번에 가는게 아니라 여러 부분으로 나뉘어져서 보내진다고 했다. 따라서 서버는 조각난 데이터의 처음과 끝을 boundary를 통해 알 수 있음
Boundary는 따라서 header에 존재해야한다. 처음에 딱 명시줘야 그 다음부터 소통이 될 것 아냐
그리고 또 Boundary는 유니크해야하는데, 그래서 보통 UUID로 만든다.
아래는 body 전체코드이다.
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="family_name"
Gupta
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="name"
Shubhransh
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="file"; filename="profilepic.jpg"
Content-Type: image/png
-a long string of image data-
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13—
하나씩 나눠서 설명해보자면
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="family_name"
Gupta
Boundary가 나왔으니 body의 시작이겠네
하이픈 두개— -
는 새로운 부분이 시작된다는 의미이다.
그리고 한줄 띄우고, 내용 Gupta
이 나온다
그 다음부분도 똑같은 형식임
그 다음다음에는 filename="profilepic.jpg"
이 추가되었는데 이것의 의미는 파일 업로드가 끝나면 그 파일의 이름을 저걸로 부를 수 있다고 알려주는 것이다
Content-Type: image/png
는 말 그대로 파일 타입을 말한다.
마지막에 하이픈 두개— - 가 나오는데 이 의미는 body가 끝났다는 것이다.
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13—
이러한 body의 구성 방식을 염두해두고 이제 직접 코드로 body를 만들어보자.
아래 코드가 전체 코드이다.
createMultiPartFormData 메서드에서 보면 알 수 있듯이... lineBreak나 하이픈 같이 사소한것 하나라도 놓치면 코드가 작동이 안된다.
let boundary = "Boundary-\(UUID().uuidString)"
func addProduct(information: NewProductInformation, images: [NewProductImage], completion: @escaping (Result<ProductDetail, Error>) -> Void) {
guard let url = URLManager.addNewProduct.url else { return }
var request = URLRequest(url: url, method: .post)
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.addValue("819efbc3-71fc-11ec-abfa-dd40b1881f4c", forHTTPHeaderField: "identifier")
request.httpBody = createRequestBody(product: information, images: images)
createDataTaskWithDecoding(with: request, completion: completion)
}
func createRequestBody(product: NewProductInformation, images: [NewProductImage]) -> Data {
let parameters = createParams(with: product)
let dataBody = createMultiPartFormData(with: parameters, images: images)
return dataBody
}
func createParams(with modelData: NewProductInformation) -> Parameters? {
guard let parameterBody = JSONParser.encodeToDataString(with: modelData) else { return nil }
let params: Parameters = ["params": parameterBody]
return params
}
func createMultiPartFormData(with params: Parameters?, images: [NewProductImage]?) -> Data {
let lineBreak = "\r\n"
var body = Data()
if let parameters = params {
for (key, value) in parameters {
body.append("--\(boundary + lineBreak)")
body.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak + lineBreak)")
body.append("\(value)\(lineBreak)")
}
}
if let images = images {
for image in images {
body.append("--\(boundary + lineBreak)")
body.append("Content-Disposition: form-data; name=\"\(image.key)\"; filename=\"\(image.fileName)\"\(lineBreak)")
body.append("Content-Type: image/jpeg, image/jpg, image/png\(lineBreak + lineBreak)")
body.append(image.data)
body.append(lineBreak)
}
}
body.append("--\(boundary)--\(lineBreak)")
return body
}
func createDataTaskWithDecoding<T: Decodable>(with request: URLRequest, completion: @escaping (Result<T, Error>) -> Void) {
let task = urlSession.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode >= 300 {
completion(.failure(URLSessionError.responseFailed(code: httpResponse.statusCode)))
return
}
if let error = error {
completion(.failure(URLSessionError.requestFailed(description: error.localizedDescription)))
return
}
guard let data = data else {
completion(.failure(URLSessionError.invaildData))
return
}
guard let decodedData = JSONParser.decodeData(of: data, type: T.self) else {
completion(.failure(JSONError.dataDecodeFailed))
return
}
completion(.success(decodedData))
}
task.resume()
}
이 코드를 Alamofire을 사용해서 똑같이 작성해보면,
앞서 골치아팠던 하이픈이나 띄어쓰기 등을 신경쓰지 않아도 되어 훨씬 간편하다는 것을 알 수 있다.
그냥 multipartFormData.append 메서드를 사용하면 key-value 형태로 데이터를 업로드할 수 있다.
이렇게 간편하고 쓸데없는 곳에 신경을 덜써도 되니 안 사용할 이유가 없다고 생각한다.
func uploadDataWithImage(information: NewProductInformation, images: [NewProductImage], completion: @escaping (Result<Int, Error>) -> Void) {
guard let url = URLManager.addNewProduct.url else { return }
let headers: Alamofire.HTTPHeaders = [
"Content-Type": "multipart/form-data",
"identifier": "819efbc3-71fc-11ec-abfa-dd40b1881f4c"
]
guard let informationData = JSONParser.encodeToDataString(with: information) else {
return
}
let parameters = ["params": informationData]
AF.upload(multipartFormData: { multipartFormData in
for (key, value) in parameters {
multipartFormData.append(value.data(using: .utf8)!, withName: key)
}
for image in images {
multipartFormData.append(image.data, withName: image.key, fileName: image.fileName, mimeType: "image/jpeg, image/jpg, image/png")
}
}, to: url, method: .post, headers: headers)
.response { response in
guard let statusCode = response.response?.statusCode else {
return
}
completion(.success(statusCode))
}
}
Alamofire 공식문서
Upload Videos to Server as a Multipart form data Using Alamofire