개발일기

Compose + Paging 3로 무한 스크롤 구현하기

뱅우 2025. 9. 18. 09:31
반응형
Compose + Paging 3로 무한 스크롤 구현하기

모바일 앱에서 무한 스크롤은 필수적인 UI 패턴 중 하나입니다. Jetpack의 Paging 3 라이브러리Jetpack Compose를 결합하면 복잡한 리스트 페이징을 간단하게 구현할 수 있습니다. 이번 글에서는 Compose와 Paging 3를 활용해 무한 스크롤 리스트를 구현하는 방법을 살펴보겠습니다.


1. Paging 3 기본 구조

  • PagingSource: 데이터 페이지를 로드하는 클래스
  • Pager: PagingSource를 사용해 Flow를 생성
  • PagingData: UI로 전달되는 페이징 데이터 컨테이너

2. PagingSource 구현

class UserPagingSource(
    private val api: ApiService
) : PagingSource<Int, User>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        return try {
            val page = params.key ?: 1
            val response = api.getUsers(page)
            LoadResult.Page(
                data = response.users,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.users.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

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

3. Pager 설정 (Repository)

class UserRepository(private val api: ApiService) {
    fun getUserPagingData(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(pageSize = 20, enablePlaceholders = false),
            pagingSourceFactory = { UserPagingSource(api) }
        ).flow
    }
}

4. ViewModel에서 Flow 전달

class UserViewModel(private val repository: UserRepository) : ViewModel() {
    val users = repository.getUserPagingData()
        .cachedIn(viewModelScope)
}

5. Compose UI에서 PagingData 구독

Compose에서는 collectAsLazyPagingItems()를 통해 PagingData를 바로 리스트에 연결할 수 있습니다.

@Composable
fun UserListScreen(viewModel: UserViewModel) {
    val users = viewModel.users.collectAsLazyPagingItems()

    LazyColumn {
        items(users.itemCount) { index ->
            val user = users[index]
            if (user != null) {
                Text(user.name)
            } else {
                CircularProgressIndicator() // 로딩 표시
            }
        }

        users.apply {
            when {
                loadState.refresh is LoadState.Error -> {
                    item { Text("새로고침 실패") }
                }
                loadState.append is LoadState.Error -> {
                    item { Text("더 불러오기 실패") }
                }
            }
        }
    }
}

6. 무한 스크롤 UX 팁

  • 마지막 아이템 아래 로딩 인디케이터 추가
  • 에러 발생 시 재시도 버튼 제공
  • Pull-to-refresh(스와이프 새로고침)과 함께 사용하면 UX 향상

7. 결론

Compose와 Paging 3를 함께 사용하면 무한 스크롤 구현이 매우 간단해집니다. 기존 RecyclerView + Adapter 대비 코드가 훨씬 줄어들고, 상태 관리도 Flow로 통합할 수 있습니다.

다음 글 예고: 다음 글에서는 멀티 모듈 + Clean Architecture 구조로 Compose 앱 설계하기를 다뤄보겠습니다.


반응형