🎯 Trouble Shooting
[ 1. KingFisher를 이용하여 API 호출수 줄이기 ]
API 호출수를 줄이기 위한 방법 찾기
현재
Google Places API -> 주변 검색 (신규) API를 사용해서 가게 정보를 받아옴 ->
받아온 정보 중 PhotoNames을 장소 사진(신규) API를 사용해서 Photo Uri를 받아옴
문제
다른 카테고리를 눌렀다가 돌아오거나, 같은 가게가 다시 검색되었을 때 매번 API 호출을 통해 가게 이미지를 받아와야 함
해결 방법
KingFisher의 cache Dictionary를 사용해서 photoNames를 Key 값으로 하여, 이전에 불러왔던 photoNames인 경우
추가로 API 호출을 하지 않고, 캐시에 있는 photoUri를 사용해서 KingFisher를 통해 이미지 넣기
추가 구현 부분
userDefault를 사용해서 cache Dictionary를 저장 & 불러오기
👨🏻💻 오늘의 작업
[ 1. KingFisher를 이용하여 API 호출수 줄이기 ]
1.1 Reactor - mutate 부분 수정
func mutate(action: Action) -> Observable<Mutation> {
case .viewDidLoad:
// 네트워크 통신 하고 zip으로 병합
let (centerLat, centerLon) = getCenterLocation()
// 가게 정보와 이미지까지 비동기로 네트워크 통신
let firstRequest = fetchStoreInfosWithImages(textQuery: "닭발", centerLat: centerLat, centerLon: centerLon)
let secondRequest = fetchStoreInfosWithImages(textQuery: "짜장면", centerLat: centerLat, centerLon: centerLon)
return Observable.zip(firstRequest, secondRequest)
.map { first, second in
let merged = (first + second).sorted { $0.rating > $1.rating }
print("첫번째 가게 수 : \(first.count)")
print("두번째 가게 수 : \(second.count)")
print("총 표시 가게 수 : \(merged.count)")
return [StoreSection(items: merged)]
}
.map { .setStore($0) }
}
mutate 안에서는 googleMap Place의 searchText API를 2번 호출하여 각 음식 키워드에 대한 가게를 합친다.
검색 위치에 대한 위도와 경도 값은 userDefault에서 불러오며, 저장된 위도 경도가 없는 경우에는
기본값인 강남역의 위도, 경도로 사용한다.
1.2 Reactor - fetchStoreInfosWithImages 함수 작성
func fetchStoreInfosWithImages(textQuery: String, centerLat: Double, centerLon: Double) -> Observable<[StoreInfo]> {
return NetworkManager.shared.fetchPlacesWithCircle(
textQuery: textQuery,
centerLat: centerLat,
centerLon: centerLon)
.flatMap { places in
Observable.from(places)
.flatMap { place -> Observable<StoreInfo?> in
let placeName = place.photos?.first?.name ?? ""
let splitPlaceName = placeName.split(separator: "/")
let photoName = place.photos?.first?.name ?? ""
// Index out of range error 방지
let cacheKey: String
if splitPlaceName.count >= 2 {
cacheKey = "\(splitPlaceName[0])/\(splitPlaceName[1])"
} else if splitPlaceName.count == 1 {
cacheKey = String(splitPlaceName[0])
} else {
cacheKey = ""
}
return self.fetchPhotoUriWithCache(placeName: cacheKey, photoName: photoName)
.asObservable()
.map { photoUri in
return self.convertToStoreInfo(place: place, photoUri: photoUri)
}
}
.toArray()
.map { $0.compactMap { $0 } }
}
.asObservable()
}
fetchStoreInfosWithImages 함수는 textQuery(음식 키워드), centerLat(위도), centerLon(경도)를 가지고
NetworkManager에 있는 fetchPlaceWithCircle을 통해 GoogleMap Place searchText를 호출한다.
그 후 받아온 가게 정보들인 places 데이터를 가지고 flatMap을 사용해서 Observable<StoreInfo?>로 변경해 주는데
여기서 하는 작업은 가게 이름과 가게 사진 이름을 가지고 fetchPhotoUriWithCache 함수를 수행해서 받아온
가게 이미지의 Uri를 palceName과 함께 캐시에 저장한다.
그 후 테이블 뷰에서 사용할 가게 정보 데이터 배열을 return 한다.
1.3 Reactor - fetchPhotoUriWithCache 함수 작성
var photoUriCache: [String: String] = [:] // 캐시 사용을 위한 프로퍼티
// photoUri를 캐싱하여 반환하는 함수
func fetchPhotoUriWithCache(placeName: String, photoName: String) -> Single<String> {
let cacheKey = placeName
if let cachedUri = photoUriCache[cacheKey] {
return .just(cachedUri)
}
// photoName이 없으면 빈 문자열 반환
guard !photoName.isEmpty else { return .just("") }
// 캐시에 없으면 네트워크 요청
return NetworkManager.shared.fetchImage(mediaName: photoName)
.map { googleUri in
print("네트워크 통신")
let photoUri = googleUri.photoUri
self.photoUriCache[cacheKey] = photoUri
UserDefaultsManager.shared.savePhotoUriCache(self.photoUriCache)
return photoUri
}
}
Reactor 파일 내의 캐시 사용을 위해 photoUriCache라는 프로퍼티를 선언했다.
fetchPhotoUriWithCache 함수는 placeName과 photoName을 가지고 이미지의 Uri를 가져오는 함수이다.
cacheKey를 placeName으로 설정하여 이전에 같은 가게 이름으로 이미지 Uri를 불러왔던 캐시가 있으면
API 호출을 하지 않고 캐시에 저장되어 있는 가게 이미지 Uri를 return 한다.
만약 구글 API를 통해 받아온 가게 데이터들 중에 placeName이 없는 경우 (사진이 없는 경우)
photoUri는 빈 문자열이 된다. (빈 문자열인 photoUri는 TabelViewCell 부분에서 기본 이미지로 설정)
placeName이 있고, 캐시에 저장되지 않았던 placeName이면,
NetworkManager의 fetchImage를 통해 photoUri를 받아온 뒤
cacheKey에 이름과 이미지 Uri를 저장한다.
사용자가 앱을 종료해도 캐시를 저장할 수 있게 userDefault를 사용해서 캐시를 저장한다.
그리고 photoUri를 return한다.
1.4 TableViewCell - configureView
func configureView(with storeInfo: StoreInfo) {
// photoNames이 빈문자열이면 DefaultImage 표시
if storeInfo.photosNames != "" {
if let url = URL(string: storeInfo.photosNames) {
storeImageView.kf.setImage(with: url)
}
} else {
storeImageView.image = UIImage(named: "DefaultImage")
}
}
TableViewCell 코드 중 configureView 함수에서는 받아온 storeInfo 데이터를 사용해서
테이블 뷰에 가게 정보와 가게 이미지를 표시한다.
만약 받아온 데이터인 storeInfo의 photoName이 빈 문자열이 아니라면
KingFisher를 사용해서 이미지를 바인딩하고,
storeInfo의 photoName이 빈 문자열이라면
Asset에 있는 DefaultImage를 넣어준다.
1.5 userDefault에 저장 및 불러오기
저장 및 불러오기 코드
func savePhotoUriCache(_ cache: [String: String]) {
defaults.set(cache, forKey: Keys.photoUriCacheKey.rawValue)
}
func readPhotoUriCache() -> [String: String]? {
return defaults.object(forKey: Keys.photoUriCacheKey.rawValue) as? [String: String]
}
수정된 코드
case .viewDidLoad:
// 네트워크 통신 하고 zip으로 병합
let (centerLat, centerLon) = getCenterLocation()
// userDefault에서 캐시 딕셔너리 불러오기
if let savedCache = UserDeafaultsManager.shared.readPhotoUriCache() {
photoUriCache = savedCache
} else {
photoUriCache = [:]
}
// 가게 정보와 이미지까지 비동기로 네트워크 통신
let firstRequest = fetchStoreInfosWithImages(textQuery: "국밥", centerLat: centerLat, centerLon: centerLon)
let secondRequest = fetchStoreInfosWithImages(textQuery: "갈비", centerLat: centerLat, centerLon: centerLon)
return Observable.zip(firstRequest, secondRequest)
.map { first, second in
let merged = (first + second).sorted { $0.rating > $1.rating }
print("첫번째 가게 수 : \(first.count)")
print("두번째 가게 수 : \(second.count)")
print("총 표시 가게 수 : \(merged.count)")
return [StoreSection(items: merged)]
}
.map { .setStore($0) }
viewDidLoad 시점에서 userDefault에 저장된 캐시키를 불러온 뒤 프로퍼티로 저장된 photoUriCache에 넣어주고
저장된 캐시가 없다면 빈 배열을 넣어준다.
func fetchPhotoUriWithCache(placeName: String, photoName: String) -> Single<String> {
let cacheKey = placeName
if let cachedUri = photoUriCache[cacheKey] {
return .just(cachedUri)
}
// photoName이 없으면 빈 문자열 반환
guard !photoName.isEmpty else { return .just("") }
// 캐시에 없으면 네트워크 요청
return NetworkManager.shared.fetchImage(mediaName: photoName)
.map { googleUri in
print("네트워크 통신")
let photoUri = googleUri.photoUri
self.photoUriCache[cacheKey] = photoUri
UserDeafaultsManager.shared.savePhotoUriCache(self.photoUriCache)
return photoUri
}
}
캐시가 있는지 확인하고 없으면 네트워크 요청하는 부분에서
API 통신 후 Uri를 받아오면 userDefault를 사용해서 캐시를 저장한다.
'스파르타 코딩 클럽 - iOS 스타터 6기 > 본 캠프' 카테고리의 다른 글
68. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #10 (0) | 2025.06.19 |
---|---|
67. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #9 (5) | 2025.06.18 |
65. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #7 (1) | 2025.06.16 |
64. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #6 (1) | 2025.06.13 |
63. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #5 (0) | 2025.06.12 |