요약 (한 줄): 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 감소