티스토리 뷰
평소 네트워크 통신을 할때 Alamofire를 사용하면서
직접 URLSession을 사용해 이미지 업로드를 구현해보며 내부 동작을 파악하고 싶어
해당 블로깅을 작성해봅니다.
기본적으로 URLSession의 request 헤더 부분에 'Content-Type: application/json'을 사용하게 되는데,
이미지나 대용량 파일 등을 전송하기 위해선 multipart/form-data로 전송하는게 적합 합니다.
multipart/form-data
먼저 application/json | multipart/form-data 각각 request body를 보내는 방법 부터가 다른데 아래를 먼저 보고 가봅시다.
// application/json
let body: [String: Any] = [
"name": name,
"content": content
]
let requestBody: Data? = try? JSONSerialization.data(withJSONObject: body)
request.httpBody = requestBody
// multipart/form
private func createUploadBody(params: [String: String], boundary: String, imageData: Data) -> Data {
var body: Data = Data()
let lineBreak: String = "\r\n"
for (key, value) in params {
body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak)\(lineBreak)".data(using: .utf8)!)
body.append("\(value)\(lineBreak)".data(using: .utf8)!)
}
// image 파일
let fileName: String = "test.jpeg"
let mimeType: String = "image/jpeg"
body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"image\"; filename=\"\(fileName)\"\(lineBreak)".data(using: .utf8)!)
body.append("Content-Type: \(mimeType)\(lineBreak)\(lineBreak)".data(using: .utf8)!)
body.append(imageData)
body.append("\(lineBreak)--\(boundary)--\(lineBreak)".data(using: .utf8)!)
return body
}
body를 만드는 방법이 다르고 뭔가 복잡해 보이지만
multipart/form 방식 통신 규약에 대해 알고가면 조금 정리가 될겁니다.
위 createUploadBody 메서드로 body를 만들어 서버에 전송하면 아래와 같은 형태가 됩니다
POST /test.html HTTP/1.1
Host: example.org
Content-Type: multipart/form-data;boundary="boundary" // 1
// name
--boundary // 2
Content-Disposition: form-data; name="name"
// content
--boundary
Content-Disposition: form-data; name="content"
// imageFile
// 3
Content-Disposition: form-data; name="image"; filename="test.jpeg"
Content-Type: image/jpeg
1. Content-Type 헤더부분에 boundary 는 구분자 라고 생각하시면 됩니다.
해당 boundary 값으로 어디가 시작점이고 어디가 다음데이터 혹은 마지막 지점인지 구분할 수 있게 해주는 역할을 합니다.
대부분 boundary 값은 UUID() 값을 통해 고유한 값을 지정합니다 (request는 여러번 보내기 때문에 고유한 값을 지정해 구분이 필요하기 때문)
2. 1번에서 설명한것 처럼 boundary 값으로 시작점을 나타냅니다.
3. 'name', 'content'와 다르게 이미지 파일 업로드의 경우엔 mimeType, filename을 기입해줍니다.
image파일을 업로드할거니 mimeType은 image/jpeg가 되겠죠
filename은 말 그대로 파일 이름을 넣어주시면 됩니다.
참고
body를 만들때 값이 하나라도 맞지 않거나, 띄어쓰기, 줄바꿈이 다를 경우 통신이 되지 않습니다.
꼼꼼히 아래 규약을 잘 지키고 있는지 확인하셔야 합니다.
1. boundary로 구분을 잘해주고 있는지
2. 'lineBreak' ("\r\n")이 잘 되어있는지
3. data를 append 해줬는지
ex) Content-Disposition: form-data; name=\"\(key)\"\(lineBreak)\(lineBreak)
// 데이터 추가
4. 파일의 경우) 파일이름이 있는지, mimeType을 지정했는지
5. 데이터의 끝을 나타내는 경계값을 추가하여 데이터의 마지막 부분을 잘 구분 하고 있는지
아래는 사용 예시 입니다.
// UIImage를 Data로 전환
// self.selectedImage: UIImage
// guard let imageData = self.selectedImage?.jpegData(compressionQuality: 0.8) else { return }
func addTodo(
name: String,
content: String,
imageData: Data,
completion: @escaping (Result<SomeModel, Error>) -> Void
) {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "localhost"
urlComponents.port = 8080
urlComponents.path = "/testPath"
guard let url = urlComponents.url
else {
completion(.failure(NetworkError.invalidURL))
return
}
// request
var request: URLRequest = .init(url: url)
request.httpMethod = "POST"
// header
let identifier: String = UUID().uuidString
let boundary: String = "Boundary-\(identifier)"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let body: Data = createUploadBody(
params: [
"name": name,
"content": content
],
boundary: boundary,
imageData: imageData
)
request.httpBody = body
let task = URLSession.shared.uploadTask(
with: request,
from: body,
completionHandler: { data, response, error in
if let error = error {
completion(.failure(error))
return
}
if let data = data {
do {
let decodedData: SomeModel = try JSONDecoder().decode(SomeModel.self, from: data)
print(#function, "success: \(decodedData)")
completion(.success(decodedData))
}
catch {
completion(.failure(error))
}
}
})
task.resume()
}
private func createUploadBody(params: [String: String], boundary: String, imageData: Data) -> Data {
var body: Data = Data()
let lineBreak: String = "\r\n"
for (key, value) in params {
body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak)\(lineBreak)".data(using: .utf8)!)
body.append("\(value)\(lineBreak)".data(using: .utf8)!)
}
// image 파일
let fileName: String = "test.jpeg"
let mimeType: String = "jpeg"
body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"imageFile\"; filename=\"\(fileName)\"\(lineBreak)".data(using: .utf8)!)
body.append("Content-Type: image/\(mimeType)\(lineBreak)\(lineBreak)".data(using: .utf8)!)
body.append(imageData)
body.append("\(lineBreak)--\(boundary)--\(lineBreak)".data(using: .utf8)!)
return body
}
Alamofire의 경우도 uploadTask를 사용하는걸 볼 수 있습니다.
public class UploadRequest: DataRequest {
}
// ...
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
// ...
switch uploadable {
case let .data(data): return session.uploadTask(with: request, from: data)
case let .file(url, _): return session.uploadTask(with: request, fromFile: url)
case .stream: return session.uploadTask(withStreamedRequest: request)
}
// ...
}
지난 블로깅에서 Kingfisher를 분해해보며
그동안 당연하게 사용하고있던 라이브러리들의 내부구조가 어떻게 되어있는지 파악하면서
동작원리를 알아가는데 배워가는게 많은것 같습니다.
이번 Alamofire를 통해 업로드 하는기능을 URLSession으로 직접 구현해보면서
다시한번 Alamofire 내부구조도 확인할 수 있었고,
막 가져다 쓰기보단 짬내서라도 하나씩 파악하면서 공부하는 습관을 길러야겠다는 생각이 들었습니다.
참고
1. https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/POST
'iOS' 카테고리의 다른 글
[iOS] Decode 한번 뜯어보자 (feat. container, nestedContainer, decode) (0) | 2025.01.18 |
---|---|
[iOS] UITableView cell LifeCycle (0) | 2024.12.22 |
[iOS] Tuist App Extension (feat. WidgetExtension) (0) | 2024.10.25 |
[iOS] 고해상도 이미지 다운시 성능 개선 (Image DownSampling) (2) | 2024.10.11 |
[iOS] Tuist (feat. xcproj 충돌) (5) | 2024.09.02 |
- Total
- Today
- Yesterday
- Swift 프로퍼티
- Swift 프로그래머스
- swift reduce
- swift protocol
- Swift Leetcode
- Combine: Asynchronous Programming with Swift
- 원티드 프리온보딩
- Class
- Swift RIBs
- CS 네트워크
- iOS error
- swift programmers
- Swift ModernRIBs
- 2023년 회고
- swift property
- Swift init
- Swift 알고리즘
- RIBs tutorial
- Swift Error Handling
- Swift final
- ios
- removeLast()
- Swift 내림차순
- RTCCameraVideoCapturer
- swift 고차함수
- Swift
- Swift joined
- Swift joined()
- Swift inout
- swift (programmers)
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |