iOS/Swift

외부 API를 받는 데이터 함수의 데이터 타입 관리

밤새는 탐험가89 2024. 9. 6. 04:06
func getImageData(contentId: String, completion: @escaping (Result<ImageItems, Error>) -> Void) {
        var components = URLComponents(string: "\(Constants.base_URL)/detailImage1?")
        
        components?.queryItems = [
            URLQueryItem(name: "serviceKey", value: Constants.api_key),
            URLQueryItem(name: "MobileOS", value: "ETC"),
            URLQueryItem(name: "MobileApp", value: "AppTest"),
            URLQueryItem(name: "_type", value: "json"),
            URLQueryItem(name: "contentId", value: contentId),
            URLQueryItem(name: "imageYN", value: "Y"),
            URLQueryItem(name: "subImageYN", value: "Y"),
            URLQueryItem(name: "numOfRows", value: "10"),
            URLQueryItem(name: "pageNo", value: "1")
        ]
        
        if let encodedQuery = components?.percentEncodedQuery?.replacingOccurrences(of: "%25", with: "%") {
            components?.percentEncodedQuery = encodedQuery
        }
        
        guard let url = components?.url else { return }

        let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in
            guard let data = data, error == nil else { return }
            
            do {
                // AttractionResponse로 JSON 디코딩
                let results = try JSONDecoder().decode(ImageResponseResponse.self, from: data)
                
                // completion handler로 결과 반환
                completion(.success(results.response.body.items))
            } catch {
                completion(.failure(error))
            }
        }
        task.resume()
    }

 

🔥 let results = try JSONDecoder().decode(ImageResponseResponse.self, from: data) 🔥

 외부 API에서 JSON 데이터를 받아 디코딩할 때, 디코딩할 구조체는 항상 최상위 구조체를 기준으로 해야 합니다. 이는 JSON 데이터가 하나의 최상위 객체로 시작되기 때문입니다.

 

이유:

JSON 데이터를 디코딩할 때 Swift의 JSONDecoder는 데이터를 주어진 구조체의 구조에 맞춰 해석합니다. API에서 제공하는 JSON 응답이 아래와 같다고 가정하면:

{
  "response": {
    "header": {
      "resultCode": "0000",
      "resultMsg": "OK"
    },
    "body": {
      "items": {
        "item": [
          {
            "contentid": "2820640",
            "originimgurl": "http://example.com/image1.jpg",
            "imgname": "Sample Image 1",
            "smallimageurl": "http://example.com/small_image1.jpg",
            "cpyrhtDivCd": "Type1",
            "serialnum": "3305037_9"
          }
        ]
      },
      "numOfRows": 10,
      "pageNo": 1,
      "totalCount": 1
    }
  }
}

 

 

여기서 최상위 키는 response입니다. 따라서, 최상위 구조체는 ImageResponseResponse가 됩니다. JSON을 파싱하기 위해서는 JSON 전체 구조를 디코딩해야 하므로 최상위 구조체를 지정하여 데이터를 해석해야 합니다.

왜 최상위 구조체로 디코딩해야 할까?

  1. JSON 구조: JSON의 구조가 트리 형태로 되어 있기 때문에 최상위 구조체부터 시작해서 그 안의 데이터를 하나씩 파싱합니다. 예를 들어, 위의 JSON은 response 안에 header와 body가 있으며, body 안에 items가 있는 구조입니다.
  2. 중간 구조체로는 불가능: 중간에 있는 구조체(ImageItems나 ImageBody)로 디코딩하려고 하면, 디코더가 이 중간 구조체로 직접 접근할 수 없습니다. 전체 JSON의 구조를 유지하면서 순차적으로 디코딩을 진행해야 하기 때문에 최상위에서부터 시작해야 합니다.
  3. 전체 응답을 고려한 에러 처리: API 응답에서 헤더의 resultCode나 resultMsg도 중요한 경우가 많습니다. 이를 처리하려면 최상위 구조체로 디코딩한 후 필요한 데이터를 추출할 수 있습니다.

최상위 구조체로 디코딩 후 필요한 데이터 추출

한 번 최상위 구조체로 디코딩한 후 필요한 하위 데이터를 추출할 수 있습니다. 예를 들어, ImageItem 배열을 사용하려면:

do {
    // 최상위 구조체로 디코딩
    let results = try JSONDecoder().decode(ImageResponseResponse.self, from: data)
    
    // 필요한 데이터 추출
    let imageItems = results.response.body.items.item
    completion(.success(imageItems))  // ImageItem 배열을 반환
} catch {
    completion(.failure(error))
}
 

 

 

🔥 [ImageItem] 대신 ImageItems를 사용할 수 있지만, 이 둘은 의미적으로 다릅니다. 두 구조체의 차이점과 그것이 completion 핸들러에서 어떻게 적용되는지 살펴보겠습니다. 🔥

차이점 설명:

  1. [ImageItem]:
    • 이것은 ImageItem의 배열입니다. 즉, 여러 개의 개별 ImageItem 객체가 배열로 존재한다는 의미입니다.
    • 데이터를 처리할 때 배열을 바로 접근할 수 있기 때문에, 각 ImageItem에 쉽게 접근할 수 있습니다.
  2. ImageItems:
    • ImageItems는 단순히 item이라는 배열을 포함한 별도의 구조체입니다. 즉, 이 구조체는 item: [ImageItem]이라는 하나의 프로퍼티를 가지고 있습니다.
    • ImageItems 구조체 안에 배열을 한 번 더 래핑한 형태입니다.

사용 시 고려 사항:

  • API 응답에서 실제로 ImageItems가 JSON의 한 부분이고, 그 안에 item 배열로 여러 ImageItem이 들어있다면, 핸들러의 결과로는 ImageItems가 더 적합할 수 있습니다. 그렇지 않으면 item 배열에 접근하기 위해 한 단계를 더 거쳐야 합니다.

 

코드 예시:

completion: @escaping (Result<[ImageItem], Error>) -> Void

func getImageData(contentId: String, completion: @escaping (Result<[ImageItem], Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        guard let data = data, error == nil else {
            completion(.failure(error!))
            return
        }
        
        do {
            // 최상위 구조체로 디코딩
            let results = try JSONDecoder().decode(ImageResponseResponse.self, from: data)
            // ImageItems 안의 item 배열을 반환
            completion(.success(results.response.body.items.item))
        } catch {
            completion(.failure(error))
        }
    }
    task.resume()
}

 

 

completion: @escaping (Result<ImageItems, Error>) -> Void

func getImageData(contentId: String, completion: @escaping (Result<ImageItems, Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        guard let data = data, error == nil else {
            completion(.failure(error!))
            return
        }
        
        do {
            // 최상위 구조체로 디코딩
            let results = try JSONDecoder().decode(ImageResponseResponse.self, from: data)
            // ImageItems 자체를 반환
            completion(.success(results.response.body.items))
        } catch {
            completion(.failure(error))
        }
    }
    task.resume()
}

 

결론:

  • [ImageItem]를 반환: 반환되는 데이터가 단순히 ImageItem 배열이기 때문에, 바로 item 배열을 사용할 수 있어 편리합니다.
  • ImageItems를 반환: 반환되는 데이터가 item 배열을 감싼 구조체(ImageItems)이므로, 배열에 접근하기 위해 .item을 추가로 호출해야 합니다. ImageItems에 다른 프로퍼티가 추가되거나 필요하다면 적절할 수 있습니다.

 즉, 반환하는 데이터가 배열인지, 배열을 감싼 구조체인지에 따라 선택하시면 됩니다. completion에서 배열을 바로 쓰고 싶다면 [ImageItem]를 쓰고, 구조체 형태로 데이터를 넘겨야 한다면 ImageItems를 사용하세요.

 

 

 

🔥 originimgurl 필드만 따로 배열에 저장하려면, completion 핸들러에서 데이터를 처리할 때, ImageItem 배열에서 originimgurl만 추출해 새로운 배열을 만들어 반환할 수 있습니다.🔥

 

단계별로 설명하면:

  1. API로부터 ImageResponseResponse 구조체를 받아서 디코딩합니다.
  2. 그 구조체의 items에서 item 배열을 추출합니다.
  3. item 배열 내의 각 ImageItem에서 originimgurl을 추출해 새로운 문자열 배열로 만듭니다.
  4. 그 배열을 completion 핸들러로 반환합니다.

코드 예시:

completion: @escaping (Result<[String], Error>) -> Void

 -> completion에서 originimgurl 배열을 반환하도록 변경합니다.

func getImageData(contentId: String, completion: @escaping (Result<[String], Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        guard let data = data, error == nil else {
            completion(.failure(error!))
            return
        }
        
        do {
            // 최상위 구조체로 디코딩
            let results = try JSONDecoder().decode(ImageResponseResponse.self, from: data)
            // ImageItem 배열에서 originimgurl만 추출하여 배열로 만듦
            let originImageUrls = results.response.body.items.item.map { $0.originimgurl }
            // originimgurl 배열을 completion 핸들러로 반환
            completion(.success(originImageUrls))
        } catch {
            completion(.failure(error))
        }
    }
    task.resume()
}

 

설명:

  • map { $0.originimgurl }: item 배열 내의 각 ImageItem에서 originimgurl 필드만 추출해 배열로 만듭니다.
  • completion(.success(originImageUrls)): 최종적으로 추출한 originimgurl 배열을 completion 핸들러에 담아 반환합니다.

 

 

🔥 let originImageUrls = results.response.body.items.item.map { $0.originimgurl } 

 이 코드는 map 함수를 사용하여 배열(item)에서 특정 필드(originimgurl)만 추출하는 방식입니다. map 함수는 배열의 각 요소에 대해 동일한 작업을 수행하고, 그 결과를 새로운 배열로 반환하는 함수입니다.

단계별로 설명:

  1. results.response.body.items.item:
    • 여기서 results는 API로부터 디코딩된 ImageResponseResponse 구조체입니다.
    • response는 ImageResponse 구조체, 그 안에 body는 ImageBody 구조체, 그리고 items는 ImageItems 구조체입니다.
    • items.item는 [ImageItem] 타입의 배열입니다. 즉, 여러 개의 이미지 정보가 들어있는 배열이죠.
  2. map { $0.originimgurl }:
    • map 함수는 배열의 각 요소를 순회하면서 동일한 연산을 수행합니다.
    • 여기서 $0는 item 배열의 각 요소(ImageItem 구조체)를 의미합니다. 즉, 배열을 순회하며 하나씩 접근하는 것이 $0입니다.
    • $0.originimgurl은 ImageItem 구조체의 originimgurl 필드를 의미합니다.
    • 결과: map은 item 배열의 각 ImageItem에서 originimgurl 필드를 추출해 새로운 배열을 만듭니다.

코드 흐름:

  • results.response.body.items.item는 [ImageItem] 타입의 배열입니다.
  • map { $0.originimgurl }는 item 배열 내의 각각의 ImageItem에서 originimgurl 필드만 추출합니다.
  • 그 결과 originimgurl 문자열들을 담은 새로운 배열이 만들어집니다.

 

 

🔥 네트워크 요청과 데이터 처리 방식을 어떻게 나눌지에 대한 고민은 코드의 유지보수성, 재사용성, 확장성을 결정하는 중요한 요소입니다. 🔥

 

네트워크 요청과 데이터 처리를 분리하는 이유:

  • 책임 분리: 네트워크 요청과 데이터 처리는 각기 다른 책임을 가지고 있습니다. 네트워크 요청의 목적은 서버에서 데이터를 받아오는 것이고, 데이터 처리의 목적은 받은 데이터를 필요한 형태로 가공하는 것입니다. 두 가지를 분리하면 코드가 더 명확해지고, 변경사항이 생길 때 각 부분만 수정하면 되므로 유지보수가 쉬워집니다.
  • 재사용성: 네트워크 요청은 데이터를 받아오는 역할만 하고, 받은 데이터를 처리하는 부분은 별도의 함수로 만들면, 다른 곳에서도 재사용하기 쉽습니다. 예를 들어, 어떤 화면에서는 이미지 URL만 필요하고, 다른 화면에서는 전체 이미지 데이터가 필요할 수 있습니다. 이 경우 네트워크 요청은 동일하지만, 처리 방식만 달라지게 됩니다.

