728x90
반응형

요약 (한 줄): Riverpod에서 상태를 읽고 구독하고 갱신하는 올바른 패턴과 성능 최적화(선택적 구독/부수효과 처리)를 정리한 블로그 포스트.

들어가며
Flutter + Riverpod으로 앱을 작성할 때 ref.watch, ref.read, ref.read(...notifier), ref.listen, select를 적절히 쓰면 성능과 유지보수성이 좋아집니다. 아래는 실무 중심의 요점 정리와 p_search.dart 적용 예시입니다.

1. 기본 개념 정리

  • ref.watch(provider)
    • UI에서 사용. provider 값이 바뀌면 위젯이 재빌드됨(reactive).
    • 예: final searchState = ref.watch(searchNotifierProvider);
  • ref.read(provider)
    • 비구독(스냅샷) 읽기. 이벤트 핸들러/초기에 한 번만 읽고 싶을 때 사용. 자동으로 재빌드되지 않음.
    • 예: final state = ref.read(searchNotifierProvider);
  • ref.read(provider.notifier)
    • Notifier 인스턴스(액션 메서드)를 호출할 때 사용. 상태를 직접 변경하는 메서드 호출 목적.
    • 예: await ref.read(searchNotifierProvider.notifier).loadMoreRestaurants();
  • ref.listen(provider, (prev, next) { ... })
    • UI 재빌드 없이 상태 변경에 따른 부수효과(마커 업데이트, 토스트, 네비게이션 등)를 처리할 때 사용.
    • 예: ref.listen(searchNotifierProvider, (prev, next) { _onStateChanged(prev, next); });

