들어가며: Riverpod, 왜 다시 주목받고 있을까

Flutter 개발을 시작한 지 3년 차에 접어들면서 상태관리 라이브러리를 정말 많이 갈아탔습니다. setState로 시작해서 Provider, GetX, Bloc까지 다 써봤는데요. 결국 최근 2년 동안은 Riverpod에 정착해서 실무 프로젝트 5개 정도를 운영하고 있습니다. 솔직히 말씀드리면 처음 Riverpod을 봤을 때는 "이걸 왜 또 만든거지?"라는 생각이 들었어요. Provider만 잘 써도 충분하다고 생각했거든요.

그런데 막상 중대형 프로젝트를 진행하면서 Provider의 한계가 명확하게 느껴졌습니다. 컨텍스트 의존성, 컴파일 타임 안전성 부재, ProviderNotFoundException 같은 런타임 에러들이 발목을 잡더라고요. 이런 고민을 해본 분이라면 이번 Flutter Riverpod 가이드가 분명 도움이 될 거예요. 실제로 프로덕션 환경에서 부딪힌 문제들과 해결 과정을 솔직하게 풀어보겠습니다.

Riverpod이 해결하려는 문제

Provider 패키지의 창시자인 Remi Rousselet이 직접 만든 Riverpod은 Provider의 단점을 보완하기 위해 탄생했습니다. 가장 큰 차이점은 BuildContext에 의존하지 않는다는 점이에요. 즉, Widget 트리 밖에서도 상태에 접근할 수 있고, 컴파일 타임에 의존성 오류를 잡아낼 수 있습니다. 제가 실무에서 가장 만족했던 부분이 바로 이 컴파일 타임 안전성이었어요.

현재 버전과 코드 생성 방식

2026년 기준으로 Riverpod 2.x가 안정화되었고, riverpod_generator를 활용한 코드 생성 방식이 표준으로 자리잡았습니다. 처음에는 코드 생성이 번거롭게 느껴졌는데 build_runner watch 모드로 돌려두면 거의 신경 쓸 일이 없더라고요. 오히려 보일러플레이트가 줄어들어서 개발 속도가 빨라졌습니다.

실전 도입 가이드: 프로젝트 세팅부터 첫 Provider까지

이론은 충분히 알아도 막상 코드를 짜려고 하면 막막하죠. 제가 신규 프로젝트를 시작할 때 사용하는 세팅 방식을 공유하겠습니다. 이대로만 따라하면 30분 안에 Riverpod 기반 프로젝트 골격을 잡을 수 있어요.

패키지 설치와 초기 구성

pubspec.yaml에 flutter_riverpod, riverpod_annotation, riverpod_generator, build_runner를 추가합니다. 그리고 반드시 main 함수에서 ProviderScope로 앱을 감싸줘야 해요. 이걸 깜빡해서 30분 헤맸던 적이 있는데, ProviderScope가 없으면 어떤 Provider도 동작하지 않으니 꼭 확인하세요. 또한 lint 규칙으로 custom_lint와 riverpod_lint를 추가하면 실수를 미리 잡아줘서 정말 편합니다.

Provider 종류 선택하기

Riverpod에는 여러 Provider 타입이 있는데 실무에서 자주 쓰는 건 세 가지로 압축됩니다. 단순 읽기 전용 값에는 Provider, 비동기 데이터에는 FutureProvider, 변경 가능한 상태에는 NotifierProvider를 씁니다. StateProvider나 ChangeNotifierProvider는 레거시 호환용이라 신규 프로젝트에서는 거의 안 써요. 처음부터 Notifier 기반으로 가는 게 정신건강에 좋습니다.

실무 디렉토리 구조

저는 features 폴더 아래 도메인별로 폴더를 나누고 그 안에 application, domain, presentation 레이어를 두는 구조를 선호합니다. Provider 파일은 application 폴더에 모아두고 파일명은 _notifier.dart 또는 _provider.dart로 통일했어요. 이렇게 일관성을 유지하면 팀원이 새로 합류해도 어디에 뭐가 있는지 금방 파악할 수 있습니다.

실무에서 바로 써먹는 핵심 팁 5가지

책이나 공식 문서에는 잘 안 나오지만 실제로 부딪혀봐야 알 수 있는 팁들을 정리했습니다. 이 부분이 이 글에서 가장 가치 있는 부분이라고 생각해요.