구조 제안:

  • 네트워크 요청 함수: 이 함수는 네트워크 요청을 통해 데이터를 받아오는 역할만 수행하고, 받은 데이터를 그대로 반환합니다.
func getImageData(contentId: String, completion: @escaping (Result<ImageItems, Error>) -> Void) {
    var components = URLComponents(string: "\(Constants.base_URL)/detailImage1?")
    
    components?.queryItems = [
        URLQueryItem(name: "serviceKey", value: Constants.api_key),
        URLQueryItem(name: "MobileOS", value: "ETC"),
        URLQueryItem(name: "MobileApp", value: "AppTest"),
        URLQueryItem(name: "_type", value: "json"),
        URLQueryItem(name: "contentId", value: contentId),
        URLQueryItem(name: "imageYN", value: "Y"),
        URLQueryItem(name: "subImageYN", value: "Y"),
        URLQueryItem(name: "numOfRows", value: "10"),
        URLQueryItem(name: "pageNo", value: "1")
    ]
    
    guard let url = components?.url else { return }
    let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in
        guard let data = data, error == nil else {
            completion(.failure(error!))
            return
        }
        
        do {
            let results = try JSONDecoder().decode(ImageResponseResponse.self, from: data)
            completion(.success(results.response.body.items)) // 네트워크 응답으로 아이템 목록 반환
        } catch {
            completion(.failure(error))
        }
    }
    task.resume()
}

 

  • 데이터 처리 함수: 네트워크에서 받은 데이터를 처리해서 필요한 데이터만 추출하는 함수를 따로 만들어서, 이를 호출하는 곳에서 사용합니다.
func extractOriginImageUrls(from items: ImageItems) -> [String] {
    return items.item.map { $0.originimgurl }
}

 

  • ViewController에서의 사용: 네트워크에서 데이터를 받아오고, 필요한 데이터를 추출하는 과정이 명확하게 나눠집니다. ViewController에서는 이 두 가지 함수를 사용하여 데이터 처리 흐름을 관리할 수 있습니다.
class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        getImageData(contentId: "2820640") { result in
            switch result {
            case .success(let items):
                let originImageUrls = extractOriginImageUrls(from: items)
                print(originImageUrls) // 추출된 이미지 URL을 사용
                
            case .failure(let error):
                print("Error: \(error)")
            }
        }
    }
}

 

 

왜 이렇게 나누는 게 좋은가?

  • 확장성: 나중에 originimgurl 외에 다른 데이터를 추출하거나 가공해야 할 때, 네트워크 요청을 수정하지 않고도 처리할 수 있습니다. 예를 들어, imgname도 추출해야 하는 경우, 데이터 처리 함수만 수정하면 됩니다.
func extractImageData(from items: ImageItems) -> [(url: String, name: String)] {
    return items.item.map { ($0.originimgurl, $0.imgname) }
}

 

  • 유지보수성: 네트워크 요청 관련 문제(예: URL 변경, API 키 갱신 등)가 발생하면 네트워크 코드만 수정하면 되고, 데이터 처리 문제(예: JSON 구조 변경 등)가 발생하면 처리 함수만 수정하면 됩니다.
  • 테스트 용이성: 네트워크 요청을 테스트할 때는 네트워크 요청 관련 코드만 테스트하고, 데이터 처리를 테스트할 때는 별도로 테스트할 수 있습니다. 이로 인해 디버깅과 테스트가 간편해집니다.

결론:

 네트워크 요청과 데이터 처리의 책임을 분리하는 것이 유지보수성과 확장성을 높이는 방법입니다. 네트워크 함수에서는 데이터를 받아오는 역할만 하고, 데이터를 가공하는 역할은 별도의 함수에서 처리하는 것이 좋습니다. 이렇게 하면 다양한 상황에서 코드 재사용이 가능해지고, 변화에 유연하게 대처할 수 있습니다.

 

 

🔥 ImageItems를 사용한 이유는 네트워크 요청에 대한 JSON 응답 구조 때문입니다. 네트워크 요청을 통해 받는 데이터가 단순히 [ImageItem] 배열이 아니라, 그 배열을 감싸고 있는 상위 구조가 있기 때문에 ImageItems로 처리하는 것이 더 맞기 때문입니다. 🔥

 

