サーバレスでフルマネージドなキャッシュサービスMomentoをRemixとNext.jsでためしてみた

世界初のサーバレスキャッシュサービス、世界最速のキャッシュサービスと謳っているMomentoをRemixとNext.jsでためしてみました。
2022.07.16

はじめに

こんにちは、CX事業本部Delivery部MADグループの森茂です。

Momentoは世界初のサーバレスキャッシュサービス、世界最速のキャッシュサービスと謳っているフルマネージドなキャッシュサービスです。アカウント登録から運用まで数分で使い始めることができます。現在は無料ですぐに使い始めることができます。SDKについても、下記のように主要な環境はほぼ網羅されています。

  • .NET
  • Go
  • Java
  • JavaScript
  • Python
  • Rust
  • CLI

今回はMomentoのキャッシュをRemixでつかってみます。また、ほぼ同じロジックで動作できるのでついでにNext.jsのSSRでも動作をためしてみました。

検証環境

  • M1 MacBook Pro
  • macOS Monterey 12.4
  • Node.js 16.14.0
  • Remix 1.6.5
  • Next.js 12.2

Momentoのセットアップ

ドキュメントを順にたどるだけにはなりますが、Homebrewを利用することでCLIを使い簡単にはじめることができます。

Momentoのインストール

まずはHomebrewを使ってCLIをインストールします。

$ brew tap momentohq/tap
$ brew install momento-cli

Homebrewを利用しない場合や、Windows環境へのCLIインストールは下記ドキュメントを参照してください。

Momentアカウントのサインアップ

現在はCLIからのみサインアップが可能となっています。サインアップ時にE-Mailアドレスが必要です。また設置するクラウドサービス(AWS、GCP)とリージョンを選択する必要があります。アプリケーションのデプロイ先にあわせて決めるのがよいでしょう。

$ momento account signup aws|gcp --email <TYPE_YOUR_EMAIL_HERE> --region <TYPE_DESIRED_REGION>

今回は、AWS、ap-northeast-1を使う想定で進めていきます。

$ momento account signup aws --email <TYPE_YOUR_EMAIL_HERE> --region ap-northeast-1

サインアップが成功すると登録したE-Mailアドレスにトークンが届きます。次のステップからそのトークンを使って設定をしていきます。

Momentoの設定

E-Mailで届いたトークンを使ってMomentoの初期設定を行います。

$ momento configure
Token: // < Enter token from email here.
Default Cache [default-cache]: my-first-cache // Name of cache to use on CLI by default.
Default Ttl Seconds [600]: 30 // Sets the default TTL for cache entries. For demostration purposes we are setting this lower right now.
[2022-07-16T15:31:25Z INFO  momento::commands::cache::cache_cli] creating cache...
[2022-07-31T15:31:33Z INFO  momento::commands::configure::configure_cli] default cache successfully created

トークンの入力と、デフォルトのキャッシュ名(ネームスペース)、デフォルトのTTLを指定して準備は完了です。

Remixのセットアップ

Remixアプリケーションを用意します。

Remixのインストール

$ npx create-remix@latest

? Where would you like to create your app? remix-momento-example
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment
targets. Remix App Server
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? No
💿 That's it! `cd` into "remix-momento-example" and check the README for development and deploy instructions!

今回は一番シンプルなRemix App Serverを選択しました。

Momentoクライアント用意

まずはJavaScript SDKをインストールします。

$ yarn add @gomomento/sdk

環境変数にアクセストークンを記載しておきます。

.env

MOMENTO_AUTH_TOKEN=アクセストークン

Remixでクライアントを用意します。

/app/utils/momentoClient.server.ts

import { SimpleCacheClient } from '@gomomento/sdk';

// ユーザーのMomentoオーストークン(環境変数から取得)
const authToken = process.env.MOMENTO_AUTH_TOKEN;

if (!authToken) {
  throw new Error('Missing required environment variable MOMENTO_AUTH_TOKEN');
}

// Momentoのクライアントを作成
const DEFAULT_TTL = 60; // デフォルトTTLは60秒
export const momento = new SimpleCacheClient(authToken, DEFAULT_TTL);

Momentoを使ったキャッシュをためしてみる

準備が整ったのでアプリケーション側でキャッシュの動作を確認します。以前Cloudflare Workers KVを利用したキャッシュの動作を見ましたが、今回も同じ方法で検証してみました。

JSONplaceholderを利用してMomentoを利用したキャッシュの動作を試してみます。

データの流れとしては、下記の通り。

  • ページへのリクエスト
  • Momentoにキャッシュがあるかどうかを確認 -> キャッシュがある場合はキャッシュを返す
  • ページリクエスト時にAPIへデータを取得
  • 取得したデータをMomentoへキャッシュとして格納(60秒の保存期限)
  • 取得したデータを利用してページをレンダリング
  • ページを返す

/app/routes/index.tsx

