대량의 데이터를 작은 단위(페이지, 스크롤 등)로 나누어서 로드하는 기술이다. 이로서 기대할 수 있는 장점은
예제) limit=20을 유지하며 모든 포켓몬의 이미지가 로드될 때 까지 스크롤이 계속 되도록
import UIKit
import RxSwift
class MainViewModel {
// DisposeBag은 RxSwift에서 메모리 누수 방지하는데 사용
// 구독한 Observable들을 여기에 넣어두면 뷰모델이 해제될 때 자동으로 구독 해제
private let disposeBag = DisposeBag()
// 이미지 캐싱용 NSCache. 네트워크 요청 줄이고 성능 향상시키는데 중요
// NSCache는 메모리 부족할 때 자동으로 일부 객체 제거해서 메모리 관리에 유용
private var imageCache = NSCache<NSString, UIImage>()
// BehaviorSubject는 RxSwift의 Subject종류. 현재 값을 가지고 있고. 구독 시 최신 값 바로 방출
// 여기서는 포켓몬 목록을 관리하는데 사용. UI에서 이걸 구독해서 데이터 변경 감지하고 화면 업데이트
let thumbnailImageSubject = BehaviorSubject(value: [Pokemon]())
// 페이지네이션 구현을 위한 offset. API 요청 시 어디서부터 데이터 가져올지 지정
// 20개씩 가져오니까 0, 20, 40 ... 이런 식으로 증가
private var offset = 0
// 중복 요청 방지용 플래그. 데이터 로딩 중에 true로 설정해서 추가 요청 막음
// 네트워크 요청이 중복되면 불필요한 리소스 낭비되고 버그 발생할 수 있어서 그렇다
private var isLoading = false
// 포켓몬 데이터 가져오는 함수. 페이지네이션 구현 핵심
func fetchThumbnail() {
// 이미 로딩 중이면 함수 종료. 중복 요청 방지
guard !isLoading else { return }
isLoading = true
// API 요청 URL 생성. offset 이용해 페이지네이션 구현
// string interpolation 사용해서 offset 값 URL에 포함. limit=20은 한 번에 20개 가져온다는 의미
let urlString = "https://pokeapi.co/api/v2/pokemon?limit=20&offset=\(offset)"
guard let url = URL(string: urlString) else { return }
// NetworkManager를 통해 데이터 fetch. Single은 RxSwift의 Observable 타입
// 단일 이벤트만 발생시키고 종료되는 Observable. 네트워크 요청에 적합
NetworkManager.shared.fetch(url: url)
.subscribe(onSuccess: { [weak self] (response: PokemonListResponse) in
// weak self 사용해서 강한 참조 순환 방지.
guard let self = self else { return }
// 현재 리스트에 새로운 데이터 추가. try?는 에러 무시하고 nil 반환
let currentList = try? self.thumbnailImageSubject.value()
let newList = (currentList ?? []) + response.results
// 업데이트된 리스트 발행. onNext로 새 값 방출
self.thumbnailImageSubject.onNext(newList)
// 오프셋 업데이트. 다음 페이지 준비
self.offset += response.results.count
// 로딩 상태 해제. 다음 요청 가능하게
self.isLoading = false
}, onFailure: { [weak self] error in
// 에러 발생 시 처리. Subject에 에러 전달하고 로딩 상태 해제
self?.thumbnailImageSubject.onError(error)
self?.isLoading = false
}).disposed(by: disposeBag) // 구독 해제 관리
}
// 추가 데이터 로드 필요한지 확인하는 함수
func loadMore(currentIndex: Int) {
// 현재 인덱스가 offset임계치 도달하면 추가 데이터 로드하는데
// offset-5처럼 미리 로드 시작하도록 UX 개선
let thresholdIndex = self.offset - 5
if currentIndex == thresholdIndex {
fetchThumbnail()
}
}
// 포켓몬 이미지 가져오는 함수. 캐싱 구현으로 효율 높이기
func getImage(for pokemon: Pokemon) -> Observable<UIImage?> {
let id = pokemon.id
// 이미지 URL 생성. 포켓몬 API의 이미지 URL 형식 따름
let urlString = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(id).png"
// 캐시에 이미지 있으면 즉시 반환. 네트워크 요청 없이 빠르게 이미지 제공
if let cachedImage = imageCache.object(forKey: NSString(string: urlString)) {
return Observable.just(cachedImage)
}
// URL 생성 실패시 nil 반환. 옵셔널 바인딩으로 안전성 확보
guard let url = URL(string: urlString) else {
return Observable.just(nil)
}
// Observable 생성해서 비동기적으로 이미지 로드
return Observable.create { [weak self] observer in
// URLSession으로 네트워크 요청. 비동기적으로 이미지 다운로드
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 옵셔널 바인딩으로 안전하게 언래핑. self, data, image 모두 확인
guard let self = self, let data = data, let image = UIImage(data: data) else {
observer.onNext(nil)
observer.onCompleted()
return
}
// 다운로드한 이미지 캐시에 저장. 다음 요청 시 빠르게 제공 가능
self.imageCache.setObject(image, forKey: NSString(string: urlString))
observer.onNext(image)
observer.onCompleted()
}
task.resume()
// 구독 취소 시 네트워크 요청도 취소. 리소스 낭비 방지
return Disposables.create {
task.cancel()
}
}
}
}
struct OffsetBasedPagination {
var offset = 0
let limit = 20
mutating func nextPage() -> String {
let urlString = "https://api.example.com/items?offset=\(offset)&limit=\(limit)"
offset += limit
return urlString
}
}
// 장점: 구현 쉬움. 특정 페이지로 쉽게 이동 가능
// 단점: 데이터 추가/삭제 시 불일치 발생 가능. 대량 데이터에서 비효율적
struct CursorBasedPagination {
var nextCursor: String?
mutating func nextPage() -> String {
let urlString = "https://api.example.com/items?cursor=\(nextCursor ?? "")"
// API 호출 후 응답에서 nextCursor 업데이트 필요
return urlString
}
}
// 장점: 데이터 일관성 유지. 대량 데이터에 효율적
// 단점: 특정 페이지로 직접 이동 어려움
// 실제 사용 예시 (커서 기반)
class PaginationViewModel {
private var pagination = CursorBasedPagination()
private let disposeBag = DisposeBag()
func fetchNextPage() {
let urlString = pagination.nextPage()
guard let url = URL(string: urlString) else { return }
NetworkManager.shared.fetch(url: url)
.subscribe(onSuccess: { [weak self] (response: ItemResponse) in
// 데이터 처리
self?.pagination.nextCursor = response.nextCursor
}, onFailure: { error in
// 에러 처리
})
.disposed(by: disposeBag)
}
}
struct TimeBasedPagination {
var lastTimestamp: TimeInterval?
mutating func nextPage() -> String {
let urlString = "https://api.example.com/items?since=\(lastTimestamp ?? 0)"
// API 호출 후 응답에서 lastTimestamp 업데이트 필요
return urlString
}
}
// 장점: 실시간 데이터에 적합. 새로운 항목 쉽게 가져올 수 있음
// 단점: 과거 데이터 탐색 어려울 수 있음
struct KeySetPagination {
var lastId: Int?
var lastValue: String?
mutating func nextPage() -> String {
guard let id = lastId, let value = lastValue else {
return "https://api.example.com/items"
}
return "https://api.example.com/items?last_id=\(id)&last_value=\(value)"
// API 호출 후 응답의 마지막 항목에서 lastId와 lastValue 업데이트 필요
}
}
// 장점: 데이터베이스 성능 좋음. 대량 데이터에 효율적
// 단점: URL 복잡해질 수 있음. 구현 복잡도 높음