팁 1: ref.watch와 ref.read 구분하기

이거 헷갈리면 진짜 골치 아픕니다. ref.watch는 빌드 메서드 안에서 위젯이 리빌드되어야 할 때 쓰고, ref.read는 이벤트 핸들러(onPressed 같은 곳) 안에서 한 번만 값을 읽을 때 씁니다. build 메서드 안에서 ref.read를 쓰면 상태 변경이 반영 안 되고, onPressed 안에서 ref.watch를 쓰면 무한 리빌드가 발생할 수 있어요. 저는 처음에 이걸 잘못 써서 위젯이 리빌드 안 되는 버그를 3시간 동안 디버깅한 적이 있습니다.

팁 2: AsyncValue.guard로 에러 처리 깔끔하게

비동기 작업에서 try-catch를 매번 작성하는 건 정말 번거롭죠. AsyncValue.guard 함수를 쓰면 자동으로 로딩, 데이터, 에러 상태를 관리해줍니다. state = const AsyncValue.loading(); state = await AsyncValue.guard(() => repository.fetchData()); 이 두 줄이면 끝나요. 코드가 훨씬 깔끔해지고 에러 처리 누락도 방지됩니다.

팁 3: ref.listen으로 사이드 이펙트 처리

스낵바를 띄우거나 다른 화면으로 이동하는 등의 사이드 이펙트는 build 메서드 안에서 처리하면 안 됩니다. ref.listen을 사용해서 상태 변화에만 반응하도록 만드세요. 특히 인증 상태가 바뀔 때 자동으로 로그인 화면으로 보내거나, API 에러 발생 시 토스트를 띄우는 패턴에 정말 유용합니다.

팁 4: family와 autoDispose 적극 활용

family modifier는 파라미터를 받는 Provider를 만들 때 쓰고, autoDispose는 더 이상 사용되지 않을 때 자동으로 메모리에서 해제해줍니다. 상세 페이지처럼 화면을 나가면 데이터를 버려도 되는 경우 autoDispose를 꼭 붙이세요. 안 그러면 메모리 누수가 생깁니다. 저는 코드 생성 방식을 쓸 때 @riverpod 어노테이션에 keepAlive: true를 명시적으로 설정하지 않는 한 기본적으로 autoDispose 동작을 하도록 통일했습니다.

장단점 비교: 다른 상태관리와 솔직하게 비교해보기

Riverpod이 만능은 아닙니다. 프로젝트 성격에 따라 다른 선택이 더 나을 수도 있어요. 제가 직접 써본 경험을 토대로 비교표를 만들어봤습니다.

항목 Riverpod Provider Bloc GetX
학습 곡선 중간 낮음 높음 낮음
컴파일 타임 안전성 매우 우수 부족 우수 부족
테스트 용이성 매우 우수 보통 매우 우수 보통
보일러플레이트 적음(코드생성) 적음 많음 매우 적음
커뮤니티 활성도 매우 활발 활발 활발 활발
대규모 프로젝트 적합도 매우 적합 중간 매우 적합 부적합

Riverpod의 명확한 장점

솔직하게 말하는 단점

마무리: Riverpod, 누구에게 추천할까

지금까지 Flutter Riverpod의 실전 활용법을 정리해봤습니다. 2년 넘게 실무에서 써본 결론은 "중대형 프로젝트라면 망설일 이유가 없다"입니다. 다만 모든 상황에 정답은 아니에요. 1주일짜리 프로토타입이나 학습용 토이 프로젝트라면 굳이 Riverpod의 모든 기능을 쓸 필요는 없습니다.

이런 분들께 추천합니다

이런 분들께는 비추천

Flutter 생태계에서 상태관리는 정답이 없는 영역이지만, Riverpod은 현 시점에서 가장 균형 잡힌 선택지라고 자신 있게 말씀드릴 수 있습니다. 처음에는 어색해도 한 달만 꾸준히 써보세요. 분명 "왜 진작 안 썼지?"라는 생각이 드실 거예요. 다음 글에서는 Riverpod과 GoRouter를 함께 활용한 라우팅 패턴, 그리고 Riverpod 테스트 코드 작성법을 더 깊이 다뤄보겠습니다. 궁금한 점이나 본인의 Flutter 상태관리 경험이 있다면 댓글로 공유해주세요.