Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기

Cloud Firestore 데이터 번들

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

Cloud Firestore 데이터 번들은 Cloud Firestore 문서 및 쿼리 스냅샷에서 빌드하고 CDN, 호스팅 서비스 또는 기타 솔루션에 게시하는 정적 데이터 파일입니다. 데이터 번들은 클라이언트 앱에 제공하려는 문서와 이를 생성한 쿼리에 대한 메타데이터를 모두 포함합니다. 클라이언트 SDK를 사용하여 네트워크 또는 로컬 저장소에서 번들을 다운로드한 후 번들 데이터를 Cloud Firestore 로컬 캐시에 로드합니다. 번들이 로드되면 클라이언트 앱은 로컬 캐시 또는 백엔드에서 문서를 쿼리할 수 있습니다.

데이터 번들을 사용하면 Cloud Firestore 백엔드를 호출할 필요 없이 시작할 때 문서를 사용할 수 있으므로 앱에서 일반적인 쿼리 결과를 더 빨리 로드할 수 있습니다. 결과가 로컬 캐시에서 로드되는 경우 액세스 비용도 절감됩니다. 동일한 초기 100개 문서를 쿼리하기 위해 백만 개의 앱 인스턴스에 대해 비용을 지불하는 대신 해당 100개 문서를 묶는 데 필요한 쿼리에 대해서만 비용을 지불합니다.

Cloud Firestore 데이터 번들은 다른 Firebase 백엔드 제품과 원활하게 작동하도록 구축되었습니다. 번들이 Cloud Functions에 의해 구축되고 Firebase 호스팅을 통해 사용자에게 제공되는 통합 솔루션 을 살펴보세요.

앱에서 번들을 사용하려면 다음 세 단계를 거쳐야 합니다.

  1. Admin SDK로 번들 빌드
  2. 로컬 스토리지 또는 CDN에서 번들 제공
  3. 클라이언트에서 번들 로드

데이터 번들이란 무엇입니까?

데이터 번들은 하나 이상의 문서 및/또는 쿼리 스냅샷 을 패키징하고 명명된 쿼리 를 추출할 수 있도록 사용자가 작성한 정적 바이너리 파일입니다. 아래에서 설명하는 것처럼 서버측 SDK를 사용하면 번들을 빌드할 수 있고 클라이언트 SDK는 번들을 로컬 캐시에 로드할 수 있는 메서드를 제공합니다.

명명된 쿼리는 번들의 특히 강력한 기능입니다. 명명된 쿼리는 Cloud Firestore와 통신하는 앱의 모든 부분에서 일반적으로 수행하는 것처럼 번들에서 추출한 다음 즉시 사용하여 캐시 또는 백엔드에서 데이터를 쿼리할 수 있는 Query 개체입니다.

서버에 데이터 번들 구축

Node.js 또는 Java 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();
      
파이썬
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()
      

데이터 번들 제공

CDN에서 또는 예를 들어 Cloud Storage에서 번들을 다운로드하여 클라이언트 앱에 번들을 제공할 수 있습니다.

이전 섹션에서 만든 번들이 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 요청을 하거나 스토리지 API를 호출하거나 네트워크에서 바이너리 파일을 가져오는 다른 기술을 사용하여 Firestore 번들을 원격 서버에서 가져와서 로드합니다.

가져오면 앱은 Cloud Firestore 클라이언트 SDK를 사용하여 작업 추적 개체를 반환하는 loadBundle 메서드를 호출합니다. 이 개체의 완료는 Promise 상태를 모니터링하는 것처럼 모니터링할 수 있습니다. 번들 로드 작업이 성공적으로 완료되면 로컬 캐시에서 번들 콘텐츠를 사용할 수 있습니다.

편물
// 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
  // ...
}
빠른
참고: 이 제품은 watchOS 및 App Clip 대상에서 사용할 수 없습니다.
// Utility function for errors when loading bundles.
func bundleLoadError(reason: String) -> NSError {
  return NSError(domain: "FIRSampleErrorDomain",
                 code: 0,
                 userInfo: [NSLocalizedFailureReasonErrorKey: reason])
}

// Loads a remote bundle from the provided url.
func fetchRemoteBundle(for firestore: Firestore,
                       from url: URL,
                       completion: @escaping ((Result<LoadBundleTaskProgress, Error>) -> ())) {
  guard let inputStream = InputStream(url: url) else {
    let error = self.bundleLoadError(reason: "Unable to create stream from the given url: \(url)")
    completion(.failure(error))
    return
  }

  // The return value of this function is ignored, but can be used for more granular
  // bundle load observation.
  let _ = firestore.loadBundle(inputStream) { (progress, error) in
    switch (progress, error) {

    case (.some(let value), .none):
      if value.state == .success {
        completion(.success(value))
      } else {
        let concreteError = self.bundleLoadError(
          reason: "Expected bundle load to be completed, but got \(value.state) instead"
        )
        completion(.failure(concreteError))
      }

    case (.none, .some(let concreteError)):
      completion(.failure(concreteError))

    case (.none, .none):
      let concreteError = self.bundleLoadError(reason: "Operation failed, but returned no error.")
      completion(.failure(concreteError))

    case (.some(let value), .some(let concreteError)):
      let concreteError = self.bundleLoadError(
        reason: "Operation returned error \(concreteError) with nonnull progress: \(value)"
      )
      completion(.failure(concreteError))
    }
  }
}

// Fetches a specific named query from the provided bundle.
func loadQuery(named queryName: String,
               fromRemoteBundle bundleURL: URL,
               with store: Firestore,
               completion: @escaping ((Result<Query, Error>) -> ())) {
  fetchRemoteBundle(for: store,
                    from: bundleURL) { (result) in
    switch result {
    case .success:
      store.getQuery(named: queryName) { query in
        if let query = query {
          completion(.success(query))
        } else {
          completion(
            .failure(
              self.bundleLoadError(reason: "Could not find query named \(queryName)")
            )
          )
        }
      }

    case .failure(let error):
      completion(.failure(error))
    }
  }
}

// Load a query and fetch its results from a bundle.
func runStoriesQuery() {
  let queryName = "latest-stories-query"
  let firestore = Firestore.firestore()
  let remoteBundle = URL(string: "https://example.com/createBundle")!
  loadQuery(named: queryName,
            fromRemoteBundle: remoteBundle,
            with: firestore) { (result) in
    switch result {
    case .failure(let error):
      print(error)

    case .success(let query):
      query.getDocuments { (snapshot, error) in

        // handle query results

      }
    }
  }
}
목표-C
참고: 이 제품은 watchOS 및 App Clip 대상에서 사용할 수 없습니다.
// 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
            // ...
        }
    });
}
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분 이내에 빌드된 번들에서 명명된 쿼리를 로드하는 경우 이를 사용하여 캐시가 아닌 백엔드에서 읽으면 백엔드에 저장된 내용과 일치하도록 문서를 업데이트하는 데 필요한 데이터베이스 읽기에 대해서만 비용을 지불하게 됩니다. ; 즉, 델타에 대해서만 비용을 지불합니다.

다음은?