| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- develop
- java 출력
- JS
- npm
- 자바 출력 방식
- LLM
- println
- 앱심사
- 자바스크립트
- abap
- DART
- ListView
- firebase
- scss
- UI/UX
- JQ
- 단축키
- riverpod
- 배포
- unity
- lifecycle
- react
- nodejs
- 자바 포맷 출력
- Clean Architecture
- Flutter
- printf
- java
- 엡
- java 콘솔 출력 차이
- Today
- Total
guricode
Riverpod 상태 변경 에러 트러블슈팅: Tried to modify a provider while the widget tree was building 본문
Riverpod 상태 변경 에러 트러블슈팅: Tried to modify a provider while the widget tree was building
agentrakugaki 2025. 8. 5. 11:491. 문제 상황
Flutter에서 Riverpod 상태관리를 사용하면서, 리뷰 데이터를 관리하는 ReviewViewModel을 AsyncNotifier로 구현하였다.
ReviewPage로 이동 시, mapX, mapY 좌표를 전달받아 아래와 같은 로직으로 리뷰를 로드하려고 했다:
@override
void initState() {
super.initState();
final vm = ref.read(reviewProvider.notifier);
vm.setCoordinates(widget.mapX, widget.mapY);
vm.loadReviews(); // 이 부분에서 예외 발생
}
앱을 실행했을 때 다음과 같은 에러 메시지가 발생했다:
FlutterError: Tried to modify a provider while the widget tree was building.
...
Modifying a provider inside those life-cycles is not allowed.
이 에러는 앱이 완전히 멈추는 수준의 런타임 예외이며, 사용자 입장에서는 화면이 표시되지 않고 로딩 중 멈추는 현상으로 나타난다.
2. 문제 분석
이 에러는 Riverpod에서 아주 자주 발생하는 대표적인 사용 실수 중 하나다. 핵심 원인은 아래와 같다:
- Flutter 위젯 생명주기상 initState, build, didChangeDependencies 등의 메서드는 위젯 트리가 아직 안정되지 않은 상태에서 실행된다.
- 이 시점에서 ref.read(...).someMethod()를 통해 Provider의 상태(state)를 변경하면, 위젯이 제대로 상태를 구독하지 못하고 UI 불일치가 생길 수 있다.
- 그래서 Riverpod은 이러한 상황을 감지하여 강제로 예외를 던진다.
위 예외 메시지는 이를 정확히 설명하고 있다:
Tried to modify a provider while the widget tree was building.
3. 시도한 해결 방법
시도 1: initState에서 직접 상태 변경
@override
void initState() {
super.initState();
final vm = ref.read(reviewProvider.notifier);
vm.setCoordinates(widget.mapX, widget.mapY);
vm.loadReviews();
}
이 코드는 개발자 입장에서 가장 자연스럽게 보일 수 있다. 하지만 Riverpod에서는 금지된 접근 방식이다. loadReviews() 호출 시 내부에서 state = AsyncData(...) 혹은 AsyncLoading()으로 상태를 변경하게 되는데, 이 시점은 아직 위젯 트리가 구성 중이므로 예외가 발생한다.
4. 최종 해결 방법
Flutter에서 위젯 트리가 안정된 이후에 provider 상태를 변경해야 한다. 이를 위한 가장 안전한 방법은 다음 두 가지 중 하나다:
방법 1: Future.microtask로 감싸서 호출
@override
void initState() {
super.initState();
Future.microtask(() {
final vm = ref.read(reviewProvider.notifier);
vm.setCoordinates(widget.mapX, widget.mapY);
vm.loadReviews();
});
}
방법 2: WidgetsBinding.instance.addPostFrameCallback 사용
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final vm = ref.read(reviewProvider.notifier);
vm.setCoordinates(widget.mapX, widget.mapY);
vm.loadReviews();
});
}
이 방식은 모두 Flutter가 한 프레임을 그리고 난 이후 다음 이벤트 큐로 로직을 미루는 기법이다. 이때는 이미 위젯 트리 전체가 안정된 상태이므로 Riverpod도 상태 변경을 허용한다.
5. 왜 build()나 initState()에서 Provider를 수정하면 안 되는가?
Flutter에서 build()나 initState()는 렌더링 사이클 중간에 있다.
이 시점에서 ref.read(...).state = ... 같은 상태 변경을 하게 되면 아래와 같은 위험이 있다:
- 위젯 트리가 아직 완성되지 않았기 때문에 상태 변화에 따른 리빌드가 중복으로 일어날 수 있다.
- 여러 위젯이 동일한 provider를 구독 중일 경우, 상태가 불안정한 시점에서 서로 다른 UI 상태를 받을 수 있다.
- 복잡한 앱에서는 이로 인해 일관성 없는 UI, setState() 호출 에러, 무한 루프 등으로 이어질 수 있다.
Riverpod은 이러한 상황을 감지하면 강제 예외를 던져 개발자가 즉시 수정하도록 설계되어 있다.
6. GPT가 권장하지 않는 방법
setState를 사용해 강제로 지연
일부 개발자들은 Future.delayed(Duration.zero, () { ... })를 쓰거나 Timer.run() 같은 지연 기법으로 우회하려 한다.
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
ref.read(reviewProvider.notifier).loadReviews();
});
}
하지만 이는 구조적으로 맞는 방식은 아니다. Future.microtask 혹은 addPostFrameCallback은 Flutter 자체적으로 제공하는 안정적인 "렌더 이후 예약 실행" 방식이며, 디버깅도 쉬워서 권장된다.
7. AsyncNotifier의 build()는 반드시 구현해야 하는가?
Riverpod의 AsyncNotifier는 build() 메서드를 통해 상태의 초기 데이터를 반환하는 것이 기본 설계다. 하지만 우리는 현재 ReviewPage에 진입할 때 setCoordinates()를 통해 동적으로 좌표를 넣고 loadReviews()를 호출하는 구조다. 따라서 build()는 아무 역할도 하지 않는다.
이 경우 아래와 같이 build()를 최소 구현으로 유지할 수 있다:
@override
FutureOr<List<Review>> build() async {
return const []; // 혹은 throw UnimplementedError(); 유지해도 괜찮음
}
단, Riverpod은 build()를 내부적으로 호출하므로 반드시 정의되어야 하며, 최소한의 기본 값이 리턴되도록 구성해야 한다.
8. 향후 고려 사항 및 설계 가이드
1) Provider 상태 변경은 항상 "빌드 이후"로 미뤄야 한다
- 특히 NotifierProvider, AsyncNotifierProvider를 사용할 경우에는 initState()에서 직접 state = ... 호출은 금지
- 항상 microtask 또는 addPostFrameCallback으로 이동
2) Provider 초기 상태 설계하기
가능하다면 build() 안에서 필요한 좌표나 초기 데이터를 ref.watch(someOtherProvider)를 통해 불러와 설정하는 구조로 가는 것이 이상적이다. 하지만 지금처럼 외부로부터 값을 받아 ViewModel에 넘기는 경우에는 .setCoordinates() 방식도 나쁜 선택은 아니다.
3) ViewModel 의존성 줄이기
의존성이 늘어날수록 build()에서 설정하지 않고 외부에서 setter를 쓰는 구조가 위험해질 수 있다. 가능한 경우 ViewModel에서 모든 초기값을 설정하거나, Provider family를 사용하는 방식도 고려할 수 있다.
예: reviewProviderFamily(mapX, mapY) 같은 구조
9. 결론
이번 트러블슈팅은 Riverpod 사용 중 가장 흔하게 발생하는 "위젯 생명주기 중 상태를 수정하는 행위"에 대한 실전 사례였다.
정리하자면:
- initState()에서 Provider 상태 변경 시 에러가 발생하는 이유는 UI 불일치 방지를 위한 Riverpod의 안전장치 때문이다.
- 이를 해결하는 가장 깔끔한 방법은 Future.microtask 또는 addPostFrameCallback 을 사용하는 것
- 가능하다면 초기 상태를 build() 내부에서 정의하거나, Provider family로 외부 값을 전달하는 구조도 고려할 수 있다.
Riverpod은 강력하지만 위젯 생명주기와 잘 호흡하지 않으면 이런 문제가 반복적으로 발생할 수 있다. 이번 경험을 통해 상태 관리와 위젯 생명주기 사이의 상호작용에 대한 감각을 조금 익힐 수 있었다....아직 잘 모르겠다...ㅜㅜ
'앱 > Flutter&Dart' 카테고리의 다른 글
| 트러블슈팅: CircleAvatar로 프로필 이미지 UI 구현 시 크기 제약 문제 (0) | 2025.08.07 |
|---|---|
| Kotlin 컴파일 오류: metadata version mismatch 트러블슈팅 (3) | 2025.08.07 |
| BottomNavigationBar_Widget (1) | 2025.08.04 |
| Consumer_Riverpod (0) | 2025.08.01 |
| Riverpod, MVVM 구조로 프로젝트를 설계하는 법 (2) | 2025.07.31 |