CDN에서 번들된 Firestore 콘텐츠 제공

대부분의 애플리케이션에서는 페이지가 처음 로드될 때 모든 사용자에게 동일한 콘텐츠를 제공합니다. 예를 들어 뉴스 사이트에 최신 뉴스를 보여주거나 전자상거래 사이트에서 가장 잘 팔리는 상품을 보여주는 경우입니다.

이 콘텐츠가 Cloud Firestore에서 제공된다면 각 사용자는 애플리케이션을 로드할 때 동일한 결과를 위해 새 쿼리를 실행합니다. 이러한 결과는 사용자 간에 캐시되지 않으므로 애플리케이션은 필요한 것보다 느리고 비용이 많이 발생합니다.

솔루션: 번들

Cloud Firestore 번들을 사용하면 Firebase Admin SDK를 사용하여 백엔드의 일반적인 쿼리 결과에서 데이터 번들을 조합할 수 있으며 CDN에 캐시된 사전 계산된 blob을 제공할 수 있습니다. 이를 통해 사용자에게 훨씬 빠른 로드 환경을 제공하고 Cloud Firestore 쿼리 비용을 줄일 수 있습니다.

이 가이드에서는 Cloud Functions를 사용하여 번들을 생성하고 Firebase 호스팅을 사용하여 번들 콘텐츠를 동적으로 캐싱하고 제공합니다. 번들에 대한 자세한 내용은 가이드를 참조하세요.

먼저 간단한 공개 HTTP 함수를 만들어 최근 50개의 '뉴스'를 쿼리하고 결과를 번들로 제공합니다.

Node.js
exports.createBundle = functions.https.onRequest(async (request, response) => {
  // Query the 50 latest stories
  const latestStories = await db.collection('stories')
    .orderBy('timestamp', 'desc')
    .limit(50)
    .get();

  // Build the bundle from the query results
  const bundleBuffer = 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-cache
  response.set('Cache-Control', 'public, max-age=300, s-maxage=600');

  response.end(bundleBuffer);
});
      
자바

package com.example;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreBundle;
import com.google.cloud.firestore.Query.Direction;
import com.google.cloud.firestore.QuerySnapshot;
import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.cloud.FirestoreClient;
import java.io.BufferedWriter;
import java.io.IOException;

public class ExampleFunction implements HttpFunction {

  public static FirebaseApp initializeFirebase() throws IOException {
    if (FirebaseApp.getApps().isEmpty()) {
      FirebaseOptions options = FirebaseOptions.builder()
          .setCredentials(GoogleCredentials.getApplicationDefault())
          .setProjectId("YOUR-PROJECT-ID")
          .build();

      FirebaseApp.initializeApp(options);
    }

    return FirebaseApp.getInstance();
  }

  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    // Get a Firestore instance
    FirebaseApp app = initializeFirebase();
    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-stores")
        .add("latest-stories-query", latestStories)
        .build();

    // Cache the response for up to 5 minutes
    // see https://firebase.google.com/docs/hosting/manage-cache
    response.appendHeader("Cache-Control", "public, max-age=300, s-maxage=600");

    // Write the bundle to the HTTP response
    BufferedWriter writer = response.getWriter();
    writer.write(new String(bundle.toByteBuffer().array()));
  }
}
      

그런 다음 firebase.json을 수정하여 이 Cloud 함수를 제공하고 캐시하도록 Firebase 호스팅을 구성합니다. 이 구성을 사용하면 Firebase 호스팅 CDN에서 Cloud 함수로 설정한 캐시 설정에 따라 번들 콘텐츠를 제공합니다. 캐시가 만료되면 함수를 다시 트리거하여 콘텐츠를 새로고칩니다.

firebase.json
{
  "hosting": {
    // ...
    "rewrites": [{
      "source": "/createBundle",
      "function": "createBundle"
    }]
  },
  // ...
}

마지막으로 웹 애플리케이션에서 CDN에서 번들 콘텐츠를 가져와 Firestore SDK에 로드합니다.

// 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
  // ...
}

예상 절감액

하루에 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 이상 낮출 수 있습니다. 연구에 의하면 페이지 속도가 빨라지면 사용자의 만족도도 높아지는 것으로 여러 차례 확인되었습니다.