개발 공부

API호출 후 데이터 가공해서 서버에 저장하기(1) (SwiftUI)

김 Duke 2023. 3. 31. 15:45

향수 관련 앱을 만들기 위해 향수의 정보 데이터가 필요했다.

팀 프로젝트 때 Sephora에서 제공해주는 향수 데이터를 가져다 썼었다. 

그 과정에서 우리 팀은 '데이터호출 / 데이터가공' 으로 역할을 나누었다.

호출을 계속 시도하던 중 데이터가 형태가 일치하지 않는 것을 알고 가공으로 넘어가게 됐다.

호출을 하다말아서 아쉬움이 남았고 내껄로 만들자 해서!

 그 과정을 공부해보았고 기록하고자 한다.

 

API: RapidAPI

데이터: Sephora

가공: Xcode, Swift

서버: Firebase Firestore

 

과정


이번 글에선 1️⃣데이터 호출에 대해 다뤄볼 것이다.

 

 

1️⃣데이터 호출 -> 2️⃣호출한 데이터를 가지고 한번 더 호출 -> 3️⃣데이터 파싱 -> 4️⃣Firebase 올리기

 

 

 

 

본론에 들어가기 앞서서 어떤 데이터를 쓰는지 설명이 필요할 것 같다.

RapidAPI를 이용하여 Sephora 데이터 호출하려고 한다.

 

 

Sephora, RapidAPI가 뭔가요 ?

Sephora는 외국의 올리브영 같은 사이트이다.

이 사이트에서 제공해주는 향수 데이터를 사용할 것이다.

https://www.sephora.com/

 

 

RapidAPI는 API를 이용하게 해주는 서비스이다.

https://rapidapi.com/hub

 

 

xcode에서 전처리한 이유?

많은 향수데이터를 다루려면 자동화가 필수임

데이터를 받아오고 가공하고 업로드까지 자동화하기 위해.

 

 

그럼 왜 Firebase에 향수 데이터를 저장했을까? 

- API에서 데이터 호출 시간 지연

- 프로젝트에 맞게끔 데이터를 가공하기 위해

 

 

 

 

데이터 호출


본격적으로 API를 이용하여 데이터를 호출해보자.

API란 ?

클라이언트 - API - 데이터

클라이언트가 데이터를 얻기위한 수단 (interface)

 

ex)

배달 시키는 상황을 생각해보자. 

배민을 켜서 가게에 대한 정보를 얻고 주문까지 완료한다. 

 

나(클라이언트) -> 인터페이스(배민) -> 데이터(가게)


 

RapidAPI는 가입 후 과금이 필요한데 나는 공부용이라 무료 요금제를 이용했다.

 

 

RapidAPI에서 Sephora를 검색한다.

 

 

 

그럼 아래처럼 Sephora의 데이터를 호출할 수 있는 페이지가 나온다.

왼쪽에서 어떤 데이터를 GET해올지 선택할 수 있는데 나는 product/list에서 향수들의 리스트를, product/detail에서 향수의 세부정보를 받아올 예정이다.

먼저 product/list에서 향수 리스트와 몇몇 정보를 받아올 것이다.

 

X-RapidAPI-Key 부분은 personal key이기 때문에 가려놨다.

Sephora에서는 향수뿐만 아니라 다양한 상품들의 데이터를 제공하는데 이를 카테고리 id로 구분할 수 있다.

 

내가 사용할 향수 카테고리의 id는 'cat60148'이다. 오른쪽의 Code snippets 부분을 보면 코드가 적혀있다.

Code snippets의 코드는 personal key(X-RapidAPI-Key)로 URL(상수 request)의 데이터를 받아온다.

언어에 따라 설정하면 되고 나는 Swift로 바꿔놓은 상태이다.  URL을 보면 카테고리id가 들어가있는게 볼 수 있다.

 

 

카테고리 id를 입력하고, 파란 버튼(Test endpoint)을 눌러보면 향수 카테고리에 속한 제품들이 json 형태로 출력된다.

살펴보면 products라는 배열에 향수의 대략적인 정보들이 Dictionary형태로 들어가있다.

여기서 향수들의 brandName, displayName, productId, image450을 저장할 것이고,

그 이후에는 productId를 이용하여 product/detail에서 향수의 세부정보를 뽑아올 예정이다

 

 

우선 위에서 설명한 부분을 코드로 설명해보려한다.

위의 code snippets를 클래스에 넣어서 함수로 만들었다.

class APIStore {
    
    let headers = [
        "X-RapidAPI-Key": "Personal Key를 입력하세요",
        "X-RapidAPI-Host": "sephora.p.rapidapi.com"
    ]
    
    func fetchList() {
        
        let request = NSMutableURLRequest(url: NSURL(string: "https://sephora.p.rapidapi.com/products/list?categoryId=cat60148&pageSize=60&currentPage=1")! as URL,
                                          cachePolicy: .useProtocolCachePolicy,
                                          timeoutInterval: 10.0)
        request.httpMethod = "GET"
        request.allHTTPHeaderFields = headers
        
        let session = URLSession.shared
        let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
            if (error != nil) {
                print(error)
            } else {
                let httpResponse = response as? HTTPURLResponse
                print(httpResponse)
                print(data)
            }
        })
        
        dataTask.resume()
    }
}

 

 

dataTask의 data를 프린트해보면 옵셔널로 출력된다.

 

