요약 (한 줄): 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로 성능 저하.select나ref.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 없이 마커만 최적화 갱신 가능.
- UI는
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 감소
'App Dev > Flutter' 카테고리의 다른 글
| [Flutter] Provider context.selector ListView 안에서 직접 사용 불가 문제 (2) | 2024.02.24 |
|---|