import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { momento } from '~/utils/momentoClient';

type Todo = {
  id: number;
  title: string;
};

type LoaderData = {
  todos: Todo[];
};

export const loader = async () => {
  // my-first-cacheというネームスペースのキャッシュを利用
  // CLIで作成した名前
  const CACHE_NAME = 'my-first-cache';

  // momentoにtodosというキーのキャッシュデータがあるか確認
  const cacheResponse = await momento.get(CACHE_NAME, 'todos');
  const cacheData = cacheResponse.text();

  // キャッシュデータがあればそれを返す
  if (cacheData) {
    return json<LoaderData>({ todos: JSON.parse(cacheData) });
  }

  // キャッシュデータがない場合は取得
  const response = await fetch('https://jsonplaceholder.typicode.com/todos');
  const todos = await response.json();

  // todosというキーでキャッシュへ保存する
  /**
   * キャッシュに格納
   * @param cacheName キャッシュのネームスペース
   * @param key キャッシュのキー
   * @param value キャッシュに格納するデータ
   * @param ttl キャッシュの有効期限
   */
  await momento.set(CACHE_NAME, 'todos', JSON.stringify(todos), 60);

  // 取得したデータを返す
  return json<LoaderData>({ todos });
};

export default function Index() {
  const { todos } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Welcome to Remix</h1>
      <ul>
        {todos && todos.map((todo) => <li key={todo.id}>{todo.title}</li>)}
      </ul>
    </div>
  );
}

動作確認

実装ができたところでサーバーへデプロイして検証してみます。今回はAWS App Runnerでデプロイしていますが、ローカルの開発サーバーでもキャッシュを利用した速度の違いは体感できるはずです。

1回目の読み込み

TTFB 128.76ms

2回目の読み込み

TTFB 39.11ms

APIからは200件のシンプルなデータを取得するだけですが、Momentoへキャッシュとしてデータが登録される前と、MomentoへキャッシュされMomentoのデータから表示した際には1/3程度(初回128.76s、2回目39.11ms)の時間でTTFBに到達しています。

今回はデータが少量のためあまり大きな違いは見えませんが、キャッシュされている間(今回は60秒)にアクセスした場合は基本的には世界のどこからアクセスしてもMomentoのキャッシュから高速に配信され受け取ることができるわけです。(もちろん通信環境によって若干は上下します)

実際に運用環境で利用する場合はバックエンド側でも処理が必要なデータで受け取りまでに時間がかかるものも多いはずです。そのため今回の結果以上に大きな効果を体験できると思います。

なお、MomentoのキャッシュサイズはKey-Valueあわせて4MBとなっています。APIのレスポンスなどをキャッシュするには十分ですがファイルのキャッシュにはサイズに気をつける必要があります。

Next.jsでもためしてみた

Next.jsのSSRでもほぼ同様のロジックになるので動作をためしてみました。クライアントの書き方はまったく同じものを利用しています。

/pages/index.tsx

import type { NextPage, InferGetServerSidePropsType } from 'next';
import { momento } from '../utils/momentoClient';

type Todo = {
  id: number;
  title: string;
};

type Props = InferGetServerSidePropsType<typeof getServerSideProps>;

const Home: NextPage<Props> = ({ todos }) => {
  return (
    <div>
      <h1>Welcome to Next.js!</h1>
      <ul>
        {todos && todos.map((todo) => <li key={todo.id}>{todo.title}</li>)}
      </ul>
    </div>
  );
};

export default Home;

export const getServerSideProps = async () => {
  // myTodoというネームスペースのキャッシュを利用
  const CACHE_NAME = 'myTodo';

  // momentoにキャッシュデータがあるか確認
  const cacheResponse = await momento.get(CACHE_NAME, 'todos');
  const cacheData = cacheResponse.text();

  // キャッシュデータがあればそれを返す
  if (cacheData) {
    return {
      props: {
        todos: JSON.parse(cacheData) as Todo[],
      },
    };
  }

  // キャッシュデータがない場合は取得
  const response = await fetch('https://jsonplaceholder.typicode.com/todos');
  const todos = (await response.json()) as Todo[];

  /**
   * キャッシュに格納
   * @param cacheName キャッシュのネームスペース
   * @param key キャッシュのキー
   * @param value キャッシュに格納するデータ
   * @param ttl キャッシュの有効期限
   */
  await momento.set(CACHE_NAME, 'todos', JSON.stringify(todos), 60);

  // 取得したデータを返す
  return {
    props: { todos },
  };
};

さいごに

今回はフルマネージドなキャッシュサービスMomentoを利用したキャッシュのRemixとNext.jsでの利用例を紹介させていただきました。まだ新しいサービスのため料金体系などどうなっていくかはわかりませんがこの手軽さは一度使うとマネージドなキャッシュサービスへは戻れなそうです。選択肢のひとつとして今後の展開がとても楽しみです。

参考情報