Room + Paging 완전 가이드 — PagingSource/RemoteMediator/오프라인 캐시

발행: (2026년 3월 2일 오후 02:12 GMT+9)
3 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 Source 링크만 그대로 두고, 번역이 필요한 본문 내용을 알려주시면 해당 부분을 한국어로 번역해 드리겠습니다.
(코드 블록이나 URL은 그대로 유지하고, 마크다운 형식과 기술 용어는 원본 그대로 유지합니다.)

이 문서에서 배울 수 있는 것

DAO

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY createdAt DESC")
    fun getArticlesPaging(): PagingSource

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(articles: List)

    @Query("DELETE FROM articles")
    suspend fun clearAll()
}

RemoteMediator

@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator @Inject constructor(
    private val api: ArticleApi,
    private val db: AppDatabase
) : RemoteMediator() {

    override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult {
        val page = when (loadType) {
            LoadType.REFRESH -> 1
            LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
            LoadType.APPEND -> {
                val lastItem = state.lastItemOrNull()
                    ?: return MediatorResult.Success(endOfPaginationReached = true)
                lastItem.page + 1
            }
        }

        return try {
            val response = api.getArticles(page = page, pageSize = state.config.pageSize)

            db.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    db.articleDao().clearAll()
                }
                db.articleDao().insertAll(response.map { it.copy(page = page) })
            }

            MediatorResult.Success(endOfPaginationReached = response.isEmpty())
        } catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }
}

Repository

class ArticleRepository @Inject constructor(
    private val db: AppDatabase,
    private val remoteMediator: ArticleRemoteMediator
) {
    @OptIn(ExperimentalPagingApi::class)
    fun getArticles(): Flow> {
        return Pager(
            config = PagingConfig(pageSize = 20, prefetchDistance = 5),
            remoteMediator = remoteMediator,
            pagingSourceFactory = { db.articleDao().getArticlesPaging() }
        ).flow
    }
}

ViewModel

@HiltViewModel
class ArticleViewModel @Inject constructor(
    repository: ArticleRepository
) : ViewModel() {
    val articles = repository.getArticles().cachedIn(viewModelScope)
}

Compose UI

@Composable
fun ArticleList(viewModel: ArticleViewModel = hiltViewModel()) {
    val articles = viewModel.articles.collectAsLazyPagingItems()

    LazyColumn {
        items(articles.itemCount) { index ->
            articles[index]?.let { article ->
                ListItem(
                    headlineContent = { Text(article.title) },
                    supportingContent = { Text(article.summary) }
                )
            }
        }

        when (articles.loadState.append) {
            is LoadState.Loading -> item {
                CircularProgressIndicator(
                    Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                )
            }
            is LoadState.Error -> item {
                Text("読み込みエラー", Modifier.padding(16.dp))
            }
            else -> {}
        }
    }
}

컴포넌트와 역할

컴포넌트역할
PagingSource페이지 단위 데이터 가져오기
RemoteMediatorAPI → DB 동기화
PagerPagingData 생성
collectAsLazyPagingItemsCompose 연동
  • Room DAO가 PagingSource를 직접 반환
  • RemoteMediator를 통해 API → 로컬 DB 캐시
  • 오프라인에서도 DB에서 표시 가능
  • cachedIn(viewModelScope)로 재구성 시 재조회 방지

템플릿 공개

8가지 Android 앱 템플릿(Paging 지원)을 공개하고 있습니다。
템플릿 목록 → Gumroad

관련 기사

  • Paging3
  • Room/Flow
  • 오프라인 퍼스트

Gumroad에서 8개의 Android 앱 템플릿(Room DB, Material3, MVVM)을 제공합니다.
템플릿 둘러보기 → Gumroad

0 조회
Back to Blog

관련 글

더 보기 »