iOS) URLSession 에 대해서 알아보자(2/2) - 실전

6 minute read

서버통신에서 가장 많이 사용하는 GET, POST 통신의 다양한 경우에 대한 코드를 작성해보겠다.

  • GET 통신
    • 기본적인 URL 만 가진 GET 통신
    • query parameter 를 가지는 GET 통신
    • http header 를 가지는 GET 통신
    • file 을 download 해서 임시저장하는 GET 통신(URLSessionDownloadTask 사용)
  • Post 통신
    • request body 를 가지는 POST 통신(URLSessionDataTask 사용)
    • request body 를 가지는 POST 통신(URLSessionUploadTask 사용)
    • request body 에 image 를 포함한 multipart/form-data POST 통신

⛑ 프로젝트 설정

https 가 아닌 http 로 진행할 경우 다음과 같이 info.plist 를 설정해주어야 한다.

스크린샷 2021-09-18 오후 5 19 46

⛑ Model 설정

다음은 임시로 만든 model 이다. 서버에서 넘겨주는 JSON 에 맞춰서 바꿔주면 된다.

// 📌 Model.swift
import Foundation

// MARK: - Model
struct Model: Codable {
    let code: Int
    let data: DataClass
    let message: String
}

// MARK: - DataClass
struct DataClass: Codable {
}

⛑ GET 통신

  • 기본적인 URL 만 가진 GET 통신
// GET
    func requestGET(url: String, completionHandler: @escaping (Bool, Any) -> Void) {
        guard let url = URL(string: url) else {
            print("Error: cannot create URL")
            return
        }

        // Request
        var request = URLRequest(url: url)
        request.httpMethod = "GET"

        // Task
        let defaultSession = URLSession(configuration: .default)
        
        defaultSession.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            guard error == nil else {
                print("Error occur: error calling GET - \(String(describing: error))")
                return
            }

            guard let data = data, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else {
                print("Error: HTTP request failed")
                return
            }

            guard let output = try? JSONDecoder().decode(Model.self, from: data) else {
                print("Error: JSON data parsing failed")
                return
            }
            
            completionHandler(true, output.data)
        }.resume()
    }
  • query parameter 를 가지는 GET 통신
// GET query parameter
    func requestGETWithQuery(url: String, query: [String : String], completionHandler: @escaping (Bool, Any) -> Void) {
        // URLComponents 를 사용하면 qery parameter 부분을 URLQueryItem 객체로 쉽게 추가할 수 있다.
        guard var urlComponents = URLComponents(string: url) else {
            print("Error: cannot create URLComponents")
            return
        }
        
        // ✅ query 추가
        let queryItemArray = query.map {
            URLQueryItem(name: $0.key, value: $0.value)
        }
        urlComponents.queryItems = queryItemArray
        
        // URL
        guard let requestURL = urlComponents.url else { return }
        
        // request method 는 기본적으로 GET 이다. 혹시나 POST 등을 사용하고 싶다면 URLRequest 을 만들어야한다.
//        var request = URLRequest(url: requestURL)
//        request.httpMethod = "GET"
        
        // Task
        let defaultSession = URLSession(configuration: .default)
        
        defaultSession.dataTask(with: requestURL) { (data: Data?, response: URLResponse?, error: Error?) in
            // ...
        }.resume()
    }
  • http header 를 가지는 GET 통신
// GET header
    func requestGETWithHeader(url: String, headerField: [String : String], completionHandler: @escaping (Bool, Any) -> Void) {
        guard let url = URL(string: url) else {
            print("Error: cannot create URL")
            return
        }

        // Request
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        
        // ✅ header 추가
        _ = headerField.map { (key, value) in
            request.addValue(key, forHTTPHeaderField: value)
        }

        // Task
        let defaultSession = URLSession(configuration: .default)
        
        defaultSession.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            // ...
        }.resume()
    }
  • file 을 download 해서 임시저장하는 GET 통신(URLSessionDownloadTask 사용)
// GET - Image 수신
    func requestPOSTWithURLSessionDownloadTask(url: String, completionHandler: @escaping (Bool, Any) -> Void) {
        guard let url = URL(string: url) else {
            print("Error: cannot create URL")
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        
        let defaultSession = URLSession(configuration: .default)
        
        defaultSession.downloadTask(with: request) { (location: URL?, response: URLResponse?, error: Error?) in
            guard error == nil else {
                print("Error occur: error calling POST - \(String(describing: error))")
                return
            }

            guard let location = location, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else {
                print("Error: HTTP request failed")
                return
            }
            
            completionHandler(true, location)
        }.resume()
    }

⛑ POST 통신

URLSessionDataTask 로 URLRequest 에 httpBody 를 설정해서 POST 통신을 할 수 있다.

또한, POST 통신을 목적으로 한 URLSessionUploadTask 를 사용할 수도 있다.

  • URLSessionDataTask : Data task 는 리소스를 요청하고 응답받아 NSData 객체로 반환. default, ephemeral, shared session 에서는 지원되지만 background sessions 에서는 지원되지 않는다.
  • URLSessionUploadTask : Upload task 는 데이터를 업로드할 수 있도록 request body 를 제공한다. background session 도 지원된다.

Alamofire 는 어떤 task 를 사용하는지 해체쇼를 해봤다.. URLSessionUploadTask 를 사용하고 있었다.

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)
        }

    // ...
}

