본문 바로가기
공부/프로그래밍

flutter 무한스크롤: riverpod 상태관리 삽질(스크롤 위치 초기화 및 리빌드 문제)

by demonic_ 2025. 3. 13.
반응형

 

Flutter에서 무한 스크롤을 구현할 때 흔히 겪는 문제 중 하나는 바로 데이터 로딩 시 스크롤 위치가 초기화되는 것입니다. 최근 제가 겪었던 문제와 해결 과정을 공유합니다.

 

 

문제 상황 파악하기

제가 최근 상담 후기 목록 화면을 Flutter로 개발하며 무한 스크롤을 적용했습니다. 일반적인 무한 스크롤 구현 방법처럼 사용자가 리스트를 가장 아래까지 스크롤하면 추가 데이터를 자동으로 불러오도록 했습니다. 그런데 예상치 못하게 새로운 데이터를 로딩할 때마다 화면이 전체적으로 다시 그려지면서 스크롤 위치가 맨 위로 강제로 이동하는 현상이 발생했습니다.

 

처음에는 스크롤 컨트롤러나 ListView의 설정이 잘못됐을 거라 생각하여 여러 번 점검해봤지만, 문제는 전혀 개선되지 않았습니다. 고민 끝에 상태 관리 라이브러리로 사용 중인 Riverpod의 ref.watch 기능을 의심하게 되었고, 원인을 구체적으로 확인하게 되었습니다.

 

조회 영역

Future<ListResponse<PartnerReviewModel>> _fetch() async {
    final partnerId = partnerState.value!.partnerId;
    final condition = ref.watch(partnerReviewConditionNotifierProvider);

    final partnerReview = await ref.watch(getPartnerReviewsUseCaseProvider(
      partnerId: partnerId,
      request: condition,
    ).future);

    _originalData = partnerReview;
    return partnerReview;
  }

 

lastId 데이터 변경 부분

@riverpod
class PartnerReviewConditionNotifier extends _$PartnerReviewConditionNotifier {
  @override
  PartnerMeReviewSearchModel build() {
    return PartnerMeReviewSearchModel(
      lastId: Pagination.DEFAULT_DESC_LAST_ID, // lastId 와 Size는 update하지 않는게 좋음.
      size: Pagination.DEFAULT_PAGE_SIZE,
      reviewType: PartnerReviewType.all,
      isNotReply: false,
    );
  }

  void updateLastId(int lastId) {
    state = state.copyWith(
      lastId: lastId,
    );
  }
}

 

이 코드는 상담 후기 목록을 가져올 때 조건(예: 필터, 마지막 데이터 ID 등)을 Provider를 통해 관리하고 있었습니다. 특히 여기서 페이지네이션을 위해 사용되는 lastId를 포함한 상태를 watch로 감시하도록 구현했습니다.

 

이렇게 하면, 데이터가 추가될 때마다 상태가 변경되고, 상태 변경을 감지한 Riverpod이 자동으로 위젯 전체를 재빌드합니다. 결과적으로 무한 스크롤의 추가 데이터를 로딩하는 순간 UI가 전체적으로 다시 그려지고, 사용자는 자신이 보던 스크롤 위치를 잃게 되었습니다.

 

 

잘못된 접근 방식

이전 코드는 추가 데이터를 로딩할 때마다 아래와 같이 상태를 직접 변경하고 있었습니다.

ref
  .read(partnerReviewConditionNotifierProvider.notifier)
  .updateLastId(lastId);
 

이처럼 상태 변경을 직접 조건 notifier를 통해 관리하면, 추가 로딩이 발생할 때마다 화면 전체의 데이터가 초기화되는 문제가 생깁니다. 사용자는 자신이 보고 있던 리스트의 위치를 잃고 다시 스크롤을 내려야 하는 불편을 겪게 됩니다.

 

 

 

코드의 문제점

위 방식의 문제를 좀 더 자세히 살펴보면 다음과 같습니다.

  1. 상태 변경 시 전체 rebuild: 조건(notifier)의 상태가 변경될 때마다 Provider를 watch하고 있는 위젯들이 자동으로 재빌드됩니다. 특히 무한 스크롤의 pagination을 위해 사용하는 lastId 같은 조건값이 변경될 때마다 전체 화면이 다시 빌드되어 효율성이 떨어집니다.
  2. 스크롤 포지션 초기화: 전체 rebuild가 일어나면 기존의 스크롤 상태가 유지되지 않고 초기 위치로 돌아가게 됩니다. 사용자가 스크롤 하던 위치를 잃게 되어 불편한 사용자 경험을 제공합니다.

 

이러한 문제로 인해 무한 스크롤 구현 시 조건값을 watch하는 방식은 적합하지 않다고 판단했습니다.

 

 

효과적인 해결책

이 문제를 해결하기 위해 pagination 정보인 lastId를 외부의 상태 관리 객체에서 관리하지 않고, 데이터 fetching 로직을 담당하는 Notifier 클래스 내부에서 직접 관리하는 것으로 변경했습니다. 이렇게 하면 상태 변경을 감지할 필요 없이 새로운 데이터를 기존 데이터 리스트에 간단히 추가할 수 있습니다.

 

구체적으로 다음과 같이 변경했습니다:

 

스크롤 이벤트 처리:

 

 

 

void _onScroll() {
  if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
    final reviewNotifier = ref.read(partnerReviewNotifierProvider.notifier);
    if (!reviewNotifier.isLoadingMore) {
      reviewNotifier.loadMore();
    }
  }
}
 
그리고 Notifier 내부에 loadMore() 메서드를 추가하여, 조건을 read로 접근해 rebuild 없이 데이터를 추가로 로딩합니다.
 
Future<void> loadMore() async {
  if (_isLoadingMore) return;

  _isLoadingMore = true;

  final condition = ref.read(partnerReviewConditionNotifierProvider);
  final lastId = _originalData?.data.last.counselReviewId;
  final updatedCondition = condition.copyWith(lastId: lastId);

  final partnerId = ref.read(partnerMeStateNotifierProvider).value!.partnerId;

  final newData = await ref.watch(getPartnerReviewsUseCaseProvider(
    partnerId: partnerId,
    request: updatedCondition,
  ).future);

  _originalData = _originalData!.copyWith(
    data: [..._originalData!.data, ...newData.data],
    total: _originalData!.total + newData.data.length,
  );

  state = AsyncData(_originalData!);

  _isLoadingMore = false;
}
 
 

 

_isLoadingMore 를 쓴 이유:

스크롤을 감지하는 형태로 하다보니 한번에 여러번 호출이 될 수 있습니다. 때문에 해당 값을 통해 중복호출을 막기위해 추가되었습니다.

 

 

마무리

이번 경험을 통해 Flutter에서 상태 관리를 할 때, 특히 무한 스크롤 같은 빈번한 데이터 로딩 상황에서는 상태 변경에 따른 rebuild를 최소화하는 설계가 중요하다는 점을 다시 한번 깨달았습니다. 사용자 경험 측면에서도, 성능 측면에서도 불필요한 rebuild를 막는 전략은 필수적입니다.

 

 

만약 같은 문제로 고민하는 분이 있다면 이 글이 도움이 되길 바랍니다. 앞으로도 Flutter를 이용한 개발 과정에서 얻는 유익한 경험들을 더 자주 공유하도록 노력하겠습니다.

 

 

 

 

 

 

 

반응형

댓글