ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Pagination Strategy
    iOS 2025. 11. 22. 08:33

    안녕하세요. 그린입니다 🍏
    이번 포스팅은 개발에서 페이지네이션 구현 방법론에 대해 깊이 있게 알아보겠습니다 🙋🏻


    Pagination Deep Dive

    개발을 하다 보면 한 번쯤 마주치는 상황이 있죠.

    let posts = try await api.fetchAllPosts() // 10,000개의 게시글...
    tableView.reloadData() // 앱이 버벅거림 😱
    

     

    수천, 수만 개의 데이터를 한 번에 로드하려다가 앱이 느려지거나 메모리가 터지는 경우 말이에요 😅

    이를 해결하기 위해서는 페이지네이션(Pagination) 전략을 취해야합니다.

     


    Why Pagination Matters?

    페이지네이션의 가장 큰 이유는 바로 성능과 사용자 경험이에요.

    // 😱 10,000개 게시글 → 수 초 대기, 메모리 수백 MB
    let allData = api.fetchAll()
    
    // 😊 20개씩 나눠서 → 즉시 표시, 메모리 효율적
    let pageData = api.fetchPage(size: 20)

     

    사용자는 실제로 화면에 보이는 몇 개의 아이템만 소비합니다.
    필요한 만큼만 로드하는 것이 핵심이죠.

    하지만 이 단순해 보이는 개념 뒤에는 다양한 구현 방식과 트레이드오프가 숨어있어요.

     


    페이지네이션의 3가지 방식

    페이지네이션은 크게 Offset-basedCursor-basedKeyset 세 가지로 나뉩니다.

     

    1️⃣ Offset-based Pagination

    페이지 번호로 데이터를 나누는 가장 흔한 방식이에요.
    struct OffsetRequest {
        let page: Int   // 페이지 번호
        let size: Int   // 페이지당 아이템 수
    }
    
    // GET /api/posts?page=2&size=20
    // → 21번째부터 40번째 게시글
    

     

    SQL로 보면 이렇게 동작합니다.

    SELECT * FROM posts 
    ORDER BY created_at DESC 
    LIMIT 20 OFFSET 40;
    

     

    장점

    • 구현이 간단
    • 특정 페이지로 바로 이동 가능 (1, 2, 3... 페이지 네비게이션)
    • 총 페이지 수 계산 용이

    단점

    • 데이터 중복/누락 문제 (새 데이터 추가 시)
    • 대용량 데이터에서 성능 저하 (OFFSET 10000은 10,020개 행을 스캔)
    • 실시간 피드에 부적합

    언제 사용?

    • 검색 결과 페이지
    • 관리자 페이지
    • 데이터 변화가 적은 경우

    2️⃣ Cursor-based Pagination

    마지막 아이템의 커서를 기준으로 다음 데이터를 가져오는 방식이에요.
    struct CursorRequest {
        let cursor: String?  // 마지막 아이템 커서
        let size: Int
    }
    
    struct CursorResponse<T> {
        let items: [T]
        let nextCursor: String?
        let hasMore: Bool
    }
    
    // GET /api/posts?cursor=eyJpZCI6MTIzNH0&size=20
    

     

    SQL로 보면

    -- 커서 = (created_at, id)
    SELECT * FROM posts 
    WHERE (created_at, id) < ('2024-01-15 10:00:00', 1234)
    ORDER BY created_at DESC, id DESC
    LIMIT 20;
    

     

    장점

    • 데이터 중복/누락 없음
    • 대용량 데이터에서도 빠름 (인덱스 활용)
    • 실시간 피드에 최적
    • 무한 스크롤에 완벽

    단점

    • 특정 페이지 점프 불가
    • 총 페이지 수 알기 어려움
    • 구현이 약간 복잡

    언제 사용?

    • SNS 피드 (Twitter, Instagram)
    • 채팅 메시지 목록
    • 실시간 알림
    • 무한 스크롤

    3️⃣ Keyset Pagination

    Cursor의 변형으로, 정렬 기준 값을 직접 노출하는 방식이에요.
    // GET /api/posts?lastCreatedAt=2024-01-15T10:00:00Z&lastId=1234&size=20
    
    struct KeysetRequest {
        let lastCreatedAt: Date?
        let lastId: Int?
        let size: Int
    }
    

     

    Cursor vs Keyset

    • Cursor: 불투명한 토큰으로 인코딩 (보안 UP)
    • Keyset: 실제 값 노출 (디버깅 용이)

    iOS 구현 예시

    SwiftUI + Cursor Pagination

    @MainActor
    class PostListViewModel: ObservableObject {
        @Published var posts: [Post] = []
        @Published var isLoading = false
        @Published var hasMorePages = true
        
        private var nextCursor: String?
        
        func loadMore() async {
            guard !isLoading && hasMorePages else { return }
            isLoading = true
            
            do {
                let response = try await api.fetchPosts(
                    cursor: nextCursor,
                    size: 20
                )
                
                posts.append(contentsOf: response.items)
                nextCursor = response.nextCursor
                hasMorePages = response.hasMore
            } catch {
                // 에러 처리
            }
            
            isLoading = false
        }
    }
    
    struct PostListView: View {
        @StateObject private var viewModel = PostListViewModel()
        
        var body: some View {
            List {
                ForEach(viewModel.posts) { post in
                    PostRowView(post: post)
                        .onAppear {
                            if post == viewModel.posts.last {
                                Task { await viewModel.loadMore() }
                            }
                        }
                }
                
                if viewModel.isLoading {
                    ProgressView()
                }
            }
            .refreshable {
                await viewModel.refresh()
            }
        }
    }

     

     


    Performance Tips

    1️⃣ 프리로딩 (Pre-loading)

    // 하단 근처에 도달하면 미리 로드
    if offsetY > contentHeight - scrollViewHeight - 200 {
        loadMore()
    }
    

     

    2️⃣ 중복 요청 방지

    private var currentTask: Task<Void, Never>?
    
    func loadMore() {
        guard currentTask == nil else { return }
        
        currentTask = Task {
            defer { currentTask = nil }
            // 로드 로직
        }
    }
    

     

    3️⃣ 메모리 관리

    // 오래된 데이터는 주기적으로 제거
    if posts.count > 1000 {
        posts.removeFirst(500)
    }
    

     


    Conclusion

    페이지네이션은 앱 성능의 핵심 요소입니다.

    핵심을 뽑아볼까요?

    • Offset: 간단하지만 대용량/실시간에 부적합
    • Cursor: 성능 좋고 실시간에 최적, 대부분의 모바일 앱에 추천
    • Keyset: Cursor의 변형, 디버깅 용이

    제 경험상 대부분의 모바일 앱에서는 Cursor-based Pagination이 가장 좋은 선택이었어요.
    무한 스크롤, 실시간 업데이트, 그리고 안정적인 성능을 모두 제공하기 때문이죠 😃

    그치만 매번 프로젝트마다 상황이 다를 수 있습니다.
    그렇기에 프로젝트의 특성에 맞는 방식을 선택해보세요!

     


    References

     

    API calls | Spotify for Developers

    API calls The Spotify Web API is a restful API with different endpoints which return JSON metadata about music artists, albums, and tracks, directly from the Spotify Data Catalogue. Base URL The base address of Web API is https://api.spotify.com. Authoriza

    developer.spotify.com

     

    Pagination | Slack Developer Docs

    Throughout the Slack platform, you'll encounter collections of things. Lists of users. Arrays of channels. A pride of lion emoji.

    docs.slack.dev

     

    Best Practices for Designing a Pragmatic RESTful API

    Your data model has started to stabilize and you're in a position to create a public API for your web app. You realize it's hard to make significant changes to your API once it's released and want to get as much right as possible up front. Now, the interne

    www.vinaysahni.com

    'iOS' 카테고리의 다른 글

    WKURLSchemeHandler  (0) 2025.11.09
    Mach-O 파일 구조 분석해보며 최적화 해보기  (2) 2025.09.13
    App Battery Drain  (5) 2025.08.09
    Diet for iOS App size (feat. App Thinning)  (5) 2025.08.02
    Meet PaperKit (feat. WWDC 2025)  (0) 2025.06.25
Designed by Tistory.