Flutter로 앱을 만들다 보면, 대부분의 초보 개발자들이 한 번쯤 마주하게 되는 질문이 있습니다. “그럼 이 데이터를 어디에 저장하지?” 단순히 로컬에만 저장해서는 안드로이드 기기를 바꾸거나 앱을 재설치했을 때 데이터가 사라지게 됩니다. 실제 서비스로 확장하려면 백엔드 서버가 필요하지만, 백엔드 구축은 프론트엔드 개발자 입장에서 결코 가볍지 않은 일이죠.
이럴 때 등장하는 것이 바로 Firebase Firestore입니다. 별도의 서버 구축 없이도 실시간 데이터 저장 및 동기화가 가능한 NoSQL 기반의 클라우드 데이터베이스로, Flutter와의 궁합도 매우 좋습니다. 이번 글에서는 Firestore가 무엇인지, 어떻게 연동하고 사용하는지, 그리고 CRUD 구현부터 실시간 스트림 활용까지 실전 중심으로 알아보겠습니다.
Firebase와 Firestore의 개념 이해
Firebase는 2011년에 탄생한 모바일 및 웹 애플리케이션 개발 플랫폼으로, 2014년 구글에 인수되며 전 세계 개발자들이 즐겨 찾는 서비스로 자리 잡았습니다. Firebase의 강점은 백엔드 기능을 쉽게 사용할 수 있도록 추상화해 놓았다는 점인데요, 회원가입·로그인(Authentication), 데이터 저장(Firestore), 파일 업로드(Storage) 등 다양한 기능을 손쉽게 연동할 수 있습니다.
그중에서도 Firestore는 실시간 동기화가 가능하고, 복잡한 서버 설정 없이도 앱 데이터를 클라우드에 안전하게 저장할 수 있는 NoSQL 기반 데이터베이스입니다. 기존의 SQL 기반 DB처럼 고정된 스키마 없이 자유롭게 데이터를 다룰 수 있으며, 오프라인 상태에서도 읽기와 쓰기가 가능하고, 이후 온라인으로 전환되면 자동으로 동기화된다는 장점이 있습니다.
Firestore의 기본 구조는 컬렉션(Collection)과 문서(Document)입니다. 쉽게 말해, 컬렉션은 폴더처럼 문서들을 모아두는 단위이고, 각 문서는 key-value 쌍의 JSON 형태로 데이터를 담고 있습니다. 예를 들어, 블로그 앱을 만든다면 posts라는 컬렉션 아래에 여러 개의 블로그 포스트 문서를 저장하는 방식으로 구성할 수 있겠죠.
Firestore 기본 연동 및 CRUD 구현
Firebase Firestore는 데이터를 저장하고 읽는 기본적인 CRUD 기능을 제공합니다. 이를 Flutter에서 사용하려면 먼저 Firebase 콘솔과 Flutter 프로젝트를 연동해야 하며, 이후 cloud_firestore 패키지를 사용해 데이터 작업을 수행합니다.
Firestore에서는 데이터가 문서(Document) 단위로 저장되고, 문서들은 컬렉션(Collection) 단위로 묶입니다. 예를 들어 블로그 포스트 앱이라면, posts라는 컬렉션 아래에 각각의 포스트가 문서로 저장되는 구조입니다.
아래 코드는 Firestore와 Flutter를 연동하고, CRUD 기능을 구현한 대표적인 예시입니다.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_firebase_blog_app/data/model/post.dart';
class PostRepository {
const PostRepository();
// 전체 문서 조회
Future<List<Post>> getAll() async {
final collectionRef = FirebaseFirestore.instance.collection('posts');
final snapshot = await collectionRef.get();
final docs = snapshot.docs;
final list = docs.map((e) {
final map = {
'id': e.id,
...e.data(),
};
return Post.fromJson(map);
}).toList();
return list;
}
// 단일 문서 조회
Future<Post?> getOne(String id) async {
try {
final snapshot =
await FirebaseFirestore.instance.collection('posts').doc(id).get();
return Post.fromJson({
'id': snapshot.id,
...snapshot.data()!,
});
} catch (e) {
print(e);
return null;
}
}
// 문서 생성
Future<bool> insert({
required String title,
required String content,
required String writer,
required String imgUrl,
}) async {
try {
final collectionRef = FirebaseFirestore.instance.collection('posts');
final docRef = collectionRef.doc();
final map = {
'title': title,
'content': content,
'writer': writer,
'createdAt': DateTime.now().toIso8601String(),
'imgUrl': imgUrl,
};
await docRef.set(map);
return true;
} catch (e) {
print(e);
return false;
}
}
// 문서 수정
Future<bool> update({
required String id,
required String writer,
required String title,
required String content,
required String imgUrl,
}) async {
try {
final docRef = FirebaseFirestore.instance.collection('posts').doc(id);
await docRef.update({
'writer': writer,
'title': title,
'content': content,
'imgUrl': imgUrl,
});
return true;
} catch (e) {
print(e);
return false;
}
}
// 문서 삭제
Future<bool> delete(String id) async {
try {
final docRef = FirebaseFirestore.instance.collection('posts').doc(id);
await docRef.delete();
return true;
} catch (e) {
print('$e');
return false;
}
}
}
Firestore 실시간 스트림 적용 및 ViewModel 연동
Firestore는 실시간 데이터베이스처럼 작동할 수 있습니다. 컬렉션이나 문서에 변화가 생기면, 앱에서 이를 자동으로 감지하여 상태를 업데이트할 수 있도록 스트림(Stream)을 활용합니다. 이를 통해 사용자는 앱을 새로고침하지 않아도 변경된 내용을 실시간으로 확인할 수 있습니다.
아래는 postListStream() 과 postStream()을 활용한 실시간 업데이트 코드와, 이를 ViewModel에 반영하는 코드입니다.
// post_repository.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_firebase_blog_app/data/model/post.dart';
class PostRepository {
const PostRepository();
// 포스트 목록 실시간 스트림
Stream<List<Post>> postListStream() {
final collectionRef = FirebaseFirestore.instance
.collection('posts')
.orderBy('createdAt', descending: true);
return collectionRef.snapshots().map((snapshot) {
return snapshot.docs.map((doc) {
return Post.fromJson({'id': doc.id, ...doc.data()});
}).toList();
});
}
// 특정 포스트 스트림
Stream<Post?> postStream(String id) {
final docRef = FirebaseFirestore.instance.collection('posts').doc(id);
return docRef.snapshots().map((snapshot) {
if (snapshot.data() == null) return null;
return Post.fromJson({'id': snapshot.id, ...snapshot.data()!});
});
}
}
// home_view_model.dart
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_firebase_blog_app/data/model/post.dart';
import 'package:flutter_firebase_blog_app/data/repository/post_repository.dart';
class HomeViewModel extends Notifier<List<Post>> {
HomeViewModel();
final postRepository = const PostRepository();
@override
List<Post> build() {
_listenToPosts();
return [];
}
void _listenToPosts() {
final stream = postRepository.postListStream();
final subscription = stream.listen((postList) {
state = postList;
});
ref.onDispose(() {
subscription.cancel();
});
}
}
final homeViewModel =
NotifierProvider<HomeViewModel, List<Post>>(() => HomeViewModel());
// detail_view_model.dart
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_firebase_blog_app/data/model/post.dart';
import 'package:flutter_firebase_blog_app/data/repository/post_repository.dart';
class DetailViewModel extends AutoDisposeFamilyNotifier<Post?, Post> {
@override
Post? build(Post arg) {
_listenToPost();
return arg;
}
final postRepository = const PostRepository();
void _listenToPost() {
final stream = postRepository.postStream(arg.id);
final subscription = stream.listen((post) {
state = post;
});
ref.onDispose(() {
subscription.cancel();
});
}
Future<bool> delete() async {
return await postRepository.delete(arg.id);
}
}
final detailViewModel = NotifierProvider.autoDispose.family<DetailViewModel, Post?, Post>(
() => DetailViewModel(),
);
마치며...
Firebase Firestore를 활용하면 복잡한 서버 인프라를 구축하지 않고도, 강력한 데이터 저장 및 실시간 동기화 기능을 갖춘 앱을 빠르게 만들 수 있습니다. 특히 Flutter와의 연동이 매우 직관적이며, Riverpod과 같은 상태 관리 라이브러리와 함께 쓰면 구조적으로도 깔끔한 앱을 설계할 수 있습니다.
CRUD는 기본 중의 기본이고, 스트림을 통한 실시간 업데이트는 사용자 경험을 한 단계 더 끌어올리는 핵심 기능입니다. 지금까지 소개한 예제를 바탕으로, 여러분만의 기능을 조금씩 붙여보면 완성도 높은 앱을 충분히 만들 수 있습니다.
'IT' 카테고리의 다른 글
Docker에 대해 알아보자! (2) | 2025.04.16 |
---|---|
코딩 몰라도 개발하는 시대, 바이브 코딩 (2) | 2025.04.15 |
DIO를 활용한 HTTP 통신 (0) | 2025.04.14 |
MVVM 아키텍처와 전역 상태 관리 (0) | 2025.04.11 |
Dart 데이터 직렬화와 역직렬화 (0) | 2025.04.10 |