Flutter 개발

Riverpod 심화편2: Firestore 데이터 연동하기 – get과 stream 연결 쉽게 관리하기 (전체 코드 예제 포함)

withmilk 2025. 6. 1. 13:34

지난 글에서 Riverpod의 FutureProvider, StreamProvider, 그리고 NotifierProvider를 활용해 상태 관리를 어떻게 더 쉽게 할 수 있는지 소개해봤어요. 각각의 Provider가 앱의 데이터 흐름과 상태를 얼마나 깔끔하게 관리해주는지 살펴보면서 Riverpod의 강력함을 느꼈을 거예요.

오늘은 그 흐름을 이어서, Firestore 데이터와 Riverpod을 결합해서 앱의 데이터 흐름을 더욱 강력하게 만드는 방법을 보여드리려고 해요. 특히 Firestore 데이터를 가져오는 get 방식과 실시간으로 반영되는 stream 방식을 Riverpod으로 어떻게 관리하면 좋을지, 예제 코드와 함께 자세히 알려드릴게요.

Firestore 데이터를 Riverpod으로 관리하면 앱이 더욱 반응형이 되고 유지보수도 쉬워진답니다. 지금부터 함께 배워볼까요?


🔥 핵심 요약

  • Firestore get 방식: FutureProvider로 한 번만 데이터 가져오기
  • Firestore stream 방식: StreamProvider로 실시간 데이터 업데이트
  • 전체 코드: 폴더 구조와 함께 Firestore 연동 전체 코드 포함

1️⃣ 프로젝트 설명

lib/
 ├─ main.dart
 ├─ providers/
 │    ├─ firestore_get_provider.dart
 │    └─ firestore_stream_provider.dart
 └─ screens/
      └─ home_screen.dart

프로젝트의 코드들은 위와 같은 구조를 가지고 있어요.

 

Firebase의 firestore db를 사용하기 위해서, pubspec.yaml에 패키지를 추가해주세요.

dependencies:
  flutter:
    sdk: flutter
    
  cloud_firestore: ^5.6.8

pubspec.yaml에 firestore db를 추가해준 모습.

 

저는 firestore에서 users라는 collection을 만들고 그 안에 유저 데이터를 추가해주었어요. 다음과 같이요.

firestore에 데이터를 추가해둔 모습.


2️⃣ Firestore get 방식 (FutureProvider)

FutureProvider를 사용해 Firestore의 데이터를 한 번만 가져와서 화면에 표시할 수 있어요.

🔥 providers/firestore_get_provider.dart

import 'package:riverpod/riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

// 특정 사용자 데이터 가져오기 (한 번만)
final userGetProvider = FutureProvider<DocumentSnapshot>((ref) async {
  final doc =
      await FirebaseFirestore.instance
          .collection('users')
          .doc('withMilk')
          .get();
  return doc;
});

Firestore의 "users" 라는 collection에서 "withMilk" 라는 document에서 데이터를 가져올거에요.


3️⃣ Firestore stream 방식 (StreamProvider)

StreamProvider를 사용하면 Firestore의 데이터를 실시간으로 감지해 화면에 반영할 수 있어요.

🔥 providers/firestore_stream_provider.dart

import 'package:riverpod/riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

// users 컬렉션 전체 실시간 데이터 감지
final usersStreamProvider = StreamProvider<QuerySnapshot>((ref) {
  return FirebaseFirestore.instance.collection('users').snapshots();
});

Firestore의 "users" 라는 collection의 모든 데이터를 가져올거에요.


4️⃣ 홈 화면에서 데이터 표시하기

Consumer 위젯을 사용해 userGetProvider와 usersStreamProvider를 화면에 표시해볼게요.

🔥 screens/home_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/firestore_get_provider.dart';
import '../providers/firestore_stream_provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class HomeScreen extends ConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsyncValue = ref.watch(userGetProvider);
    final usersStreamAsyncValue = ref.watch(usersStreamProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Firestore + Riverpod')),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                '👤 특정 사용자 데이터 (get)',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              userAsyncValue.when(
                data: _buildUserInfo,
                loading: () => const CircularProgressIndicator(),
                error: (e, st) => Text('에러: $e'),
              ),
              const SizedBox(height: 20),
              const Text(
                '👥 모든 사용자 데이터 (stream)',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              usersStreamAsyncValue.when(
                data: (snapshot) {
                  final docs = snapshot.docs;
                  return ListView.builder(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    itemCount: docs.length,
                    itemBuilder: (context, index) {
                      return _buildUserInfo(docs[index]);
                    },
                  );
                },
                loading: () => const CircularProgressIndicator(),
                error: (e, st) => Text('에러: $e'),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildUserInfo(DocumentSnapshot snapshot) {
    final userData = snapshot.data() as Map<String, dynamic>;
    return Container(
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey),
        borderRadius: BorderRadius.circular(10),
      ),
      padding: const EdgeInsets.all(10),
      child: Text('이름: ${userData['name']}'),
    );
  }
}

 

