Android Academy · 강의

PagingSource와 Pager

페이지를 로드하는 방식을 정의합니다.

레슨 2/413개 단계

PagingSource와 Pager은(는) CoddyKit의 무료 Android Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Android Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

페이지 로드 방식 정의하기

네트워크 API에서 페이지 단위로 데이터를 가져오려면 PagingSource를 작성합니다. Paging 3에 다음 두 가지를 알려 주는 역할입니다.

  • 주어진 키에 해당하는 페이지를 어떻게 로드할까요?
  • 사용자가 새로 고침하면 어디에서 다시 시작해야 할까요?

그런 다음 이를 Pager로 감싸 Flow<PagingData>로 변환합니다.

PagingSource 타입 매개변수

PagingSource<Key, Value>는 두 개의 타입 매개변수를 받습니다.

  • Key - 페이지를 식별합니다. 페이지 번호 API에서는 Int이고, 커서 API에서는 String 토큰입니다.
  • Value - 항목 타입입니다. 예를 들면 Article입니다.
import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource(
    private val api: ArticleApi
) : PagingSource<Int, Article>() {
    // implement load() and getRefreshKey()
}

load() 구현하기

load()는 suspend 함수입니다. 가져올 페이지를 나타내는 params.key를 받고 LoadResult를 반환합니다.

성공하면 항목과 이전 키 및 다음 키를 포함하는 LoadResult.Page를 반환합니다. 키가 null이면 해당 방향에 페이지가 더 없다는 뜻입니다.

override suspend fun load(
    params: LoadParams<Int>
): LoadResult<Int, Article> {
    val page = params.key ?: 1   // first load has a null key
    return try {
        val response = api.getArticles(page = page, size = params.loadSize)
        LoadResult.Page(
            data = response.items,
            prevKey = if (page == 1) null else page - 1,
            nextKey = if (response.items.isEmpty()) null else page + 1
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

prevKey와 nextKey가 중요한 이유

Paging은 사용자가 아래로 스크롤할 때 nextKey를 사용해 다음 페이지를 로드하고, 위로 이동할 때 prevKey를 사용해 이전 페이지를 로드합니다. 목록 중간에서 시작할 때 유용합니다.

nextKey에 null을 반환하면 더 이상 페이지가 없다는 뜻이므로 Paging은 요청을 중지합니다. 이를 잊으면 비어 있는 요청이 무한히 발생할 수 있습니다.

// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1

// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1

getRefreshKey() 구현하기

목록을 새로 고칠 때(당겨서 새로 고침하거나 무효화할 때) Paging은 사용자가 대략 같은 위치에 머물 수 있도록 어느 페이지를 다시 로드해야 하는지 알아야 합니다.

getRefreshKey()는 현재 anchorPosition(뷰포트에 가장 가까운 항목)을 사용해 적절한 키를 선택합니다.

override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
    return state.anchorPosition?.let { anchor ->
        val closestPage = state.closestPageToPosition(anchor)
        closestPage?.prevKey?.plus(1)
            ?: closestPage?.nextKey?.minus(1)
    }
}

오류를 깔끔하게 처리하기

네트워크 호출을 try/catch로 감싸고 실패하면 LoadResult.Error(e)를 반환합니다. Paging은 이를 UI에서 LoadState.Error로 노출하므로 재시도 버튼을 표시할 수 있습니다.

예외가 load() 밖으로 전파되도록 두지 마세요. 예외를 잡아 LoadResult.Error로 변환해야 합니다.

return try {
    val response = api.getArticles(page = page, size = params.loadSize)
    LoadResult.Page(
        data = response.items,
        prevKey = if (page == 1) null else page - 1,
        nextKey = if (response.items.isEmpty()) null else page + 1
    )
} catch (e: IOException) {        // no network
    LoadResult.Error(e)
} catch (e: HttpException) {       // non-2xx response
    LoadResult.Error(e)
}

Pager 만들기

Pager는 PagingConfig를 새로운 PagingSource를 생성하는 팩토리에 연결합니다. .flow 속성은 UI가 수집하는 Flow<PagingData>입니다.

Paging은 새로 고침할 때 소스를 무효화하고 다시 생성하므로, 팩토리 람다는 호출될 때마다 새로운 소스를 만들어야 합니다.

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

class ArticleRepository(private val api: ArticleApi) {
    fun articleStream(): Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 5),
        pagingSourceFactory = { ArticlePagingSource(api) }
    ).flow
}

