Cloud Firestore 데이터 번들은 Cloud Firestore 문서 및 쿼리 스냅샷에서 사용자가 빌드한 정적 데이터 파일로, 사용자가 CDN, 호스팅 서비스 또는 기타 솔루션에 게시합니다. 데이터 번들에는 클라이언트 앱에 제공하려는 문서 및 이를 생성한 쿼리에 대한 메타데이터가 모두 포함됩니다. 클라이언트 SDK를 사용하여 네트워크를 통해서 또는 로컬 스토리지에서 번들을 다운로드한 후 번들 데이터를 Cloud Firestore 로컬 캐시에 로드합니다. 번들이 로드되면 클라이언트 앱이 로컬 캐시 또는 백엔드에서 문서를 쿼리할 수 있습니다.
데이터 번들을 사용하면 Cloud Firestore 백엔드 호출 없이도 시작 시에 문서를 사용할 수 있으므로 앱에서 일반 쿼리 결과를 더 빨리 로드할 수 있습니다. 또한 결과가 로컬 캐시에서 로드되면 액세스 비용도 줄어들게 됩니다. 동일한 최초의 100개 문서를 쿼리하는 100만 개의 앱 인스턴스에 대해 비용을 지불하는 대신 해당하는 100개 문서를 번들로 만드는 데 필요한 쿼리에 대해서만 비용을 지불하면 됩니다.
Cloud Firestore 데이터 번들은 다른 Firebase 백엔드 제품과도 잘 작동하도록 빌드되었습니다. 번들이 Cloud Functions에서 빌드되고 Firebase Hosting을 통해 사용자에게 제공되는 통합 솔루션을 살펴보세요.
앱에 번들을 사용하려면 다음 3단계를 따르세요.
- Admin SDK로 번들 빌드
- 로컬 스토리지 또는 CDN에서 번들 제공
- 클라이언트에 번들 로드
데이터 번들이란?
데이터 번들은 하나 이상의 문서 및 쿼리 스냅샷을 패키지로 만들기 위해 사용자가 빌드하고 이름이 지정된 쿼리를 추출할 수 있는 정적 바이너리 파일입니다. 아래 설명과 같이 서버 측 SDK를 사용하면 번들을 빌드할 수 있으며 클라이언트 SDK를 통해 번들을 로컬 캐시에 로드할 수 있습니다.
이름이 지정된 쿼리는 번들의 특히 강력한 기능 중 하나입니다. 이름이 지정된 쿼리는 번들에서 추출한 다음, 일반적으로 Cloud Firestore와 커뮤니케이션하는 앱의 특정 부분에서 쿼리하는 것과 같이 캐시나 백엔드에서 바로 데이터 쿼리에 사용할 수 있는 Query
객체입니다.
서버에서 데이터 번들 빌드
Node.js 또는 자바 Admin SDK를 사용하면 사용자가 번들에 포함할 항목과 제공 방법을 완벽하게 제어할 수 있습니다.
Node.js
var bundleId = "latest-stories"; var bundle = firestore.bundle(bundleId); var docSnapshot = await firestore.doc('stories/stories').get(); var querySnapshot = await firestore.collection('stories').get(); // Build the bundle // Note how querySnapshot is named "latest-stories-query" var bundleBuffer = bundle.add(docSnapshot); // Add a document .add('latest-stories-query', querySnapshot) // Add a named query. .build()
자바
Firestore db = FirestoreClient.getFirestore(app); // Query the 50 latest stories QuerySnapshot latestStories = db.collection("stories") .orderBy("timestamp", Direction.DESCENDING) .limit(50) .get() .get(); // Build the bundle from the query results FirestoreBundle bundle = db.bundleBuilder("latest-stories") .add("latest-stories-query", latestStories) .build();
Python
from google.cloud import firestore from google.cloud.firestore_bundle import FirestoreBundle db = firestore.Client() bundle = FirestoreBundle("latest-stories") doc_snapshot = db.collection("stories").document("news-item").get() query = db.collection("stories")._query() # Build the bundle # Note how `query` is named "latest-stories-query" bundle_buffer: str = bundle.add_document(doc_snapshot).add_named_query( "latest-stories-query", query, ).build()
데이터 번들 제공
Cloud Storage와 같은 서비스에서 번들을 다운로드하는 방식으로 또는 CDN에서 클라이언트 앱에 번들을 제공할 수 있습니다.
이전 섹션에서 만든 번들이 bundle.txt
라는 파일에 저장되고 서버에 게시되었다고 가정해 보겠습니다. 이 번들 파일은 여기에서 설명한 간단한 Node.js Express 앱의 경우와 같이 웹을 통해 제공할 수 있는 기타 애셋과 유사합니다.
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) => {
const src = fs.createReadStream('./bundle.txt');
src.pipe(res);
});
server.listen(8000);
클라이언트에서 데이터 번들 로드
HTTP 요청, Storage API 호출 또는 네트워크에서 바이너리 파일을 가져오는 기타 기법 사용 여부에 관계없이 원격 서버에서 Firestore 번들을 가져와 로드할 수 있습니다.
가져온 후에는 Cloud Firestore 클라이언트 SDK를 사용해 앱에서 태스크 추적 개체를 반환하는 loadBundle
메서드를 호출하고, 반환된 객체를 통해 프로미스의 상태를 모니터링하는 것과 같이 완료 상태를 모니터링할 수 있습니다.
번들 로드 작업이 성공적으로 완료되면 로컬 캐시에서 번들 콘텐츠를 사용할 수 있습니다.
Web
import { loadBundle, namedQuery, getDocsFromCache } from "firebase/firestore"; async function fetchFromBundle() { // Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache' // response header will be set to 'HIT' const resp = await fetch('/createBundle'); // Load the bundle contents into the Firestore SDK await loadBundle(db, resp.body); // Query the results from the cache const query = await namedQuery(db, 'latest-stories-query'); const storiesSnap = await getDocsFromCache(query); // Use the results // ... }
Web
// If you are using module bundlers. import firebase from "firebase/app"; import "firebase/firestore"; import "firebase/firestore/bundle"; // This line enables bundle loading as a side effect. // ... async function fetchFromBundle() { // Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache' // response header will be set to 'HIT' const resp = await fetch('/createBundle'); // Load the bundle contents into the Firestore SDK await db.loadBundle(resp.body); // Query the results from the cache // Note: omitting "source: cache" will query the Firestore backend. const query = await db.namedQuery('latest-stories-query'); const storiesSnap = await query.get({ source: 'cache' }); // Use the results // ... }
Swift
// Utility function for errors when loading bundles. func bundleLoadError(reason: String) -> NSError { return NSError(domain: "FIRSampleErrorDomain", code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: reason]) } func fetchRemoteBundle(for firestore: Firestore, from url: URL) async throws -> LoadBundleTaskProgress { guard let inputStream = InputStream(url: url) else { let error = self.bundleLoadError(reason: "Unable to create stream from the given url: \(url)") throw error } return try await firestore.loadBundle(inputStream) } // Fetches a specific named query from the provided bundle. func loadQuery(named queryName: String, fromRemoteBundle bundleURL: URL, with store: Firestore) async throws -> Query { let _ = try await fetchRemoteBundle(for: store, from: bundleURL) if let query = await store.getQuery(named: queryName) { return query } else { throw bundleLoadError(reason: "Could not find query named \(queryName)") } } // Load a query and fetch its results from a bundle. func runStoriesQuery() async { let queryName = "latest-stories-query" let firestore = Firestore.firestore() let remoteBundle = URL(string: "https://example.com/createBundle")! do { let query = try await loadQuery(named: queryName, fromRemoteBundle: remoteBundle, with: firestore) let snapshot = try await query.getDocuments() print(snapshot) // handle query results } catch { print(error) } }
Objective-C
// Utility function for errors when loading bundles. - (NSError *)bundleLoadErrorWithReason:(NSString *)reason { return [NSError errorWithDomain:@"FIRSampleErrorDomain" code:0 userInfo:@{NSLocalizedFailureReasonErrorKey: reason}]; } // Loads a remote bundle from the provided url. - (void)fetchRemoteBundleForFirestore:(FIRFirestore *)firestore fromURL:(NSURL *)url completion:(void (^)(FIRLoadBundleTaskProgress *_Nullable, NSError *_Nullable))completion { NSInputStream *inputStream = [NSInputStream inputStreamWithURL:url]; if (inputStream == nil) { // Unable to create input stream. NSError *error = [self bundleLoadErrorWithReason: [NSString stringWithFormat:@"Unable to create stream from the given url: %@", url]]; completion(nil, error); return; } [firestore loadBundleStream:inputStream completion:^(FIRLoadBundleTaskProgress * _Nullable progress, NSError * _Nullable error) { if (progress == nil) { completion(nil, error); return; } if (progress.state == FIRLoadBundleTaskStateSuccess) { completion(progress, nil); } else { NSError *concreteError = [self bundleLoadErrorWithReason: [NSString stringWithFormat: @"Expected bundle load to be completed, but got %ld instead", (long)progress.state]]; completion(nil, concreteError); } completion(nil, nil); }]; } // Loads a bundled query. - (void)loadQueryNamed:(NSString *)queryName fromRemoteBundleURL:(NSURL *)url withFirestore:(FIRFirestore *)firestore completion:(void (^)(FIRQuery *_Nullable, NSError *_Nullable))completion { [self fetchRemoteBundleForFirestore:firestore fromURL:url completion:^(FIRLoadBundleTaskProgress *progress, NSError *error) { if (error != nil) { completion(nil, error); return; } [firestore getQueryNamed:queryName completion:^(FIRQuery *query) { if (query == nil) { NSString *errorReason = [NSString stringWithFormat:@"Could not find query named %@", queryName]; NSError *error = [self bundleLoadErrorWithReason:errorReason]; completion(nil, error); return; } completion(query, nil); }]; }]; } - (void)runStoriesQuery { NSString *queryName = @"latest-stories-query"; FIRFirestore *firestore = [FIRFirestore firestore]; NSURL *bundleURL = [NSURL URLWithString:@"https://example.com/createBundle"]; [self loadQueryNamed:queryName fromRemoteBundleURL:bundleURL withFirestore:firestore completion:^(FIRQuery *query, NSError *error) { // Handle query results }]; }
Kotlin+KTX
@Throws(IOException::class) fun getBundleStream(urlString: String?): InputStream { val url = URL(urlString) val connection = url.openConnection() as HttpURLConnection return connection.inputStream } @Throws(IOException::class) fun fetchFromBundle() { val bundleStream = getBundleStream("https://example.com/createBundle") val loadTask = db.loadBundle(bundleStream) // Chain the following tasks // 1) Load the bundle // 2) Get the named query from the local cache // 3) Execute a get() on the named query loadTask.continueWithTask<Query> { task -> // Close the stream bundleStream.close() // Calling .result propagates errors val progress = task.getResult(Exception::class.java) // Get the named query from the bundle cache db.getNamedQuery("latest-stories-query") }.continueWithTask { task -> val query = task.getResult(Exception::class.java)!! // get() the query results from the cache query.get(Source.CACHE) }.addOnCompleteListener { task -> if (!task.isSuccessful) { Log.w(TAG, "Bundle loading failed", task.exception) return@addOnCompleteListener } // Get the QuerySnapshot from the bundle val storiesSnap = task.result // Use the results // ... } }
Java
public InputStream getBundleStream(String urlString) throws IOException { URL url = new URL(urlString); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); return connection.getInputStream(); } public void fetchBundleFrom() throws IOException { final InputStream bundleStream = getBundleStream("https://example.com/createBundle"); LoadBundleTask loadTask = db.loadBundle(bundleStream); // Chain the following tasks // 1) Load the bundle // 2) Get the named query from the local cache // 3) Execute a get() on the named query loadTask.continueWithTask(new Continuation<LoadBundleTaskProgress, Task<Query>>() { @Override public Task<Query> then(@NonNull Task<LoadBundleTaskProgress> task) throws Exception { // Close the stream bundleStream.close(); // Calling getResult() propagates errors LoadBundleTaskProgress progress = task.getResult(Exception.class); // Get the named query from the bundle cache return db.getNamedQuery("latest-stories-query"); } }).continueWithTask(new Continuation<Query, Task<QuerySnapshot>>() { @Override public Task<QuerySnapshot> then(@NonNull Task<Query> task) throws Exception { Query query = task.getResult(Exception.class); // get() the query results from the cache return query.get(Source.CACHE); } }).addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() { @Override public void onComplete(@NonNull Task<QuerySnapshot> task) { if (!task.isSuccessful()) { Log.w(TAG, "Bundle loading failed", task.getException()); return; } // Get the QuerySnapshot from the bundle QuerySnapshot storiesSnap = task.getResult(); // Use the results // ... } }); }
Dart
// Get a bundle from a server final url = Uri.https('example.com', '/create-bundle'); final response = await http.get(url); String body = response.body; final buffer = Uint8List.fromList(body.codeUnits); // Load a bundle from a buffer LoadBundleTask task = FirebaseFirestore.instance.loadBundle(buffer); await task.stream.toList(); // Use the cached named query final results = await FirebaseFirestore.instance.namedQueryGet( "latest-stories-query", options: const GetOptions( source: Source.cache, ), );
C++
db->LoadBundle("bundle_name", [](const LoadBundleTaskProgress& progress) { switch(progress.state()) { case LoadBundleTaskProgress::State::kError: { // The bundle load has errored. Handle the error in the returned future. return; } case LoadBundleTaskProgress::State::kInProgress: { std::cout << "Bytes loaded from bundle: " << progress.bytes_loaded() << std::endl; break; } case LoadBundleTaskProgress::State::kSuccess: { std::cout << "Bundle load succeeeded" << std::endl; break; } } }).OnCompletion([db](const Future<LoadBundleTaskProgress>& future) { if (future.error() != Error::kErrorOk) { // Handle error... return; } const std::string& query_name = "latest_stories_query"; db->NamedQuery(query_name).OnCompletion([](const Future<Query>& query_future){ if (query_future.error() != Error::kErrorOk) { // Handle error... return; } const Query* query = query_future.result(); query->Get().OnCompletion([](const Future<QuerySnapshot> &){ // ... }); }); });
30분 이내에 빌드한 번들에서 이름이 지정된 쿼리를 로드하는 경우 캐시가 아닌 백엔드에서 쿼리를 읽으면 백엔드에 저장된 항목과 일치하도록 문서를 업데이트하는 데 필요한 데이터베이스 읽기에 대한 비용만 지불하면 됩니다. 즉, 델타에 대해서만 비용을 지불합니다.
다음 단계
클라이언트 측(Apple, Android, 웹) 및 서버 측(Node.js)의 데이터 번들 API 참조 문서를 확인하세요.
아직 살펴보지 않았다면 번들 빌드 및 제공을 위한 Cloud Functions 및 Firebase Hosting 솔루션을 살펴보세요.