Jetpack Compose와 Paging3 통합 - PagingDataAdapter 문제 해결기


들어가며

최근 Jetpack Compose와 Paging3를 활용한 프로젝트를 진행하면서 예상치 못한 문제가 발생했습니다. 바로 Paging3의 PagingDataAdapter와 Jetpack Compose의 LazyColumn을 함께 사용할 때 발생하는 충돌이었습니다. 이 글에서는 문제를 어떻게 해결했는지, 그 과정에서 겪었던 시행착오와 최종 해결 방안을 공유하려고 합니다.


PagingDataAdapter와 Jetpack Compose의 문제

PagingDataAdapter의 기본 개념

먼저, PagingDataAdapter는 RecyclerView와 함께 사용하여 대용량 데이터를 효율적으로 표시할 수 있도록 도와주는 어댑터입니다. 이 어댑터는 RecyclerView와 긴밀하게 결합되어 있어, UI의 상태를 변경할 때 데이터를 자동으로 갱신하고, 데이터 변경 사항을 감지하여 UI를 업데이트합니다.

하지만 Jetpack Compose로 넘어오면서 문제가 발생했습니다. Jetpack Compose는 상태 기반의 UI 라이브러리로, 기존의 ViewGroup인 RecyclerView를 대체할 수 있는 LazyColumn 같은 컴포저블을 사용합니다. 문제는 PagingDataAdapter가 Compose 환경에서 제대로 동작하지 않는다는 점이었습니다.

Compose 환경에서의 주요 문제점

  1. LazyColumn과 PagingDataAdapter 간의 충돌:

    • Jetpack Compose에서는 상태 변화에 따라 UI를 다시 그리게 됩니다. 하지만 PagingDataAdapter는 Compose의 LazyColumn과는 호환되지 않아, 데이터를 렌더링하거나 업데이트하는 과정에서 문제를 일으켰습니다.
    • 이로 인해, 리스트 아이템이 중복되거나, 특정 상황에서 UI가 예상과 다르게 동작하는 등의 문제가 발생했습니다.
  2. 데이터 변경 감지의 어려움:

    • Jetpack Compose는 데이터 클래스의 equalshashCode 메서드를 기반으로 상태를 감지하고, 상태가 변경되었을 때만 UI를 리렌더링합니다. 그런데 Paging3에서 제공하는 PagingData는 내부적으로 메모리 주소가 다르면 다른 객체로 인식되어, 같은 데이터를 가지고 있음에도 불구하고 불필요한 리렌더링이 발생할 수 있습니다.
    • 특히 대용량 데이터를 처리할 때 이런 리렌더링은 성능 저하로 이어질 수 있습니다.

해결을 위한 시도와 문제 분석

문제 원인 분석

이번 문제의 근본 원인은 Paging3와 Jetpack Compose의 동작 방식의 차이에서 비롯되었습니다. 이 문제를 해결하기 위해 몇 가지 접근 방식을 시도해 보았습니다.

  1. Flow의 재생성 문제:

    • Jetpack Compose에서 Flow를 사용해 PagingData를 처리할 때, Flow가 불필요하게 재생성되면서 반복적으로 데이터를 불러오는 문제가 발생했습니다. 이는 동일한 쿼리를 반복적으로 입력할 때마다 Flow가 새로 생성되면서 발생한 문제였습니다.
  2. RemoteMediator에서의 무한 호출 문제:

    • RemoteMediator를 사용해 원격 데이터를 페이징 처리할 때, 특정 검색어에 대해 데이터가 없을 경우, 무한 루프가 발생하는 문제가 있었습니다. 이는 RemoteMediator가 현재 페이지를 제대로 관리하지 못해 발생한 문제였습니다.