두가지 모두 가능하니 해보겠다.

  • request body 를 가지는 POST 통신(URLSessionDataTask 사용)
// POST - URLSessionDataTask
    func requestPOSTWithURLSessionDataTask(url: String, parameters: [String : Any], completionHandler: @escaping (Bool, Any) -> Void) {
        // ✅ paramters 를 JSON 으로 encode.
        let requestBody = try! JSONSerialization.data(withJSONObject: parameters, options: [])
        
        guard let url = URL(string: url) else {
            print("Error: cannot create URL")
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        // ✅ request body 추가
        request.httpBody = requestBody
        
        let defaultSession = URLSession(configuration: .default)
        
        defaultSession.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            guard error == nil else {
                print("Error occur: error calling POST - \(String(describing: error))")
                return
            }

            guard let data = data, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else {
                print("Error: HTTP request failed")
                return
            }

            guard let output = try? JSONDecoder().decode(Model.self, from: data) else {
                print("Error: JSON data parsing failed")
                return
            }
            
            completionHandler(true, output.data)
        }.resume()
    }
  • URLSessionUploadTask 사용
// POST - URLSessionUploadTask
    func requestPOSTWithURLSessionUploadTask(url: String, parameters: [String : Any], completionHandler: @escaping (Bool, Any) -> Void) {
        // ✅ parameters 를 JSON 으로 encode.
        let uploadData = try! JSONSerialization.data(withJSONObject: parameters, options: [])
        
        guard let url = URL(string: url) else {
            print("Error: cannot create URL")
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        // request.httpBody = uploadData
        
        let defaultSession = URLSession(configuration: .default)

        // ✅ uploadTask(with:from:) 메서드 사용해서 reqeust body 에 추가.
        defaultSession.uploadTask(with: request, from: uploadData) { (data: Data?, response: URLResponse?, error: Error?) in
            // ...
        }.resume()
    }
  • request body 에 image 를 포함한 Multipart/form-data POST 통신

multipart/form-data POST 통신에 대해서 코드를 살펴보기 전에 multipart/form-data 에 대해서 알아보자.

따로 글로 정리해보았다.

iOS) HTTP Multipart/form-data 이해하기

본격적으로 코드를 짜보자.

// POST - Image 송신
    // ✅ data : UIImage 를 pngData() 혹은 jpegData() 사용해서 Data 로 변환한 것.
    // ✅ filename : 파일이름(img.jpg 과 같은 이름)
    // ✅ mimeType :  타입에 맞게 png면 image/png, text text/plain 등 타입.
    func requestPOSTWithMultipartform(url: String,
                                      parameters: [String : String],
                                      data: Data,
                                      filename: String,
                                      mimeType: String,
                                      completionHandler: @escaping (Bool, Any) -> Void) {
        
        guard let url = URL(string: url) else {
            print("Error: cannot create URL")
            return
        }
        
        // ✅ boundary 설정
        let boundary = "Boundary-\(UUID().uuidString)"
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        // request.httpBody = uploadData
        
        // ✅ data
        var uploadData = Data()
        let imgDataKey = "img"
        let boundaryPrefix = "--\(boundary)\r\n"
        
        for (key, value) in parameters {
            uploadData.append(boundaryPrefix.data(using: .utf8)!)
            uploadData.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
            uploadData.append("\(value)\r\n".data(using: .utf8)!)
        }
        
        uploadData.append(boundaryPrefix.data(using: .utf8)!)
        uploadData.append("Content-Disposition: form-data; name=\"\(imgDataKey)\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        uploadData.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
        uploadData.append(data)
        uploadData.append("\r\n".data(using: .utf8)!)
        uploadData.append("--\(boundary)--".data(using: .utf8)!)
        
        let defaultSession = URLSession(configuration: .default)
        // ✅ uploadTask(with:from:) 메서드 사용해서 reqeust body 에 data 추가.
        defaultSession.uploadTask(with: request, from: uploadData) { (data: Data?, response: URLResponse?, error: Error?) in
            // ...
        }.resume()
    }

느낀점

URLSession 을 사용해보니까 통신의 경우의 수는 많은데 URLSession 을 통해서 매번 그에 맞는 최적의 인터페이스를 짜주는 기분이었다.

Alamofire 의 경우에는 AF.request() 를 통해서 어떤 통신의 종류든 파라미터가 있든 없든 쉽게 작성가능했다. 또한 validate() 를 통해서 유효성 검사도 간편했다. 다양한 response handler 를 만들어 두었기 때문에 이 역시 편리하게 느껴졌다.

URLSession 에 대해서 공부해봤으니 다음에는 Alamofire 를 해체해보려고 한다..🔪슉..슈슉….슈..슈슉..

참고

[iOS - swift] URLSession 네트워크 통신 기본 (URLSessionConfiguration, URLSession, URLComponents, URLSessionTask)

POST, 파일 업로드 요청을 URLSession 사용하여 구현해보자.

URLSession을 통한 간단한 업로드 - Swift · Wireframe

[Swift] - MultiPart통신 (멀티파트 이미지업로드)

깃허브

GitHub - hyun99999/URLSessionTutorial-iOS: 🛸 URLSession 서버통신 튜토리얼

Categories:

Updated: