Bibi's DevLog 🤓🍎

[Swift] JSON API와 네트워크 통신하기 - URLSession, JSONConverter 본문

📱🍎 iOS/Code Templates

[Swift] JSON API와 네트워크 통신하기 - URLSession, JSONConverter

비비 bibi 2022. 4. 29. 00:27

220426

[Swift] JSON API와 네트워크 통신하기

1. HTTPManager (URLManager) 만들기

  • HTTP 요청을 보내고 그 결과를 받는 역할.
  • URLSession
  • URL
  • completionHandler : '완료 처리기'

HTTPManager

import Foundation
import os

// HTTP 요청을 보내고, 그 결과를 받는 역할
final class HTTPManager {

    static func requestGET(url: String, complete: @escaping (Data) -> ()) {
      // complete @escaping  : 클로저가 바로 실행되지 않고, 조건에 해당될 때 클로저가 실행됨
        guard let validURL = URL(string: url) else { return }

        var urlRequest = URLRequest(url: validURL) // URL에 보내는 URLRequest 생성
        urlRequest.httpMethod = HTTPMethod.get.description

        URLSession.shared.dataTask(with: urlRequest) { data, urlResponse, error in
            // 비동기!! -> 서버에서 요청이 처리된 다음 실행되는 부분.
                        // dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
                        // completion handler의 내용은 모두 옵셔널로 넘어온다.
            guard let data = data else { return }
            guard let response = urlResponse as? HTTPURLResponse,
                                    (200..<300).contains(response.statusCode) else { // response를 HTTPURLResponse로 바꿨을 때 statusCode가 200번대(성공)이면 계속 진행
                if let response = urlResponse as? HTTPURLResponse {
                    os_log("%@", "\(response.statusCode)") // 아니라면 statusCode 출력
                }
                return
            }

            complete(data) // complete에 담겨 온 디코더 클로저에 data를 넘김
        }.resume() // 해당 task를 실행함
    }

    //Post - encode된 Data를 매개변수로 받아옴
    static func requestPOST(url: String, encodingData: Data, complete: @escaping (Data) -> ()) {
        guard let validURL = URL(string: url) else { return }

        var urlRequest = URLRequest(url: validURL)
        urlRequest.httpMethod = HTTPMethod.post.description
        urlRequest.httpBody = encodingData // GET과 다르게 보낼 데이터를 httpBody에 넣어준다
        urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
        urlRequest.setValue("\(encodingData.count)", forHTTPHeaderField: "Content-Length")

        URLSession.shared.dataTask(with: urlRequest) { data, urlResponse, error in
            guard let data = data else { return }
            guard let response = urlResponse as? HTTPURLResponse, (200..<300).contains(response.statusCode) else {
                if let response = urlResponse as? HTTPURLResponse {
                    os_log("%@", "\(response.statusCode)")
                }
                return
            }

            complete(data)
        }.resume()
    }

    //Patch - Post와 비슷함
    static func requestPATCH(url: String, encodingData: Data, complete: @escaping (Data) -> ()) {
        guard let validURL = URL(string: url) else { return }

        var urlRequest = URLRequest(url: validURL)
        urlRequest.httpMethod = HTTPMethod.patch.description
        urlRequest.httpBody = encodingData
        urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
        urlRequest.setValue("\(encodingData.count)", forHTTPHeaderField: "Content-Length")

        URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            guard let data = data else { return }
            guard let response = response as? HTTPURLResponse, (200..<300).contains(response.statusCode) else {
                if let response = response as? HTTPURLResponse{
                    os_log("%@", "\(response.statusCode)")
                }
                return
            }

            complete(data)
        }.resume()
    }

    // Delete
    static func requestDELETE(url: String, encodingData: Data, complete: @escaping (Data) -> ()) {
        guard let validURL = URL(string: url) else { return }
        var urlRequest = URLRequest(url: validURL)
        urlRequest.httpMethod = HTTPMethod.delete.description
        urlRequest.httpBody = encodingData
        urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")

        URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            guard let data = data else { return }
            guard let response = response as? HTTPURLResponse, (200..<300).contains(response.statusCode) else { return }

            complete(data)
        }.resume()
    }
}

HTTPMethod


import Foundation

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case patch = "PATCH"
    case delete = "DELETE"

    var description: String {
        switch self {
        case .get:
            return "GET"
        case .post:
            return "POST"
        case .patch:
            return "PATCH"
        case .delete:
            return "DELETE"
        }
    }
}

2. JSONConverter 만들기

  • JSON을 swift객체로 변환하거나 (decoding),
  • swift객체를 JSON으로 변환하는 (encoding) 역할.
  • 객체 T는 Codable 프로토콜을 채택해야 한다.
import Foundation

// JSON decoding(JSON->swift객체)과 encoding(swift객체->JSON) 담당
final class JSONConverter {

    static func decodeJsonArray<T: Codable>(data: Data) -> [T]? {
        // JSON 배열 디코더 : JSON Array를 [T] 타입으로 변환
        do {
            let result = try JSONDecoder().decode([T].self, from: data)
            return result
        } catch { // 에러 발생 시
            guard let error = error as? DecodingError else { return nil }
            // error : catch 내부에서 암시적으로 사용되는 에러를 나타내는 파라미터

            switch error { // 에러 유형에 따른 에러메시지 출력 가능
            case .dataCorrupted(let context):
                print(context.codingPath, context.debugDescription, context.underlyingError ?? "", separator: "\n")
                return nil
            default :
                return nil
            }
        }
    }

    static func decodeJson<T: Codable>(data: Data) -> T? {
        // JSON 객체 디코더 : JSON 객체를 T타입으로 변환
        do {
            let result = try JSONDecoder().decode(T.self, from: data) // T타입인 것 빼고 위 메서드와 동일
            return result
        } catch {
            guard let error = error as? DecodingError else { return nil }

            switch error {
            case .dataCorrupted(let context):
                print(context.codingPath, context.debugDescription, context.underlyingError ?? "", separator: "\n")
                return nil
            default :
                return nil
            }
        }
    }

    static func encodeJson<T: Codable>(param: T) -> Data? {
        // JSON 인코더 : T타입의 객체를 JSON으로 변환
        do {
            let result = try JSONEncoder().encode(param)
            return result
        } catch {
            return nil
        }
    }
}

3. JSON응답 데이터에 맞는 구조체 만들기

  • 프로젝트마다 새로 설계 필요
  • JSON response 형식과 동일해야 함
  • Codable해야 함
  • API key와 동일하지 않은 프로퍼티명이 있다면, CodingKey를 import한 enum을 선언해 해결해 주어야 함
  • 유용한 사이트 : https://app.quicktype.io/

import Foundation

// MARK: - HomeData
struct HomeData: Codable {
    let displayName: String
    let yourRecommand: Recommand
    let mainEvent: MainEvent
    let nowRecommand: Recommand

    enum CodingKeys: String, CodingKey {
        case displayName = "display-name"
        case yourRecommand = "your-recommand"
        case mainEvent = "main-event"
        case nowRecommand = "now-recommand"
    }
}

// MARK: - MainEvent
struct MainEvent: Codable {
    let imageUploadPath: String
    let mobTHUM: String

    enum CodingKeys: String, CodingKey {
        case imageUploadPath = "img_UPLOAD_PATH"
        case mobTHUM = "mob_THUM"
    }
}

// MARK: - Recommand
struct Recommand: Codable {
    let products: [String]
}

4-1. 네트워크 요청을 보내고 결과를 받을 Manager 클래스 만들기

  • 1.에서 만든 HTTPManager와 2.에서 만든 JSONConverter를 사용해 HTTP 요청을 보내고, 그 결과를 담아 관리할 클래스 생성
import Foundation

// HTTPManager와 JSONConverter를 사용해 HTTP요청을 보내고, 그 결과를 관리하는 클래스
final class NetworkManager {

    public static let publicNetworkManager = NetworkManager()

    static let identifier = "NetworkManager"
    static let homeDataNotification = "HomeDataNotificationName"

    func getHomeData(completion: @escaping (HomeData?) -> Void) {
        HTTPManager.requestGET(url: "서버API주소") { data in
            // get요청의 CompletionHandler로 JSON Decoder를 보냄 : 응답 정보를 Swift객체로 변환하기 위해
            // 응답 정보가 단일객체이면 decodeJson()을, 배열이면 decodeJsonArray()를 사용
            guard let data: HomeData = JSONConverter.decodeJson(data: data) else {
                return
            }
            completion(data)
        }
    }
}

4-2. 이미지URL을 통해 요청을 보내고 이미지를 다운로드받는 ImageCacheManager 추가 (선택)

5. 관련 ViewController에서 extension을 통해 API 호출 시점과 이후 처리 구현