요 data는 현재 json형태이다. 옵셔널 바인딩 해준 뒤, 데이터를 디코딩을 해줄 것이다.

웹에서 보는 데이터

 

 

위의 데이터를 우리가 보기 쉽게  만들어보면 이런 식의 json 형태의 데이터이다.

{ 
  products: [
    {	
      brandName: "CHANEL",
      displaName: "CHANCE EAU TENDRE Eau de Toilette",
      ...
    }, 
    {
      brandName: "Dior",
      displaName: "Miss Dior Blooming Bouquet",
      ...
    },
    ...
  ]
}

 

그래서 swift의 JSONDecoder를 사용하여 디코딩 해준다.

디코딩을 수월하게 해줄 Model이다.

struct FetchList: Codable {
    let products: [Product]
}

struct Product: Codable, Identifiable {
    var id: String { self.productId }
    let brandName: String
    let displayName: String
    let productId: String
    let image450: String
}

 

 

guard let을 이용하여 data를 바인딩해준다.

let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
            if (error != nil) {
                print(error)
            } else {
                
                guard let data = data else {return}
                guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {return}

                do {
                    let list = try JSONDecoder().decode(FetchList.self, from: data)
                    print(list.products)
                } catch (let err) {
                    print(err)
                }
            }
        })

 

 

요렇게 디코딩을 해주고 list의 프로퍼티인 products를 출력해보면

요런 식으로 향수의 정보들이 담긴 것이 보인다.(60개)

 

 

간단하게 정리해보면 이렇다. 

list.products = [
  Product(brandName: "CHANEL", displayName: "CHANCE EAU TENDRE Eau de Toilette", productId: "P258612", image450: "https://www.sephora.com/productimages/sku/s1237379-main-Lhero.jpg"), 
  Product(brandName: "Glossier", displayName: "Glossier You Eau de Parfum", productId: "P504364", image450: "https://www.sephora.com/productimages/sku/s2649770-main-Lhero.jpg"),
  Product(brandName: "Dior", displayName: "Miss Dior Blooming Bouquet", productId: "P385477", image450: "https://www.sephora.com/productimages/sku/s1599869-main-Lhero.jpg")
]

 

 

이것을 뷰에 연결해보면 아래처럼 나온다.

 

 

이후 내용은 다음 글에 이어서 올리겠습니다.

 

글에 나온 코드

ContentView

 

//
//  ContentView.swift
//  API
//
//  Created by TAEHYOUNG KIM on 2023/03/28.
//

import SwiftUI

struct ContentView: View {
    @ObservedObject var apiStore: APIStore = APIStore()
    var body: some View {
        NavigationStack {
            VStack {
                List(apiStore.products) { product in
                    NavigationLink {
                        VStack(alignment: .leading) {
                            Text("브랜드: \(product.brandName)")
                            Text("이름: \(product.displayName)")
                            Text("Id: \(product.productId)")
                            AsyncImage(url: URL(string: product.image450)) { image in
                                image.image?
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                            }
                        }
                        .padding(20)
                    } label: {
                        VStack(alignment: .leading) {
                            Text(product.brandName)
                                .font(.largeTitle)
                            Text(product.displayName)
                        }
                    }
                }
            }
        }
        .onAppear {
            apiStore.fetchList()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

Model

//
//  Model.swift
//  API
//
//  Created by TAEHYOUNG KIM on 2023/03/31.
//

import Foundation

 struct FetchList: Codable {
    let products: [Product]
 }

 struct Product: Codable, Identifiable {
    var id: String { self.productId }
    let brandName: String
    let displayName: String
    let productId: String
    let image450: String
 }
}

 

APIstore

 

//
//  APIStore.swift
//  API
//
//  Created by TAEHYOUNG KIM on 2023/03/28.
//

import Foundation

class APIStore: ObservableObject {
    
    @Published var products: [Product] = []
    
    let headers = [
        "X-RapidAPI-Key": "Personal Key를 입력하세요",
        "X-RapidAPI-Host": "sephora.p.rapidapi.com"
    ]
    
    func fetchList() {
        
        let request = NSMutableURLRequest(url: NSURL(string: "https://sephora.p.rapidapi.com/products/list?categoryId=cat60148&pageSize=60&currentPage=1")! as URL,
                                          cachePolicy: .useProtocolCachePolicy,
                                          timeoutInterval: 10.0)
        request.httpMethod = "GET"
        request.allHTTPHeaderFields = headers
        
        let session = URLSession.shared
        
        let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
            if (error != nil) {
//                print(error)
            } else {
                
                guard let data = data else {return}
                guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {return}

                do {
                    let list = try JSONDecoder().decode(FetchList.self, from: data)
                    DispatchQueue.main.async {
                        self.products = list.products
                    }
                } catch (let err) {
                    print(err)
                }
            }
        })
        dataTask.resume()
    }
}

 

 

다음 글 링크: https://yahoth.tistory.com/7

 

 

API호출 후 데이터 가공해서 서버에 저장하기(2) (SwiftUI)

앞글에서 API를 이용하여 1️⃣데이터 호출에 대해 다뤘다. 이번 글에선 2️⃣먼저 호출한 데이터로 다른 데이터를 호출하는 것부터 이후 3️⃣,4️⃣까지 마무리할 것이다. 1️⃣✅ 데이터 호출

yahoth.tistory.com