userAsyncValue를 통해서 withMilk라는 사용자의 데이터를 보여주고, usersStreamAsyncValue를 통해서 users에 있는 모든 사용자의 데이터를 보여주는 화면이에요.


5️⃣ Riverpod의 .when() 함수로 상태 관리하기

home_screen.dart에서 Firestore 데이터를 가져와 화면에 표시할 때, 우리는 ref.watch(provider)로 상태를 구독해 사용했어요. 그런데 Firestore 데이터는 비동기로 가져오다 보니 데이터가 아직 도착하지 않았거나, 에러가 발생했거나, 정상적으로 데이터가 도착했을 수도 있죠. 이럴 때 Riverpod의 AsyncValue<T> 타입이 데이터를 안전하게 관리해줘요.

그리고 여기서 바로 .when() 함수가 등장합니다! 😎

 

✨ .when() 함수란?

  • AsyncValue<T> 객체는 loading, error, data 상태를 포함한 상태 관리 클래스예요.
  • when() 함수를 사용하면 세 가지 상태를 한꺼번에 처리할 수 있어요:
    • loading: 데이터를 가져오는 중일 때 로딩 UI를 표시
    • error: 데이터 가져오다가 에러가 발생했을 때 에러 UI를 표시
    • data: 데이터가 성공적으로 도착했을 때 UI를 표시

 

📦 사용 예시

userAsyncValue.when(
  data: (snapshot) {
    final userData = snapshot.data() as Map<String, dynamic>;
    return Text('이름: ${userData['name']}');
  },
  loading: () => CircularProgressIndicator(),
  error: (e, st) => Text('에러: $e'),
);
  • data: Firestore에서 받아온 문서(snapshot)가 도착하면 실행.
  • loading: 데이터를 가져오는 동안 로딩 스피너를 보여줌.
  • error: Firestore에서 데이터를 가져오다가 에러가 나면 에러 메시지를 보여줌.

6️⃣ main.dart

마지막으로 main.dart에서 Firebase와 Riverpod을 초기화해줘야 해요.

🔥 main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'screens/home_screens.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firestore + Riverpod',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const HomeScreen(),
    );
  }
}

코드 작성을 다 해주고, flutter run을 통해서 앱을 실행시켜 보면 다음과 같은 화면을 확인할 수 있을거에요.

firestore에 있는 데이터를 riverpod을 사용해서 불러와서 UI에 보여줬어요.

처음 예제 코드를 실행한 모습


7️⃣ stream의 실시간 데이터 반영 동작 확인

이 상태에서 한번 firestore에서 withMilk 유저의 name"우유싫어"로 변경해볼게요.

변경하면 특정 사용자 데이터(get)의 데이터는 변경되지 않지만, 모든 사용자 데이터(stream)의 데이터는 실시간으로 데이터를 받고 있기 때문에 바로 ui에도 데이터의 변경이 반영되는 모습을 확인할 수 있어요.

stream의 데이터만 실시간으로 변경되는 모습

  • get는 딱 get하는 순간의 데이터만 읽어옵니다.
  • stream은 실시간으로 데이터가 변경되는 것을 계속 감시하고 있습니다.

firestore부터의 ui까지 데이터가 보여지는 흐름의 다이어그램


✍️ 마무리하며

이제 Firestore 데이터를 Riverpod과 함께 연동하는 방법을 배웠어요! 😊
지난 글들에서 Riverpod의 기본 개념과 FutureProvider, StreamProvider, NotifierProvider를 배워봤는데요, 이번에는 그 흐름을 이어서 Firestore와 실제로 연결하는 예제를 보여드렸어요.

  • FutureProvider: Firestore에서 데이터를 한 번만 가져올 때 유용해요.
  • StreamProvider: 실시간 데이터 변화를 감지해서 UI를 업데이트해줘요.
  • .when() 함수: 데이터 상태별(loading, error, data)로 UI 처리가 깔끔해져요.

이제 Riverpod으로 Firestore 데이터 연동까지 자연스럽게 할 수 있게 됐네요!
실시간 데이터가 필요한 채팅 앱, 공지사항, 댓글 알림 등에도 쉽게 적용할 수 있을 거예요.

다음 글에서는 또 다른 Riverpod의 강력한 기능이나 앱 개발 팁을 들고 찾아올게요!
질문이나 다뤄줬으면 하는 주제가 있다면 언제든 댓글로 알려주세요! 🙌