Pagination
Pagination(페이지 매기기) 또는 Paging(페이징) 이란 문서를 전자 페이지 또는 인쇄된 페이지 등 개별 페이지로 나누는 프로세스이다.
우리가 사용하는 웹페이지에서 쉽게 Pagination이 구현한 것을 볼 수 있다. 아래 더보기 같은 버튼이 예시 중 하나이다.

이번에는 UIKit과 SwiftUI에서 사용하는 방식을 포스팅 하려 한다. 내가 이번에는 Github API를 사용하여 구현해볼 것이다.
아래 API 문서를 참고하자
https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#list-users
REST API endpoints for users - GitHub Docs
Status: 200 { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_
docs.github.com
avatar_url(프로필 사진), login(닉네임), html_url(Github 주소)를 표시하기 위한 간단하게 UI를 구현하였다.

이미지를 가져오기 위한 이미지 캐싱 클래스 ImageLoader 를 추가했다. ImageLoader 내용은 아래 포스팅 참고
[UIKit] TableView에서 URL 이미지 캐싱 및 로딩 최적화 - ImageLoader + ImageCache 구현하기
UITableView나 UICollectionView에서 cell에 이미지를 넣을 때 url로 다운로드하여 이미지를 적용하면 잘못된 셀에 이미지가 추가되거나 앱이 백그라운드로 갔다오면 이미지를 전부 재로딩을 해야하는 문
kimkhuna99.tistory.com

일단 이미지는 나온다. 이제 페이징을 구현해보자. ViewModel 에 아래와 같이 구현했다.
import Foundation
import Combine
import UIKit
@MainActor
final class GithubViewModel: ObservableObject {
@Published private(set) var users: [User] = []
@Published private(set) var isLoading: Bool = false
@Published private(set) var isPaging: Bool = false
@Published private(set) var error: Error?
private let service: GithubService
private var nextSince: Int? = nil // 마지막으로 받은 id
private var hasMore = true
private var currentTask: Task<Void, Never>?
private let imageLoader = ImageLoader()
init(service: GithubService = GithubService()) {
self.service = service
}
func loadInitial(perPage: Int = 30) {
guard !isLoading else { return }
currentTask?.cancel()
isLoading = true
error = nil
nextSince = nil
currentTask = Task { [weak self] in
guard let self = self else { return }
do {
let first = try await service.fetchUsers(since: nil, perPage: perPage)
self.users = first
self.nextSince = first.last?.id
self.hasMore = !first.isEmpty
} catch {
self.error = error
}
self.isLoading = false
}
}
func load(currentIndex: Int, perPage: Int = 30, prefetchThreshold: Int = 5) {
guard hasMore, !isPaging, !isLoading else { return }
guard currentIndex >= users.count - prefetchThreshold else { return }
isPaging = true
currentTask?.cancel()
currentTask = Task { [weak self] in
guard let self = self else { return }
do {
let more = try await service.fetchUsers(since: self.nextSince, perPage: perPage)
// 중복 방지
let existing = Set(self.users.map(\.id))
let filtered = more.filter { !existing.contains($0.id) }
self.users.append(contentsOf: filtered)
self.nextSince = self.users.last?.id
self.hasMore = !filtered.isEmpty
} catch {
self.error = error
}
isPaging = false
}
}
func refresh(perPage: Int = 30) {
loadInitial(perPage: perPage)
}
func cancel() {
currentTask?.cancel()
}
// MARK: - Image
func imagePublisher(for user: User) -> AnyPublisher<UIImage, Never> {
guard let url = URL(string: user.avatar_url) else {
let fallback = UIImage(systemName: "person") ?? UIImage()
return Just(fallback).eraseToAnyPublisher()
}
return imageLoader.loadImage(from: url)
.map { $0 ?? UIImage(systemName: "person") ?? UIImage() }
.eraseToAnyPublisher()
}
}
처음 loadInitial()로 로드 후에 Paging 하게 되면 load()를 사용한다.
extension GithubViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let maxIndex = indexPaths.map(\.row).max() ?? 0
viewModel.load(currentIndex: maxIndex, perPage: 30)
}
}
ViewController 에서는 UITableViewDataSourcePrefetching 을 사용하여 스크롤을 추적한다.
최종적으로 결과는 아래와 같이 스크롤 시 UI가 업데이트되는 것을 볼 수 있다.

자세한 코드는 아래 Github에 올려 놓았다.
GitHub - kyeonghunkim0/Pagination_UIKit
Contribute to kyeonghunkim0/Pagination_UIKit development by creating an account on GitHub.
github.com
'Develop > UIKit' 카테고리의 다른 글
| [UIKit]UIModalTransitionStyle (0) | 2026.01.11 |
|---|---|
| [AutoLayout] Compression Resistance Priority (0) | 2025.09.14 |
| [UIKit] TableView에서 URL 이미지 캐싱 및 로딩 최적화 - ImageLoader + ImageCache 구현하기 (3) | 2025.08.10 |
| [AutoLayout] Hugging Priority (0) | 2025.07.30 |
| [UIKit] UIResponder (0) | 2025.01.29 |