2. provider.select((s) => s.field) — 무엇을 하고, 언제 쓸까?

  • 목적: provider 전체 변경이 자주 발생해도, 선택한 필드 값이 실제로 바뀌었을 때만 위젯을 rebuild/리스닝 하도록 범위를 좁힘.
    • 예: ref.watch(searchNotifierProvider.select((s) => s.isLoading))isLoading이 바뀔 때만 rebuild.
  • 동작: select의 결과값 간 비교는 == 연산을 사용.
    • 기본 타입: 값 비교(숫자/문자열 등).
    • 리스트/맵/객체: == 구현 방식에 따라 다름(보통 참조 비교).
    • 리스트 내부의 요소만 바뀌어도 동일 인스턴스를 재사용하면 select((s) => s.list)는 변경으로 인식되지 않을 수 있음.
  • 권장 사용:
    • 특정 필드(예: isLoading, currentQuery, restaurants.length)만 구독하고 싶을 때 select.
    • 리스트 내부 변화(요소 추가·삭제 등)를 감지하려면 select((s) => s.restaurants.length) 또는 select((s) => s.restaurants.map(...)로 반환값을 가공.

3. read vs “직접 state 객체 접근”(local var) 차이

  • final s = ref.read(provider)는 현재 상태의 스냅샷입니다. 이후 provider가 바뀌어도 이 s 참조는 자동으로 최신 값으로 갱신되지 않음(스냅샷).
  • final s = ref.watch(provider)는 위젯 빌드 시 다시 읽어서 최신 상태를 반영(구독/리액티브).
  • 즉, UI가 자동 업데이트되어야 하면 watch를, 이벤트 핸들러에서 일회성 읽기를 원하면 read를 사용하세요.

4. ref.listen vs watch

  • watch는 UI 재빌드 목적이라서, 무거운 작업(예: 마커 재생성, 애니메이션 트리거)을 watch로 처리하면 불필요한 rebuild/작업이 발생할 수 있음.
  • listen은 UI rebuild 없이 상태 변화에 따른 부수작업(서버 호출, 마커 업데이트 등)을 처리하기 좋음. listen은 빌드 컨텍스트에서 안전하게 사용(예: initState나 build에서 설정).

5. 실무 팁(성능/안정성)

  • 빌드에서 많은 데이터 구조(리스트 전체)를 watch하면 잦은 re-render로 성능 저하. selectref.watch(provider.select(...))로 구독을 좁히세요.
  • 리스트 변경을 감지해야 한다면 select로 길이(length)나 해시를 구독하거나, ref.listen(listProvider.select((s)=>s))로 listen 하세요.
  • Notifier 메서드는 ref.read(provider.notifier)로 호출하세요. ref.read(provider)는 상태 값만 반환합니다.
  • 같은 목록을 UI와 마커(지도)로 동시에 다루는 경우:
    • UI는 ref.watch(searchNotifierProvider)로 재빌드,
    • 마커 업데이트는 ref.listen(searchNotifierProvider.select((s)=>s.restaurants), ...)로 처리하면 rebuild 없이 마커만 최적화 갱신 가능.

6. p_search.dart에 적용할 수 있는 실례 (추천 코드)

  • 현재 코드는 await ref.read(searchNotifierProvider.notifier).loadMoreRestaurants();로 다음 페이지를 불러오고, 이후 ref.read(searchNotifierProvider)로 상태를 읽어 _updateSearchResultMarkers를 호출합니다.
  • 더 깔끔하게: ref.listen으로 restaurants만 감시하여 마커 업데이트 책임을 provider 상태 변경에 위임하면, 호출 지점에서 마커 업데이트를 매번 호출할 필요가 없습니다.
  • 권장 리팩터 예시 (p_search.dart에 넣을 수 있음):
// 상태가 바뀔 때마다 마커를 갱신하도록 리스너 등록 (initState에 추가)
@override
void initState() {
  super.initState();
  // ... 기존 initState
  ref.listen<List<RestaurantModel>>(
    searchNotifierProvider.select((s) => s.restaurants),
    (previous, next) {
      if (mounted) _updateSearchResultMarkers(next);
    },
  );
}
  • 이렇게 하면 loadMoreRestaurants() 호출 직후에 별도의 _updateSearchResultMarkers(...) 호출을 제거할 수 있어 중복 호출을 줄일 수 있습니다.
  • select((s) => s.restaurants)는 리스트 객체 인스턴스 자체가 바뀌었을 때 트리거됩니다. 내부 변경(동일 인스턴스의 내부 mutate)은 트리거되지 않으니 Notifier에서 불변성(immutable 객체/새 리스트 할당)을 유지하는 것이 권장됩니다.

7. 리스트 변경(깊은 비교) 주의

  • Notifier에서 종종 state = state.copyWith(restaurants: newList)처럼 리스트를 항상 새 인스턴스로 교체해줘야 select((s)=>s.restaurants)로 정확히 감지됩니다.
  • 만약 불변성을 유지하지 않는 코드가 있다면 select((s) => s.restaurants.length) 같은 대체 선택자를 사용해 변경을 감지하세요.

8. 권장 패턴 요약 (p_search.dart 적용 권장)

  • build(): final searchState = ref.watch(searchNotifierProvider); — UI 자동 갱신
  • onScroll/onPressed 등 이벤트: await ref.read(searchNotifierProvider.notifier).loadMoreRestaurants(); — Notifier 메서드 호출
  • 마커 업데이트: ref.listen(searchNotifierProvider.select((s) => s.restaurants), (prev, next) {...}) — UI rebuild 없이 마커 업데이트
  • 제안/히스토리 등 UI 전용 위젯은 select로 해당 필드만 구독해서 불필요한 rebuild 감소
728x90
반응형
LIST
728x90
반응형

썸넬

사전지식

provider의 context.selector(....)는 selector에 등록된 변수의 값이 바뀌는지 tracking하고, 값이 바뀌면 해당 위젯을 rebuild하는 메소드이다.
context.watch()도 있지만 wathc는 해당 값 말고 같은 changeNotifier 안에 등록된 다른 값이 바뀌어도 rebuild가 일어나 selector를 사용하면 불필요한 rebuild를 줄일 수 있다.

문제상황

════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building:
Tried to use context.select inside a SliverList/SliderGridView.

    This is likely a mistake, as instead of rebuilding only the item that cares
    about the selected value, this would rebuild the entire list/grid.

    To fix, add a `Builder` or extract the content of `itemBuilder` in a separate widget:

    ```dart
    ListView.builder(
      itemBuilder: (context, index) {
        return Builder(builder: (context) {
          final todo = context.select((TodoList list) => list[index]);
          return Text(todo.name);
        });
      },
    );
    ```
'package:provider/src/inherited_provider.dart':
Failed assertion: line 247 pos 12: 'widget is! SliverWithKeepAliveWidget'

 

ListView.builder 안에서 직접 context.selector를 사용해 해당 오류가 뜬걸로 보인다.
ListView.builder안에 직접 selector를 넣으면 ListView 전체가 rebuild가 일어나는 비효율적인 상황이 발생하므로 해당 오류를 던진 것으로 보인다.

해결 방법

친절하게도, flutter에서는 이번 오류 안에서 어떻게 해야할지를 알려준다.
context.select를 바로 ListView.builder안에 넣지 말고, 대신 Builder 위젯을 사용해 그 안에서 selector를 사용하는 방식으로 문제를 해결할 수 있을 것이다.

728x90
반응형
LIST

'App Dev > Flutter' 카테고리의 다른 글

[flutter] riverpod 상태관리 - watch, read  (2) 2025.12.13

+ Recent posts