1. 배열 ([ImageItem])을 사용하는 경우

배열은 같은 타입의 데이터를 여러 개 관리해야 할 때 사용합니다. 데이터가 단순하고, 같은 타입의 여러 항목을 모아 관리할 때 매우 효율적입니다.

배열을 사용하는 경우:

  • 단순한 목록: 같은 유형의 객체들이 나열된 목록을 처리할 때.
  • 데이터가 동일한 구조를 반복할 때: 모든 항목이 동일한 구조를 가질 경우 배열로 다루는 것이 적합합니다.
  • 순서가 중요한 경우: 배열은 순서를 보장하기 때문에 순서대로 항목을 처리해야 하는 상황에 적합합니다.

예시:

  • 사진의 목록 ([ImageItem])
  • 사용자 댓글 목록 ([Comment])
  • 책 목록 ([Book])
let imageItems: [ImageItem] = [
    ImageItem(contentid: "123", originimgurl: "url1", imgname: "image1", smallimageurl: "smallurl1", cpyrhtDivCd: "Type1", serialnum: "1"),
    ImageItem(contentid: "456", originimgurl: "url2", imgname: "image2", smallimageurl: "smallurl2", cpyrhtDivCd: "Type2", serialnum: "2")
]

 

2. 구조체 (ImageItems)를 사용하는 경우

구조체는 다양한 속성들을 하나의 객체로 묶어 사용하고 싶을 때 사용합니다. 구조체는 여러 관련된 데이터를 하나의 의미 있는 단위로 관리합니다.

구조체를 사용하는 경우:

  • 다양한 속성을 하나로 묶고 싶은 경우: 여러 종류의 데이터를 모아 하나의 객체로 다루고 싶을 때.
  • 데이터의 그룹을 묶어서 의미 있는 단위로 관리하고 싶은 경우: 예를 들어, 응답 데이터의 상위 객체로 body, header 같은 추가적인 정보가 있을 때, 그 모든 것을 한 번에 관리하는 것이 더 적합합니다.
  • 복잡한 데이터를 관리하는 경우: 단순히 항목들을 나열하는 것 이상으로, 각각의 항목에 추가적인 메타데이터나 상태를 관리하고 싶을 때.

예시:

  • API 응답 데이터: 일반적으로 API의 응답은 하나의 객체에 여러 속성을 포함하고 있으며, 그 속성 중 하나로 배열을 포함할 수 있습니다.
  • 사용자의 프로필 정보: 이름, 이메일, 나이, 성별 등 다양한 속성을 한 번에 묶어 관리할 때.
struct ImageItems {
    let item: [ImageItem]  // 배열로 이미지를 포함
    let numOfRows: Int
    let pageNo: Int
    let totalCount: Int
}

 

언제 배열을 쓰고, 언제 구조체를 쓸까?

  • 배열을 사용할 때: 단순히 동일한 유형의 데이터가 나열되고, 그 데이터에 대해 순서가 중요하거나, 특정 그룹화가 필요 없을 때.
    • 예: "이미지 목록만 필요하다", "모든 항목이 동일한 구조를 가지고 있다."
  • 구조체를 사용할 때: 데이터를 더 의미 있는 단위로 묶어서 관리하고 싶을 때, 또는 추가적인 속성들이 필요할 때.
    • 예: "이미지 목록 외에 다른 메타 정보도 필요하다" (페이지 정보, 총 항목 수 등).

실제 사례 예시

  • 배열로 처리: 사진의 URL 목록을 간단히 출력하거나 사용할 때.
let imageURLs = imageItems.map { $0.originimgurl }

 

  • 구조체로 처리: 이미지 목록 외에도 페이지 정보나 다른 메타데이터를 함께 다루고 싶을 때.
let imageResponse = ImageItems(item: [ImageItem(...), ImageItem(...)], numOfRows: 10, pageNo: 1, totalCount: 100)
print(imageResponse.pageNo)  // 페이지 번호 출력

 

결론:

  • 배열은 동일한 타입의 데이터를 간단히 나열해야 할 때 사용.
  • 구조체는 데이터를 더 의미 있는 단위로 묶고, 여러 관련 데이터를 함께 관리할 때 사용.