목차
앞글에서 API를 이용하여 1️⃣데이터 호출에 대해 다뤘다.
이번 글에선 2️⃣먼저 호출한 데이터로 다른 데이터를 호출하는 것부터 이후 3️⃣,4️⃣까지 마무리할 것이다.
1️⃣✅ 데이터 호출 -> 2️⃣호출한 데이터를 가지고 한번 더 호출 -> 3️⃣데이터 파싱 -> 4️⃣Firebase 올리기
앞글 링크: https://yahoth.tistory.com/6
API호출 후 데이터 가공해서 서버에 저장하기(1) (SwiftUI)
향수 관련 앱을 만들기 위해 향수의 정보 데이터가 필요했다. 팀 프로젝트 때 Sephora에서 제공해주는 향수 데이터를 가져다 썼었다. 그 과정에서 우리 팀은 '데이터호출 / 데이터가공' 로 역할을
yahoth.tistory.com
2️⃣호출한 데이터를 가지고 한번 더 호출
앞의 글에서는 product/list에서 데이터를 호출했다면,
이번 글에서는 앞서 호출했던 데이터 중 향수의 id(productId)를 가지고 product/detail에서 데이터를 호출해보려고 한다.
가운데 하단의 productId 칸에 앞에서 가져온 향수 중 하나의 id를 넣었다.
Code snippets 부분이 앞 글과 유사하나, 호출 내용에 따라 URL부분이 달라졌다.
이 상태로 Test Endpoint버튼을 눌러보면 나오는 두 가지 항목을 얻어오려고 한다.
바로 코드로 가보겠다.
func fetchDetail(productId: String) {
let request = NSMutableURLRequest(url: NSURL(string: "https://sephora.p.rapidapi.com/products/detail?productId=\(productId)&preferedSku=2210607")! 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 detail = try JSONDecoder().decode(FetchDetail.self, from: data)
DispatchQueue.main.async {
self.detail = detail
}
} catch (let err) {
print(err)
}
}
})
dataTask.resume()
}
fetchDetail 함수에 특정 향수id를 변수로 넣고 뷰에서 호출해보면 아래처럼 나온다.
여기서 받아온 quickLookDescription과 longDescription 데이터를, 앞서 호출한 향수정보(전글 참고)와 합쳐주려고 한다.
3️⃣ 데이터 파싱
여기서 문제 발생! longDescription(롱)을 살펴보면 HTML 태그로 감싸져있는 것을 볼 수 있다.
아래 사진은 실제 Sephora 사이트에서 캡쳐한 사진인데 위의 long부분이 밑의 사진의 내용이라고 보면 된다.
나는 Fragrance Family, Scent Type, Key Notes 부분을 사용하기 위해 데이터 파싱의 과정을 거쳤다.
이 부분은 간단하게 다루고 넘어가겠다.
과정
1. HTML 태그를 제거한다.
2. 3가지 항목으로 나누어 저장한다.
SwiftSoup이라는 라이브러리를 이용하여 HTML태그를 모두 제거해준 뒤 코드를 짜서 3가지 항목으로 나누어주었다.
이 때 Sephora에서 제공해주는 데이터가 모두 일정한 형태를 갖고 있진 않았다. 그래서 예외처리도 필요하였다.
import Foundation
import SwiftSoup ///https://github.com/scinfu/SwiftSoup
/// HTML코드가 제거된 LongDescription을 받아서 Fragrance Family, Scent Type, Key Notes로 가공해줌.
func dataParsing(longDesc: String) -> (String, String, [String]) {
var string: String = ""
do {
let doc: Document = try SwiftSoup.parse(longDesc)
string = try doc.text()
} catch Exception.Error(_, let message) {
print(message)
} catch {
print("error")
}
//MARK: - Step1. 띄어쓰기 제거
/// string을 좀 더 편하게 가공하기 위해 띄어쓰기를 제거함
var step1 = ""
for i in string {
if i != " " {
step1.append(i)
}
}
//MARK: - Step2. 문자열을 Fragrance Family, Scent Type, Key Notes로 나누기
var step2 = ""
var tempFragranceFamily = ""
var tempScentType = ""
var tempKeyNotes = ""
/// HTML코드와 띄어쓰기가 제거된 step2: String을 갖고
/// Fragrance Family, Scent Type, Key Notes로 가공함
/// 조건문을 통해 아래의 형태로 만들어준다
///[tempFragranceFamily]: FreshScentT
///[tempScentType]: FreshCitrus&FruitsKeyNot
///[tempKeyNotes]: Citron,Jasmine,TeakwoodFragra
for (idx, char) in step1.enumerated() {
if step2.contains("ScentType:") {
if !step2.contains("KeyNot") {
tempScentType.append(char)
}
}
if step2.contains("KeyNotes:") {
if !step2.contains("Fragra") && !step2.contains("About:") {
tempKeyNotes.append(char)
}
}
if idx > 15 {
step2.append(char)
if !step2.contains("ScentTy") {
tempFragranceFamily.append(char)
}
}
}
///[tempFragranceFamily]: FreshScentT
///[tempScentType]: FreshCitrus&FruitsKeyNot
///[tempKeyNotes]: Citron,Jasmine,TeakwoodFragra
/// 각 문자열의 뒤의 글자들을 지워준다. (뒤의 남는 글자를 6개로 맞춤)
for _ in 0...5 {
if !tempFragranceFamily.isEmpty {
tempFragranceFamily.removeLast()
}
if !tempKeyNotes.isEmpty {
tempKeyNotes.removeLast()
}
if !tempScentType.isEmpty {
tempScentType.removeLast()
}
}
let fragranceFamily = tempFragranceFamily
//MARK: - Step3. "ScentType" 띄어쓰기 및 "KeyNotes" 배열로 변환
/// [tempScentType]: FreshCitrus&Fruits -> Fresh Citrus & Fruits
/// [tempKeyNotes]: Citron,Jasmine,Teakwood -> [Citron,Jasmine,Teakwood]
var scentType = ""
var keyNotes = [String]()
for (idx, char) in tempScentType.enumerated() {
if char == "&" {
scentType.append(" ")
} else if idx > 0 && char.isUppercase {
scentType.append(" ")
}
scentType.append(char)
}
var note = ""
for (idx, char) in tempKeyNotes.enumerated() {
if char != "," {
note.append(char)
} else {
keyNotes.append(note)
note = ""
}
if idx == (tempKeyNotes.count - 1) {
keyNotes.append(note)
}
}
return (fragranceFamily, scentType, keyNotes)
}
본론으로 와서,
fetchList함수는 말그대로 향수의 리스트를 호출하는 것에 반해,
fetchDetail함수는 향수 하나의 디테일 데이터를 호출하는 것이다.
그래서 어떻게 합쳐줄까 고민을 했고,
fetchList에서 호출한 후, 호출한 데이터 list(배열)를 for-in문을 돌려주어 fetchDetail 함수를 실행시킨 후
Perfume 구조체의 객체를 만들어주기로 했다.
fetchList 통해 향수 list 호출 -> list(배열)을 for-in문을 돌려 list item(product) 하나하나 fetchDetail로 호출 -> fetchDetail 안에서 item + detail(파싱해준 데이터) 로 perfume 객체 만들어서 perfumes 프로퍼티에 저장
하지만 이 과정에서 문제가 발생했다.
fetchList와 fetchDetail 둘다 네트워크 통신이 필요하다보니 시간의 지연이 생겨 코드가 제대로 돌아가지 않았다.
그래서 async / await 를 통해 비동기 처리를 해주었다.
동시성(Concurrency)이란?
동시성을 설명하기 위해 비동기와 병렬의 개념을 간단하게 알고 넘어가자
비동기(asynchronous): 한 스레드에서 일련의 일을 순서대로 처리하다가 일시중지하고 다른 일을 처리하고 다시 원래 하던 일로 복귀 할 수 있다.
병렬(parallel): 여러 스레드에서 여러개의 일련의 일을 나누어서 처리한다.
하지만 이렇게만 설명하면 이해가 잘 가지 않는다. 예시를 들어보자!
알바생이 3명인 카페가 있다.
음료주문이 3잔 들어왔다.
한 명이 1잔 만들고 2번째 잔 만들고 3번째 잔을 순서대로 만든다.
→ 동기는 오래걸리지만 단순.
한 명이 커피를 데우다가 도중에 아이스티를 만들고 다데워진 커피를 마저 만들고 3번째 잔을 만든다.
→ 비동기는 하나의 작업을 하던 중 기다려야할 일이 있으면 다른 작업을 수행할 수 있다.
세 명이 한잔씩 만든다
→ 병렬
🔥 그래서 동시성이란?
일을 좀 더 빠르게, 효율적으로 처리할 수 있도록 비동기와 병렬을 통해 동시에 처리하는 것을 말한다!!!!
동시성 (Concurrency) - Swift
한 동시성 도메인에서 다른 동시성 도메인으로 공유될 수 있는 타입을 전송 가능 타입 (sendable type) 이라고 합니다. 예를 들어, 액터 메서드로 호출될 때 인수로 전달되거나 작업의 결과로 반환될
bbiguduk.gitbook.io
본론으로 돌아와서 코드에 비동기처리를 해주려고 한다.
기존의 URLSession의 dataTask 메소드는 completion handler나 delegates 패턴을 이용하여 비동기처리를 해줬다.
async/await를 적용하면 보다 쉽고 단순하게 data 메소드를 사용하여 비동기 처리를 할 수있다.
아래를 보시면 API를 호출하는 두개의 함수를 Async/await를 이용하여 비동기 처리해주었다.
fetchList 하나만 실행하면 먼저 List를 호출해준 뒤 거기서 사용한 배열을 이용하여fetchDetail이 실행되면서 Detail을 호출하여 Perfume 객체들의 배열을 만들어준다.
FetchList(향수 리스트) + Detail(세부 정보) = Perfume
class APIStore: ObservableObject {
var perfumes: [Perfume] = []
let headers = [
"X-RapidAPI-Key": "Personal Key를 입력하세요",
"X-RapidAPI-Host": "sephora.p.rapidapi.com"
]
func fetchDetail(product: Product) async throws {
let request = NSMutableURLRequest(url: NSURL(string: "https://sephora.p.rapidapi.com/products/detail?productId=\(product.productId)&preferedSku=2210607")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
var longDescription = ""
var shortDescription = ""
let (data, response) = try await URLSession.shared.data(for: request as URLRequest)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { return }
do{
let detail = try JSONDecoder().decode(Detail.self, from: data)
longDescription = detail.longDescription
shortDescription = detail.quickLookDescription
let (fragranceFamily, scentType, keyNotes) = dataParsing(longDesc: longDescription)
let perfume = Perfume(brandName: product.brandName, displayName: product.displayName, productId: product.productId, image450: product.image450, shortDescription: shortDescription, fragranceFamily: fragranceFamily, scentType: scentType, keyNotes: keyNotes)
self.perfumes.append(perfume)
} catch(let err) {
print(err.localizedDescription)
}
}
func fetchList(page: Int) async throws {
let request = NSMutableURLRequest(url: NSURL(string: "https://sephora.p.rapidapi.com/products/list?categoryId=cat60148&pageSize=60¤tPage=\(page)")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let (data, response) = try await URLSession.shared.data(for: request as URLRequest)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {return}
do {
let list = try JSONDecoder().decode(FetchList.self, from: data)
print(list.products)
Task {
for product in list.products {
try await self.fetchDetail(product: product)
}
try await FirebaseStore().uploadAll(perfumes: self.perfumes)
}
}
catch(let err) {
print(err.localizedDescription)
}
}
}
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
}
struct Perfume: Codable {
let brandName: String
let displayName: String
let productId: String
let image450: String
let shortDescription: String
let fragranceFamily: String
let scentType: String
let keyNotes: [String]
}
struct Detail: Codable {
let quickLookDescription: String
let longDescription: String
}
지금까지 API를 호출해보고, 거기서 데이터를 이용하여 한번 더 호출, 필요한 데이터를 파싱하여 하나의 객체를 만들어보았다.
위 코드를 자세히 보면, fetchList함수의 do / catch문 안에 Firebase 관련 코드가 들어가 있는 것을 볼 수 있다.
4️⃣ Firebase 올리기
이제부터는 데이터를 Firebase에 서버를 만들어서 올려보려고 한다.
Firebase란?
앱을 만들기 위해 필요한 서비스를 제공하는 구글의 플랫폼이다.
다양한 서비스 중 나는 DB, Storage, Auth 기능을 사용해보았다.
특별한 서버의 설계 없이 일련의 과정으로 간단하게 서버를 구성할 수 있다.
여기서는 DB의 역할로 사용할 것이다.
위 과정에서 perfumes라는 Perfume 객체의 배열을 만들었다.
그것을 firebase 코드를 적용하여 서버에 저장할 수 있다.
Firebase 관련 코드이다.
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class FirebaseStore {
let db = Firestore.firestore()
func uploadPerfumeData(perfume: Perfume) throws {
try db.collection("Perfume").document(perfume.productId).setData(from: perfume)
}
func uploadAll(perfumes: [Perfume]) async throws {
for perfume in perfumes {
try self.uploadPerfumeData(perfume: perfume)
}
}
}
이렇게 해서 공부한 내용들을 기록해보았다. 생각보다 시간이 오래걸렸다.
게시글을 작성하면서 느낀 것은 코드를 썼지만 관련 내용에 대해 모르는 것도 있고 더 알고 싶어진 부분도 있다.
또 기록하고 싶은 것이 생기면 기록해 보겠다.
혹시 궁금하거나 잘못된 내용 있다면 언제든지 환영합니닷!
'개발 공부' 카테고리의 다른 글
앱스토어 리젝 대응(크래시 로그 분석방법) (0) | 2024.08.12 |
---|---|
애플 개발자 등록하기(feat: apple developer program 등록을 완료할 수 없습니다) (0) | 2024.03.15 |
ChatGPT 4.0을 무료로 사용가능한 AI 툴 [뤼튼: wrtn] (0) | 2023.07.06 |
Github 레포에 올린 민감한 정보 예방하기 + 제거하기(.gitignore + BFG Repo-Cleaner 사용법) (0) | 2023.06.30 |
API호출 후 데이터 가공해서 서버에 저장하기(1) (SwiftUI) (0) | 2023.03.31 |