대부분의 애플리케이션에서는 페이지가 처음 로드될 때 모든 사용자에게 동일한 콘텐츠를 제공합니다. 예를 들어 뉴스 사이트에 최신 뉴스를 보여주거나 전자상거래 사이트에서 가장 잘 팔리는 상품을 보여주는 경우입니다.
이 콘텐츠가 Cloud Firestore에서 제공된다면 각 사용자는 애플리케이션을 로드할 때 동일한 결과를 위해 새 쿼리를 실행합니다. 이러한 결과는 사용자 간에 캐시되지 않으므로 애플리케이션은 필요한 것보다 느리고 비용이 많이 발생합니다.
솔루션: 번들
Cloud Firestore 번들을 사용하면 Firebase Admin SDK를 사용하여 백엔드의 일반적인 쿼리 결과에서 데이터 번들을 조합할 수 있으며 CDN에 캐시된 사전 계산된 blob을 제공할 수 있습니다. 이를 통해 사용자에게 훨씬 빠른 로드 환경을 제공하고 Cloud Firestore 쿼리 비용을 줄일 수 있습니다.
이 가이드에서는 Cloud Functions를 사용하여 번들을 생성하고 Firebase Hosting을 사용하여 번들 콘텐츠를 동적으로 캐싱하고 제공합니다. 번들에 대한 자세한 내용은 가이드를 참조하세요.
먼저 간단한 공개 HTTP 함수를 만들어 최근 50개의 '뉴스'를 쿼리하고 결과를 번들로 제공합니다.
Node.js
exports.createBundle=functions.https.onRequest(async(request,response)=>{// Query the 50 latest storiesconstlatestStories=awaitdb.collection('stories').orderBy('timestamp','desc').limit(50).get();// Build the bundle from the query resultsconstbundleBuffer=db.bundle('latest-stories').add('latest-stories-query',latestStories).build();// Cache the response for up to 5 minutes;// see https://firebase.google.com/docs/hosting/manage-cacheresponse.set('Cache-Control','public, max-age=300, s-maxage=600');response.end(bundleBuffer);});
자바
packagecom.example;importcom.google.auth.oauth2.GoogleCredentials;importcom.google.cloud.firestore.Firestore;importcom.google.cloud.firestore.FirestoreBundle;importcom.google.cloud.firestore.Query.Direction;importcom.google.cloud.firestore.QuerySnapshot;importcom.google.cloud.functions.HttpFunction;importcom.google.cloud.functions.HttpRequest;importcom.google.cloud.functions.HttpResponse;importcom.google.firebase.FirebaseApp;importcom.google.firebase.FirebaseOptions;importcom.google.firebase.cloud.FirestoreClient;importjava.io.BufferedWriter;importjava.io.IOException;publicclassExampleFunctionimplementsHttpFunction{publicstaticFirebaseAppinitializeFirebase()throwsIOException{if(FirebaseApp.getApps().isEmpty()){FirebaseOptionsoptions=FirebaseOptions.builder().setCredentials(GoogleCredentials.getApplicationDefault()).setProjectId("YOUR-PROJECT-ID").build();FirebaseApp.initializeApp(options);}returnFirebaseApp.getInstance();}@Overridepublicvoidservice(HttpRequestrequest,HttpResponseresponse)throwsException{// Get a Firestore instanceFirebaseAppapp=initializeFirebase();Firestoredb=FirestoreClient.getFirestore(app);// Query the 50 latest storiesQuerySnapshotlatestStories=db.collection("stories").orderBy("timestamp",Direction.DESCENDING).limit(50).get().get();// Build the bundle from the query resultsFirestoreBundlebundle=db.bundleBuilder("latest-stores").add("latest-stories-query",latestStories).build();// Cache the response for up to 5 minutes// see https://firebase.google.com/docs/hosting/manage-cacheresponse.appendHeader("Cache-Control","public, max-age=300, s-maxage=600");// Write the bundle to the HTTP responseBufferedWriterwriter=response.getWriter();writer.write(newString(bundle.toByteBuffer().array()));}}
그런 다음 firebase.json을 수정하여 이 Cloud 함수를 제공하고 캐시하도록 Firebase 호스팅을 구성합니다. 이 구성을 사용하면 Firebase Hosting CDN에서 Cloud 함수로 설정한 캐시 설정에 따라 번들 콘텐츠를 제공합니다. 캐시가 만료되면 함수를 다시 트리거하여 콘텐츠를 새로고칩니다.
마지막으로 웹 애플리케이션에서 CDN에서 번들 콘텐츠를 가져와 Firestore SDK에 로드합니다.
// If you are using module bundlers.importfirebasefrom"firebase/app";import"firebase/firestore";import"firebase/firestore/bundle"// This line enables bundle loading as a side effect.asyncfunctionfetchFromBundle(){// Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'// response header will be set to 'HIT'constresp=awaitfetch('/createBundle');// Load the bundle contents into the Firestore SDKawaitdb.loadBundle(resp.body);// Query the results from the cache// Note: omitting "source: cache" will query the Firestore backend.constquery=awaitdb.namedQuery('latest-stories-query');conststoriesSnap=awaitquery.get({source:'cache'});// Use the results// ...}
예상 절감액
하루에 100,000명의 사용자가 유입되고 각 사용자가 최초 로드 시 동일한 50개의 주요 뉴스를 로드하는 뉴스 웹사이트를 가정해 보겠습니다. 캐싱이 없으면 Cloud Firestore에서 하루에 50 x 100,000 = 5,000,000회의 문서 읽기가 발생합니다.
이제 사이트에서 위의 기법을 채택하고 50개의 결과를 최대 5분 동안 캐시한다고 가정해 보겠습니다. 따라서 모든 사용자에게 쿼리 결과를 로드하는 대신 결과가 정확히 시간당 12회 로드됩니다. 사이트에 얼마나 많은 사용자가 유입되었는지에 관계없이 Cloud Firestore의 쿼리 수는 동일하게 유지됩니다. 이 페이지에서는 5,000,000회의 문서 읽기 대신 하루에 12 x 24 x 50 = 14,400회의 문서 읽기를 사용합니다. Firebase 호스팅 및 Cloud Functions에 대한 소액의 추가 비용은 Cloud Firestore 비용 절감으로 쉽게 상쇄됩니다.
개발자는 비용 절감 혜택을 얻을 수 있지만 가장 큰 수혜자는 사용자입니다. 이 50개 문서를 Cloud Firestore에서 직접 로드하지 않고 Firebase 호스팅 CDN에서 로드하면 페이지의 콘텐츠 로드 시간을 100~200ms 이상 낮출 수 있습니다. 연구에 의하면 페이지 속도가 빨라지면 사용자의 만족도도 높아지는 것으로 여러 차례 확인되었습니다.
[[["이해하기 쉬움","easyToUnderstand","thumb-up"],["문제가 해결됨","solvedMyProblem","thumb-up"],["기타","otherUp","thumb-up"]],[["필요한 정보가 없음","missingTheInformationINeed","thumb-down"],["너무 복잡함/단계 수가 너무 많음","tooComplicatedTooManySteps","thumb-down"],["오래됨","outOfDate","thumb-down"],["번역 문제","translationIssue","thumb-down"],["샘플/코드 문제","samplesCodeIssue","thumb-down"],["기타","otherDown","thumb-down"]],["최종 업데이트: 2025-09-04(UTC)"],[],[],null,["\u003cbr /\u003e\n\nMany applications serve the same content to all users on first page load. For\nexample a news site may show the latest stories, or an e-commerce site may show\nthe best-selling items.\n\nIf this content is served from Cloud Firestore, each user will issue a new\nquery for the same results when they load the application. Because these\nresults are not cached between users, the application is slower and more\nexpensive than it needs to be.\n\nSolution: Bundles\n\nCloud Firestore bundles allow you to assemble data bundles from common query\nresults on the backend using the Firebase Admin SDK, and serve these\npre-computed blobs cached on a CDN. This gives your users a much\nfaster first load experience and reduces your Cloud Firestore query costs.\n\nIn this guide we will use Cloud Functions to generate bundles and\nFirebase Hosting to dynamically cache and serve bundle content. More\ninformation about bundles is available in the [guides](/docs/firestore/bundles).\n\nFirst create a simple public HTTP function to query the 50 latest \"stories\" and\nserve the result as a bundle. \n\nNode.js \n\n```javascript\nexports.createBundle = functions.https.onRequest(async (request, response) =\u003e {\n // Query the 50 latest stories\n const latestStories = await db.collection('stories')\n .orderBy('timestamp', 'desc')\n .limit(50)\n .get();\n\n // Build the bundle from the query results\n const bundleBuffer = db.bundle('latest-stories')\n .add('latest-stories-query', latestStories)\n .build();\n\n // Cache the response for up to 5 minutes;\n // see https://firebase.google.com/docs/hosting/manage-cache\n response.set('Cache-Control', 'public, max-age=300, s-maxage=600');\n\n response.end(bundleBuffer);\n});\n \n```\n\nJava \n\n```java\n\npackage com.example;\n\nimport com.google.auth.oauth2.GoogleCredentials;\nimport com.google.cloud.firestore.Firestore;\nimport com.google.cloud.firestore.FirestoreBundle;\nimport com.google.cloud.firestore.Query.Direction;\nimport com.google.cloud.firestore.QuerySnapshot;\nimport com.google.cloud.functions.HttpFunction;\nimport com.google.cloud.functions.HttpRequest;\nimport com.google.cloud.functions.HttpResponse;\nimport com.google.firebase.FirebaseApp;\nimport com.google.firebase.FirebaseOptions;\nimport com.google.firebase.cloud.FirestoreClient;\nimport java.io.BufferedWriter;\nimport java.io.IOException;\n\npublic class ExampleFunction implements HttpFunction {\n\n public static FirebaseApp initializeFirebase() throws IOException {\n if (FirebaseApp.getApps().isEmpty()) {\n FirebaseOptions options = FirebaseOptions.builder()\n .setCredentials(GoogleCredentials.getApplicationDefault())\n .setProjectId(\"YOUR-PROJECT-ID\")\n .build();\n\n FirebaseApp.initializeApp(options);\n }\n\n return FirebaseApp.getInstance();\n }\n\n @Override\n public void service(HttpRequest request, HttpResponse response) throws Exception {\n // Get a Firestore instance\n FirebaseApp app = initializeFirebase();\n Firestore db = FirestoreClient.getFirestore(app);\n\n // Query the 50 latest stories\n QuerySnapshot latestStories = db.collection(\"stories\")\n .orderBy(\"timestamp\", Direction.DESCENDING)\n .limit(50)\n .get()\n .get();\n\n // Build the bundle from the query results\n FirestoreBundle bundle = db.bundleBuilder(\"latest-stores\")\n .add(\"latest-stories-query\", latestStories)\n .build();\n\n // Cache the response for up to 5 minutes\n // see https://firebase.google.com/docs/hosting/manage-cache\n response.appendHeader(\"Cache-Control\", \"public, max-age=300, s-maxage=600\");\n\n // Write the bundle to the HTTP response\n BufferedWriter writer = response.getWriter();\n writer.write(new String(bundle.toByteBuffer().array()));\n }\n}\n \n```\n\nNext configure Firebase Hosting to serve and cache this Cloud Function by\nmodifying `firebase.json`. With this configuration the Firebase Hosting CDN\nwill serve the bundle content according to the cache settings set by the\nCloud Function. When the cache expires it will refresh the content by triggering\nthe function again. \n\n firebase.json\n {\n \"hosting\": {\n // ...\n \"rewrites\": [{\n \"source\": \"/createBundle\",\n \"function\": \"createBundle\"\n }]\n },\n // ...\n }\n\nFinally in your web application, fetch the bundled content from the CDN and load\nit into the Firestore SDK. \n\n // If you are using module bundlers.\n import firebase from \"firebase/app\";\n import \"firebase/firestore\";\n import \"firebase/firestore/bundle\" // This line enables bundle loading as a side effect.\n\n async function fetchFromBundle() {\n // Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'\n // response header will be set to 'HIT'\n const resp = await fetch('/createBundle');\n\n // Load the bundle contents into the Firestore SDK\n await db.loadBundle(resp.body);\n\n // Query the results from the cache\n // Note: omitting \"source: cache\" will query the Firestore backend.\n \n const query = await db.namedQuery('latest-stories-query');\n const storiesSnap = await query.get({ source: 'cache' });\n\n // Use the results\n // ...\n }\n\nEstimated Savings\n\nConsider a news website which gets 100,000 users per day and each user loads the\nsame 50 top stories on initial load. Without any caching, this would result in\n50 x 100,000 = 5,000,000 document reads per day from Cloud Firestore.\n\nNow assume the site adopts the technique above and caches those 50 results for\nup to 5 minutes. So instead of loading the query results for every user, the\nresults are loaded exactly 12 times per hour. No matter how many users arrive\nat the site, the number of queries to Cloud Firestore stays the same. Instead of\n5,000,000 document reads, this page would use 12 x 24 x 50 = 14,400 document\nreads per day. The small additional costs for Firebase Hosting and\nCloud Functions are easily offset by the Cloud Firestore cost savings.\n\nWhile the developer benefits from the cost savings, the biggest beneficiary is\nthe user. Loading these 50 documents from the Firebase Hosting CDN rather than\nfrom Cloud Firestore directly can easily shave 100-200ms or more from the\ncontent load time of the page. Studies have repeatedly shown that speedy pages\nmean happier users."]]