-
Riverpod 3에서 await 중 ref가 죽는 순간AI Agent/flutter 2026. 7. 6. 15:00728x90반응형

화면을 빨리 닫으면 재현되던 간헐 크래시 크래시 리포트가 하루에 두세 번씩 올라오는데, 제 손으로는 한 번도 재현이 안 됐습니다.
스택 트레이스는 늘 같은 줄을 가리켰습니다.
폼을 제출하는 provider의 submit 함수, 그 안에서 await 다음 줄이었습니다.
로그만 보면 명백한데, 똑같이 눌러보면 멀쩡했습니다.
그렇게 이틀을 흘려보냈습니다.재현이 안 되는 버그가 제일 무섭습니다.
고쳤는지 확인할 방법이 없으니까요.
그래서 저는 버그를 고치기 전에, 이걸 제 마음대로 터뜨리는 방법부터 찾기로 했습니다.어떻게 하면 매번 터질까?
크래시가 난 로그들을 쭉 내려보며 공통점을 찾았습니다.
크래시 직전에 항상 화면 전환 로그가 한 줄 찍혀 있었습니다.
제출 버튼을 누르고 서버 응답을 기다리는 동안, 사용자가 뒤로 가기를 눌러 화면을 닫은 흔적이었습니다.
느린 네트워크에 성질 급한 손이 겹친 조합이었습니다.처음엔 그냥 빠르게 눌러 닫아봤습니다.
제출하고 곧바로 뒤로 가기.
그런데 제 폰에서는 서버가 너무 빨리 답해서, 화면이 닫히기도 전에 이미 await가 끝나 버렸습니다.
크래시가 날 틈이 없었던 겁니다.
몇 번을 반복해도 저는 그 타이밍을 손으로 못 맞췄습니다.
로그 속 사용자는 저보다 훨씬 느린 네트워크에 있었고, 저는 그 느림을 흉내 내야 했습니다.
그래서 문제를 뒤집었습니다.
손을 빠르게 만드는 대신, 서버를 느리게 만들기로.그래서 재현 조건을 직접 만들었습니다.
서버 호출에 일부러 3초 딜레이를 걸고, 제출을 누르자마자 뒤로 가기를 눌렀습니다.
그제야 매번 터졌습니다.
이미 dispose된 ref를 썼다는 에러였습니다.매번 터지는 걸 확인한 순간, 오히려 마음이 놓였습니다.
이제 이 조건만 안 터지게 만들면 되니까요.
재현이 안 될 때 이틀을 태운 건, 실은 고칠 대상이 손에 안 잡혀서였습니다.await 앞과 뒤는 같은 세계가 아니다
그때 알았습니다.
await 앞의 ref와 await 뒤의 ref는 같은 코드에 있어도 같은 상황이 아닙니다.
await 앞에서는 이 provider를 보고 있는 화면이 살아 있습니다.
그런데 서버를 기다리는 그 몇 초 사이에 화면이 닫히면, 이 provider를 watch하던 곳이 사라집니다.
autoDispose provider는 보는 사람이 없어지는 순간 스스로 정리됩니다.
정리된 다음에 await가 끝나서 돌아오면, 이미 없어진 ref를 만지는 겁니다.제 코드가 딱 그랬습니다.
Future<void> submit(FormData data) async { await ref.read(repoProvider).save(data); // 이 사이에 화면이 닫히면 ref.invalidate(listProvider); // 여기서 disposed ref }submit을 호출하는 화면은 이 notifier를 read만 하고 watch는 안 했습니다.
그러니 화면이 닫히면 붙잡아 줄 게 아무것도 없었습니다.
read는 값을 한 번 꺼내 쓰는 거지, "내가 이걸 보고 있으니 살려둬"라는 신호가 아니었습니다.
이 차이를 그동안 같은 걸로 여기고 있었습니다.그럼 await 동안만 붙잡아 두면 되지 않나?
Riverpod에는 정확히 그걸 위한 게 있었습니다.
keepAlive입니다.
await가 진행되는 동안만 provider를 살려두는 링크를 만들고, 끝나면 놓아줍니다.Future<void> submit(FormData data) async { final link = ref.keepAlive(); try { await ref.read(repoProvider).save(data); ref.invalidate(listProvider); } finally { link.close(); } }keepAlive를 걸고 나서, 아까 만든 재현 조건을 다시 돌렸습니다.
3초 딜레이에 뒤로 가기를 눌러도 크래시가 안 났습니다.
딜레이를 5초로 늘리고 뒤로 가기를 두 번 눌러도 조용했습니다.
그제야 고쳤다는 확신이 들었습니다.
재현 방법을 먼저 만들어 둔 게, 여기서 제일 크게 남았습니다.근데 화면이 닫힌 뒤에 상태를 바꿔도 되나?
여기서 한 번 더 걸렸습니다.
크래시는 안 나는데, 이미 사라진 화면을 위해 서버 저장을 끝까지 하는 게 맞나 싶었습니다.
반은 맞고 반은 아니었습니다.
서버에 저장하는 것 자체는 끝내는 게 맞습니다.
사용자가 제출을 눌렀으니까요.
다만 저장이 끝난 뒤 화면 상태를 건드리는 코드는, 화면이 아직 살아 있는지 한 번 보고 하는 게 안전했습니다.그래서 지금은 저장 성공 뒤 스낵바를 띄우는 코드에도 화면 생존 확인을 답니다.
없어진 화면 위에 스낵바를 띄우려다 또 다른 자리에서 조용히 실패하는 걸 봤거든요.
크래시는 아닌데, 사용자한테 아무 피드백도 안 가는 상태였습니다.(사실 이 부분은 아직도 케이스마다 다르게 판단합니다.
저장은 살리고 화면 갱신만 조건부로 두는 쪽으로 굳혔지만, 정답이라고는 못 하겠습니다.)지금은 async가 들어간 notifier를 짤 때, await 앞뒤로 "이 사이에 화면이 닫히면?"을 한 번 되뇝니다.
그 질문에 답이 안 나오는 자리에 keepAlive나 mounted 확인이 들어갑니다.
이틀을 태운 버그가 남긴 건, 결국 그 짧은 질문 하나입니다.원문 기준
autoDispose 개념 문서: https://riverpod.dev/docs/concepts2/auto_dispose (확인일 2026-07-01).
keepAlive의 이름이나 시그니처는 메이저 버전에서 바뀔 수 있어, 올리기 전에 현재 문서로 다시 맞춰봅니다.728x90반응형'AI Agent > flutter' 카테고리의 다른 글
Flutter는 widget을 그리는 게 아니라 element/render tree를 갱신합니다 (1) 2026.07.07 post-frame, microtask, animation tick은 같은 큐가 아닙니다 (0) 2026.07.06 로컬 상태는 서버 상태의 대체물이 아니라 임시 복사본입니다 (0) 2026.07.06 Flutter 라우팅은 화면 이동이 아니라 상태와 권한의 계약입니다 (0) 2026.07.05 SafeArea는 항상 안전하지 않습니다 (0) 2026.07.05