Redis
 Computer >> コンピューター >  >> プログラミング >> Redis

ClerkとUpstash Redisで構築する、安全かつスケーラブルなセッションストアの作り方

Redisの主要なユースケースのひとつが、Webアプリケーションにおいてリクエスト間の状態を維持するためのユーザーセッションの保存・管理です。実装方法は複数あり、近年登場したサーバーレスツールの中には、簡単にデプロイできる選択肢を提供しているものもあります。

ユーザーセッションデータの管理は、さまざまなビジネスアプリケーションにとって非常に重要です。たとえば、パーソナライゼーションプラットフォームでは、Redisを使ってユーザーの操作履歴や設定を保存し、カスタマイズされたコンテンツや商品のおすすめを提供しています。ゲーム業界では、プレイヤー同士のやり取りをリアルタイムに追跡することで、快適なマルチプレイヤー体験を実現するためにRedisが活用されています。広告プラットフォームもセッションデータをRedisに格納し、広告配信の最適化や将来のキャンペーンのパーソナライズに役立てています。本記事では、ECアプリケーションを例に挙げ、Redisを活用してショッピングカートを効果的に管理する方法を見ていきます。

プロジェクト概要

この記事では、Clerk、Next.js、Upstash Redisを組み合わせて、ショッピングアプリケーション向けのセッションストアを構築します。このプロジェクトで実装する主な機能は以下の通りです。

  • ユーザーによるサインアップ、サインイン、サインアウトが可能。
  • 各ユーザーは自分専用のショッピングカートに商品を追加・削除できる。
  • ユーザーはカート内の商品数量を更新できる。

さらに、このプロジェクトにはQStashとUpstash Ratelimitに関連する機能も含まれています。

  • アプリ内の特定のアクションがイベントを発火し、QStash経由でメール送信がスケジュールされます。メールの実際の配信はResendが担当します。たとえば、チェックアウト後24時間後に配送確認メールを送る、商品購入後に一定時間経過してからレビューを促すメッセージを送る、といったことが可能です。
  • ユーザーは商品を評価(レーティング)することもできます。評価データはすべてUpstash Redis上の適切なデータ構造に丁寧に保存されます。また、ユーザー操作のバランスを保ち、不正利用を防ぐため、評価イベントにはUpstash Ratelimitによるレート制限が適用されます。

デモ

デプロイされたデモはこちらからご覧いただけます。

このプロジェクトのGitHubリポジトリにもアクセスできます。

Next.jsアプリケーションの作成

新しいターミナルウィンドウを開き、以下のコマンドでアプリケーションを作成します。

npx create-next-app@latest

プロジェクトのオプションについていくつか質問されるので答えると、Next.jsのプロジェクトテンプレートが完成します。

npx create-next-app@latest
Need to install the following packages:
 create-next-app@13.4.18
Ok to proceed? (y) y
✔ What is your project named? shopstash
✔ Would you like to use TypeScript? No / -> Yes
✔ Would you like to use ESLint? No / -> Yes
✔ Would you like to use Tailwind CSS? No / -> Yes
✔ Would you like to use `src/` directory? -> No / Yes
✔ Would you like to use App Router? (recommended) No / -> Yes
✔ Would you like to customize the default import alias? -> No / Yes
Creating a new Next.js app in /Users/***/shopstash.

Clerkの統合

Clerkをプロジェクトに追加するのはとても簡単です。ここでは、事前構築されたコンポーネントやフックを利用できるClerkのNext.js SDKを使用します。まずはインストールしましょう。

npm install clerk@nextjs

次に、Clerkダッシュボード上でアプリケーションを作成します。サインアップ時に必要な情報などの設定は、お好みに合わせて行えます。アプリを作成すると、必要な認証情報が表示されるので、それらを.env.localファイルにコピーします。以下はその例です。

.env.local

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_********
CLERK_SECRET_KEY=sk_test_********

あわせて、Clerk用のパス設定も.env.localファイルに記述します。

.env.local

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/