문제 해결 과정

  1. Flow 상태 관리:

    • Flow가 불필요하게 재생성되지 않도록 하기 위해, 새로운 쿼리가 기존 쿼리와 다를 때만 Flow를 재생성하도록 변경했습니다. 이를 통해 동일한 쿼리가 반복 입력되더라도 불필요한 재생성을 방지할 수 있었습니다.
  2. RemoteMediator에서의 페이지 관리:

    • RemoteMediator에서 마지막으로 요청한 페이지를 추적하고, 중복된 페이지 요청을 방지하기 위해 lastRequestedPage 변수를 도입했습니다. 또한, 데이터가 없을 때 endOfPaginationReached 플래그를 설정하여 무한 호출 문제를 해결했습니다.

아키텍처 관점에서의 해결 방안

Repository에서의 처리

왜 이러한 문제들을 Repository에서 처리해야 하는지 고민해 보았습니다.

  1. 데이터의 일관성:

    • Repository는 애플리케이션의 데이터 계층으로, 로컬 데이터베이스와 원격 데이터 소스 간의 일관성을 유지하는 역할을 합니다. 따라서 Paging과 관련된 모든 복잡한 로직은 Repository에서 처리하는 것이 바람직합니다.
    • RemoteMediator를 Repository에서 관리함으로써 로컬 데이터와 원격 데이터 간의 동기화를 효과적으로 관리할 수 있습니다.
  2. 관심사의 분리:

    • MVI 아키텍처에서 ViewModel은 UI 상태를 관리하고, UI와의 상호작용에 집중해야 합니다. 따라서 Paging과 관련된 복잡한 로직은 Repository에 위임하여 ViewModel의 책임을 줄이고, 코드의 가독성과 유지보수성을 높일 수 있습니다.
  3. 테스트 용이성:

    • Paging 관련 로직을 Repository에 두면, 이를 개별적으로 테스트할 수 있습니다. ViewModel과 UI에 대한 테스트와는 별도로 Repository를 독립적으로 테스트할 수 있어, 더 나은 유지보수가 가능합니다.

Hilt를 활용한 모듈화

Paging3와 Jetpack Compose를 통합하는 과정에서 Hilt를 활용하여 의존성 주입을 관리했습니다. 특히 RemoteMediator를 동적으로 생성할 수 있도록 Hilt 모듈을 설계하여 코드의 유연성을 확보했습니다.

Hilt 모듈 설계

Hilt를 사용해 PagingConfig와 RemoteMediator의 생성을 관리하였고, 이를 통해 Paging 관련 로직을 더 깔끔하게 분리할 수 있었습니다.

@Module
@InstallIn(SingletonComponent::class)
object PagingModule {

    @Provides
    @Singleton
    fun providePagingConfig(): PagingConfig =
        PagingConfig(
            pageSize = 10,
            prefetchDistance = 5,
            enablePlaceholders = false
        )

    @Provides
    fun provideRemoteMediatorFactory(
        remote: BookRemoteDataSource,
        local: BookLocalDataSource
    ): (String) -> BookRemoteMediator {
        return { query -> BookRemoteMediator(query, remote, local) }
    }
}

장점

  • 유연성: 다양한 쿼리에 대응할 수 있도록 RemoteMediator를 동적으로 생성할 수 있었습니다.
  • 모듈화: PagingConfig와 RemoteMediator의 생성을 별도의 모듈로 분리하여 코드의 재사용성과 유지보수성을 높였습니다.

마무리

이번 프로젝트에서 Jetpack Compose와 Paging3를 결합하면서 예상치 못한 문제들을 많이 겪었지만, 이를 통해 많은 것을 배울 수 있었습니다. 특히, Jetpack Compose와 기존 UI 라이브러리의 차이점, Paging3의 동작 원리 등을 깊이 이해할 수 있었고, 문제 해결을 위해 아키텍처적으로 접근하는 방법을 습득했습니다.

이 글이 Jetpack Compose와 Paging3를 함께 사용할 때 발생할 수 있는 문제를 해결하는 데 도움이 되길 바랍니다. 추가적인 질문이나 논의가 필요하다면 언제든지 댓글로 알려주세요!

profile
클린코드와 UX를 생각하는 비즈니스 드리븐 소프트웨어 엔지니어입니다.

0개의 댓글