ViewModel에서 cachedIn 사용하기

PagingData 수집은 한 번만 수행되는 작업이므로 다시 수집하면 로딩이 다시 시작됩니다. 구성 변경에도 유지되고 여러 수집기가 데이터를 공유하도록 하려면 cachedIn을 사용해 viewModelScope에 흐름을 캐시합니다.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn

class ArticleViewModel(
    repo: ArticleRepository
) : ViewModel() {
    val articles = repo.articleStream()
        .cachedIn(viewModelScope)
}

loadSize와 pageSize 비교

load()는 설정한 pageSize를 직접 읽는 것이 아니라 params.loadSize를 읽는다는 점에 유의하세요.

첫 로드에서는 Paging이 더 큰 초기 묶음을 요청할 수 있습니다. 이는 PagingConfig의 initialLoadSize로 제어되며 기본값은 페이지 크기의 3배입니다. API에는 항상 params.loadSize를 전달하여 요청 크기가 Paging이 반환받기를 기대하는 크기와 일치하도록 하세요.

// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)

// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)

커서 기반 API

모든 API가 페이지 번호를 사용하는 것은 아닙니다. 일부 API는 다음 페이지를 가리키는 커서 또는 토큰을 반환합니다. 패턴은 동일하며, Key 타입을 String으로 바꾸고 응답의 토큰을 사용하면 됩니다.

class CursorArticleSource(
    private val api: ArticleApi
) : PagingSource<String, Article>() {
    override suspend fun load(
        params: LoadParams<String>
    ): LoadResult<String, Article> {
        val cursor = params.key   // null on first load
        return try {
            val res = api.getArticles(cursor = cursor, size = params.loadSize)
            LoadResult.Page(
                data = res.items,
                prevKey = null,            // forward-only cursor
                nextKey = res.nextCursor   // null when exhausted
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<String, Article>) = null
}

하나로 연결하기

이제 전체 데이터 계층을 갖추었습니다. 페이지 하나를 로드하는 PagingSource, 페이지를 스트리밍하는 Pager, 그리고 흐름을 캐시하는 ViewModel입니다.

UI 계층은 간단히 viewModel.articles를 수집하면 됩니다. 다음 레슨에서는 Compose LazyColumn으로 이를 렌더링합니다.

// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow  -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articles

빠른 확인

페이지 번호를 사용하는 PagingSource에서 load()가 nextKey = null을 반환하면 무엇을 의미하나요?

복습: PagingSource와 Pager

페이징을 위한 데이터 계층을 만들었습니다.

  • PagingSource<Key, Value>는 load()와 getRefreshKey()를 구현합니다.
  • load()는 prevKey/nextKey와 함께 LoadResult.Page를 반환하거나 LoadResult.Error를 반환합니다.
  • null 키는 해당 방향의 페이징을 중지합니다.
  • Pager(config, factory).flow는 Flow<PagingData>를 생성합니다.
  • cachedIn(viewModelScope)은 구성 변경 이후에도 데이터를 유지합니다.

다음: Compose 목록에서 이 흐름을 렌더링합니다.

무료로 시작

AI 튜터와 함께 Kotlin을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
36
레슨
152

자주 묻는 질문

“PagingSource와 Pager” 강의는 무료인가요?

네 — “PagingSource와 Pager” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Android Academy 강의 전체를 잠금 해제할 수 있습니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“PagingSource와 Pager”에서 뭘 배우나요?

페이지를 로드하는 방식을 정의합니다. 브라우저에서 직접 실행하는 실습 코드로 Android Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Android Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Android Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“PagingSource와 Pager” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Android Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Android Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 페이징이 필요한 이유
  2. PagingSource와 Pager
  3. Compose 목록에서 페이징
  4. RemoteMediator와 캐싱
← Android Academy(으)로 돌아가기