続いて、アクティブなセッションやユーザーコンテキストを利用できるよう、ルートレイアウトを<ClerkProvider>で囲みます。このチュートリアルの後半では、Headerコンポーネントも実装していきます。

layout.tsx

import { ClerkProvider } from "@clerk/nextjs";
 
import Header from "./components/Header";
 
export default function RootLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 return (
 <ClerkProvider>
 <html lang="en">
 <body className="bg-white">
 <Header />
 <main className="container bg-white">
 <div className="flex min-h-screen items-start justify-center ">
 <div className="mt-5">{children}</div>
 </div>
 </main>
 </body>
 </html>
 </ClerkProvider>
 );
}

これでClerkの導入は完了です。次のステップは、どのページを認証で保護するかを決めることです。この処理は、ルートフォルダに配置したmiddleware.tsxファイルで行います。

middleware.tsx

import { authMiddleware } from "@clerk/nextjs";
 
export default authMiddleware({});
 
export const config = {
 matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

これにより、アプリケーション全体が保護されます。サインインせずに任意のページへアクセスしようとすると、認証のためにインデックスページへリダイレクトされます。

この時点で、アプリにはサインアップページとサインインページが必要です。これらのページへのナビゲーションはHeaderから提供し、このコンポーネントはログイン状態に応じて表示が切り替わります。アクティブなユーザーがいれば、サインアウトやプロフィール表示ができ、そうでなければサインイン・サインアップへのリンクが表示されます。

ログインしていない状態のヘッダーは次のようになります。

ClerkとUpstash Redisで構築する、安全かつスケーラブルなセッションストアの作り方

最後のステップとして、ユーザーアクションに必要なルートを作成します。本プロジェクトではClerk組み込みのサインアップ/サインインコンポーネントを使用しますが、独自のデザインでカスタムページを作成することも可能です。まず、サインアップ用にapp/sign-in/[[...sign-up]]/page.tsxというルートを作成します。

app/sign-in/[[...sign-up]]/page.tsx

import { SignUp } from "@clerk/nextjs";
 
const SignUpPage = () => {
 return (
 <>
 <SignUp />
 </>
 );
};
export default SignUpPage;

サインインページもほぼ同じ構成なので、同様のパスで実装します。

app/sign-in/[[...sign-up]]/page.tsx

import { SignIn } from "@clerk/nextjs";
 
const SignInPage = () => {
 return (
 <>
 <SignIn />
 </>
 );
};
export default SignInPage;

Clerkの統合が完了したので、次に進む準備が整いました。ログインユーザーの仕組みが整ったことで、ユーザーごとに固有のセッションストアをRedis上に構築できます。戦略としては、ClerkからユーザーIDを取得し、そのユーザーのセッションデータをUpstash Redisに保存します。具体的な流れをつかむために、ショッピングカートの構築を考えてみましょう。ここでは、個々のセッションがそれぞれ対応するカートアイテムのデータを保持します。

まず最初に、「カートアイテムとは何か」を定義する必要があります。これは、その後のアプリケーション全体を設計する際の青写真となります。さまざまな商品データでアプリを埋めたい場合は、ChatGPTのようなツールが非常に役立ちます。あるいは、より直接的な方法として、このサンプルに関連するGitHubリポジトリからデータを取得してもよいでしょう。もちろん、フロントエンドを魅力的に見せるには、各商品に適した画像を用意する必要もあります。

public/items.tsx

export const items = [
 {
 id: 1,
 title: "Elegant Leather Watch",
 image: "/images/1.png",
 description: "A sophisticated leather watch for all occasions.",
 company: "Timepiece Creations",
 price: 99.99,
 },
];

ショッピングカートの実装

一般的なショッピングアプリケーションでは、ユーザーのカートに複数の場面からアクセスできる必要があります。それによって、カートの内容に応じて関連コンポーネントを表示できます。たとえば、商品詳細ページでも商品一覧ページでも、その商品がすでにカートに入っているかどうかを確認できるべきです。完成イメージは以下のようになります。

ClerkとUpstash Redisで構築する、安全かつスケーラブルなセッションストアの作り方

これを実現するために、React Context APIを使用します。これにより、カートに対する必要な操作(商品の追加、削除、カートのリセットなど)を準グローバルなスコープで提供できます。

Upstash Redisへの接続を設定するには、コンソールからUPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKENの値をコピーし、.envファイルに貼り付けます。

.env

UPSTASH_REDIS_REST_URL=<YOUR_URL>
UPSTASH_REDIS_REST_TOKEN=<YOUR_TOKEN>

Cart Contextはapp/context/CartContext.tsxファイルに配置します。このコンテキストをアプリケーション全体を囲むように設定すれば、提供されるメソッドをどこからでも使えるようになります。主な機能は以下の通りです。

  • ユーザーはカートに商品を追加し、数量を調整できる。
  • カートから商品を削除できる。
  • カート全体をリセットできる。
  • チェックアウト機能もある。
    以下にContext APIの全体像を示します。各メソッドについては順番に分解しながら実装していきます。

アルゴリズムの全体像は次のようになっています。

  • セッションストアとして扱われるカートは、Upstash Redis上のハッシュに保持されます。ハッシュの一意な識別子はユーザーIDに基づき、各ユーザーはcart:<USER_ID>という形式で命名されたカートを持ちます。
  • カートデータをRedisハッシュに保存する際は、キーにアイテムID、値に各アイテムの数量を使用します。Redisの組み込みコマンドのおかげで、カートの変更は非常に簡単です。
  • クライアント側では、カートはItemオブジェクトの配列からなるステートとして管理されます。ページ読み込み時にuseEffectフックがUpstash Redisからカートデータを取得し、カートに変更があれば関連するすべてのコンポーネントが再レンダリングされます。
  • Redisのシンプルなデータ構造により、addItemremoveItemの実装も容易です。redis.hincrby()コマンドを使えば、カートへの追加・削除から数量の調整まで、幅広いタスクをこなせます。まさにRedisの実力が光るところです。
  • resetCart関数については、redis.del()を使ってUpstashデータベースからハッシュのキーを削除するだけです。

カートの概念がつかめたところで、いよいよ中核となるメソッドの実装に取り掛かりましょう。

カートへの商品追加

Redisハッシュの観点から見ると、商品の追加と数量の変更は同じコマンドで処理できます。hincrbyコマンドは、キーが存在しなければ新規作成して値を1にセットし、存在すればincrementパラメータに応じて値を増加させます。

クライアント側では、新しい商品をカートステートに追加するか、既存商品の数量を変更することで、これらの操作を反映します。

contexts/CartContext.tsx

const addItem = async (id: number) => {
 const item = items.find((i) => i.id === id);
 if (!item) return;
 
 const doesItemExist = cart.some((i) => {
 return id === i.id;
 });
 
 let newCart: Item[];
 
 if (!doesItemExist) {
 newCart = [...(cart || []), item];
 redis.hincrby(`user:${userId}`, id.toString(), 1);
 
 //We create an item in the state object with the given id, and set the quantity to 1.
 const newCartItemIDs = { ...cartItems, [id]: 1 };
 
 setCartItemIDs(newCartItemIDs);
 setCart(newCart);
 } else {
 const item = items.find((i) => i.id === id);
 
 //This item currently exists in the state object as key, so we increase the value by 1.
 const updatedItemQuantities = {
 ...cartItems,
 [id]: cartItems[id] + 1,
 };
 
 setCartItems(updatedItemQuantities);
 redis.hincrby(`user:${userId}`, id.toString(), 1);
 }
};

カートからの商品削除

削除も追加とよく似ています。hincrbyincrementパラメータに-1を指定すれば、ハッシュの値を減少させることができます。

contexts/CartContext.tsx

const removeItem = async (id: number, force: boolean = false) => {
 const doesItemExist = cart.some((i) => {
 return id === i.id;
 });
 
 if (!doesItemExist) return;
 
 if (cartItems[id] === 1 || force) {
 const newCart: Item[] = cart.filter((item: { id: number }) => {
 return item.id !== id;
 });
 
 // Creating the new state object for cart
 const newCartItems = { ...cartItems };
 delete newCartItems[id];
 
 //Removing the item from Upstash Redis hashset.
 redis.hdel(`user:${userId}`, id.toString());
 
 setCart(newCart);
 setCartItems(newCartItems);
 } else if (cartItems[id] > 1) {
 const updatedItemQuantities = {
 ...cartItems,
 [id]: cartItems[id] - 1,
 };
 
 setCartItems(updatedItemQuantities);
 redis.hincrby(`user:${userId}`, id.toString(), -1);
 }
};

これでカート機能に必要なすべてのメソッドが揃いました。グローバルにアクセス可能な形でプロジェクト全体に統合されています。この機能の代表的なユースケースを2つ紹介します。

  • インデックスページ: ここではすべての商品が表示されます。各商品には専用ボタンが付いており、カートへの追加が可能です。すでにカートに入っている商品であれば、削除することもできます。

  • React UIライブラリ「shadcn/ui」を使用して、カート内の全商品を1つの画面にまとめて表示するモーダル/シートを構築します。ここでは閲覧だけでなく、商品数量の変更も可能です。さらに、カートのリセットやチェックアウトへの進行もこの画面から行えます。

インデックスページ

注意点として、インデックスページの商品はサインイン済みのアクティブなユーザーにのみ表示されます。まずClerkからユーザーデータを取得し、そのレスポンスに基づいてコンポーネントを描画します。

カードコンポーネントでは、CartContextから必要な関数とオブジェクトを取得するだけです。

components/CardComponent.tsx

export default function CardComponent(props: { item: cardProps }) {
 const { item } = props;
 const { id, title, image, company } = item;
 const { addItem, removeItem, cartItems } = useContext(CartContext);
 
 return (
 <>
 <Card className="transition duration-200 hover:shadow-lg">
 <Link href={`/products/${id}`}>
 <CardHeader>
 <CardTitle>{title}</CardTitle>
 </CardHeader>
 <CardContent>
 <Image src={image} alt={title} width={300} height={300}></Image>
 <CardDescription>{company}</CardDescription>
 </CardContent>
 </Link>
 <CardFooter>
 <div className="grid grid-rows-2">
 <CartButton
 id={id}
 cartItems={cartItems}
 addItem={addItem}
 removeItem={removeItem}
 />
 </div>
 </CardFooter>
 </Card>
 </>
 );
}

重要なのはカートボタンです。ここからカートへの商品追加・削除を行います。このボタンはカートの現在の状態に基づいて描画されます。

components/CartButton.tsx

const CartButton = ({
 id,
 cartItems,
 addItem,
 removeItem,
}: {
 id: number;
 cartItems: cartContent;
 addItem: (id: number) => Promise<void>;
 removeItem: (id: number, force: boolean) => Promise<void>;
}) => {
 const itemExists: boolean = cartItems?.hasOwnProperty(id);
 const { triggerEvent } = useContext(UserStateContext);
 return (
 <button
 className={`{
 itemExists ? "bg-red-400 text-black" : "bg-cyan-500 text-black"
 } flex items-center justify-center gap-3 rounded-full px-4 py-2 transition-all duration-300`}
 onClick={() => {
 if (itemExists) {
 removeItem(id, true);
 } else {
 addItem(id);
 }
 }}
 >
 <p className="text-sm font-bold">
 {itemExists ? "Remove from Cart" : "Add to Cart"}
 </p>
 <FaCartShopping size="25" />
 </button>
 );
};

カートの状態が動的になったことで、現在の状況に応じて特定のコンポーネントを表示できるようになりました。次に、カート専用のコンポーネントを導入します。ここはカートの中身を一望したり、数量を調整したり、リセットボタンを押したりする中心的な場所になります。

ベースにはshadcn/uiのSheetコンポーネントを採用し、その内部をカスタマイズしていきます。

カートが空の場合、コンポーネントはその状態を明確に表示します。

ClerkとUpstash Redisで構築する、安全かつスケーラブルなセッションストアの作り方

一方、商品がカートに追加されていくと、それらがこのコンポーネント内に表示されるようになります。選んだ商品を確認できるだけでなく、数量の変更や合計金額の確認も自由に行えます。

ClerkとUpstash Redisで構築する、安全かつスケーラブルなセッションストアの作り方

このコンポーネントの作成手順の全体像は以下をご覧ください。ボタンの細かい設定など詳細を知りたい方は、コードベース全体を公開しているGitHubリポジトリを参照することをおすすめします。

以上のコードで、私たちのプロジェクトは完成です。概念から実装までの道のりを歩み、Upstash Redisのパワーとshadcn/uiライブラリの柔軟性を活かして、カート機能を見事に実現することができました。

まとめ

ユーザー管理を担うClerkと、効率的なデータストレージを提供するUpstash Redisの組み合わせは、動的なカートシステムの構築において大きな力を発揮しました。両者はアプリケーションの背骨となり、セキュリティとパフォーマンスの両方を支えています。このプロジェクトは、Upstash RedisやClerkといった強力なツールを組み合わせることで、非常に複雑な問題をスムーズに解決できる好例といえるでしょう。

さらなる改善のためのアイデアをいくつか紹介します。

  • ユーザー体験: 堅牢で実用的なカートはすでに完成していますが、UIの強化——アニメーション、フィードバックの演出、詳細な商品プレビューなど——に踏み込めば、さらにシームレスなユーザージャーニーを提供できます。

  • パフォーマンス: Upstash Redisを基盤としているので、高度なキャッシング戦略を検討したり、Service Workerを組み込んで読み込み速度の向上やオフライン体験の充実を図ることも可能です。

  • 機能: ウィッシュリストの追加、カート内容に基づくパーソナライズされた商品レコメンド、プロモーションコード適用システムなどを実装すれば、ショッピング体験を一段引き上げられます。

  • 連携: 決済ゲートウェイを統合してスムーズなチェックアウトを実現したり、サードパーティの在庫管理やCRMシステムと連携して、包括的なECソリューションへ発展させる可能性もあります。

最後までこの開発の旅にお付き合いいただき、ありがとうございました。皆さんからのフィードバックをお待ちしています。この基盤をもとにどんな革新的なものが生まれるのか、楽しみにしています。このプロジェクトに関する質問や問題がある場合は、fahreddin@upstash.comまでお気軽にご連絡ください。

プロジェクトのGitHubリポジトリはこちらからアクセスできます。

QStashとResendを使ったメールのスケジューリングについては、別の記事で取り上げる予定です。それまでの間は、サンプルリポジトリで実装内容をご確認いただけます。

  1. RedisConf 2021総まとめ:部門別に選ばれた注目セッション10選

    毎年開催されるRedisコミュニティの祭典「RedisConf」。今年のテーマは「リアルタイムデータの力の再発見」でした。122か国から12,000人を超える開発者、アーキテクト、ビジネス・テクノロジーのリーダーたちが参加登録し、基調講演、座談会、60以上のブレイクアウトセッションがライブ配信されました。なお、2021年5月20日以降、RedisConf 2021の全セッションはYouTubeチャンネルで視聴可能です。 ユニークなユースケースを扱う多数のセッションの中から、投票によって10部門のトップセッションが選出されました。ここでは、各部門の受賞セッションをご紹介します。 最もエンター

  2. Redis と NextAuth で Next.js ブログに安全なコメント機能を実装する方法

    このチュートリアルでは、ブログにコメントセクションを実装する方法を解説します。使用する技術スタックは以下の通りです。 Next.js 13(App ディレクトリ構成) NextAuth(認証) Upstash Redis(コメントの保存) SWR(コメントのキャッシュと再検証) それでは始めましょう。 NextAuth で認証を処理する まず、誰でも自由にコメントを投稿できる状態は避けなければなりません。スクリプトを使ってスパムコメントを大量投稿されてしまう恐れがあるからです。コメント投稿を許可する前に、まず認証システムを構築しましょう。ここでは NextAuth を使用します。 プロジェ