カクテルの作成がより簡単になる Web アプリを作ってみた !

2021-11-02
日常で楽しむクラウドテクノロジー

Author : 武松 未来 (監修 : 金杉 有見子)

こんにちは、アマゾン ウェブ サービス ジャパンでインターンシップをしている武松です。

突然ですが、みなさん、カクテルを自分で作ることは好きですか ? 昨今はお家時間も増え、自分でカクテルを作る人が増えていると思います。私も最近、ウイスキーを買って炭酸水やコーラなどの色々なミキサーで割って飲んでいるのですが、リキュールとミキサーの分量がわからず、カクテルをたくさん入れすぎて一杯で酔って寝てしまうことがよくあります (本当は色々なカクテルを楽しみたいのですが・・・)。また、アルコール度数が弱いなと思ってリキュールを足した際に、リキュールを入れ過ぎてアルコール度数が強くなりすぎてしまうこともあると思います。すると、今度はミキサーを足すと思うのですがその際に、今度はミキサーを入れすぎてアルコール度数が強くなり過ぎて....というようなループに陥ったことはありませんか ?

このような問題を解決するために、自身が作りたいアルコール度数に応じたリキュールとミキサーそれぞれの分量を教えてくれる新しいサービスを開発しました。その名も「Drink@home」です。

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

*ハンズオン記事およびソースコードにおける免責事項 »


1. 宅飲みの課題

昨今、外で飲酒することが難しくなった代わりに、自宅で飲む、つまり宅飲みをする人の数が増加しています。また、宅飲みをする回数が増加するにあたり、リキュールを購入する人も増加しています (参照リンクの図 5)。これは、自分でリキュールとミキサーからカクテルを作る人が増加しているということです。

自分でリキュールとミキサーからカクテルを作る時、どのリキュールとミキサーを組み合わせていいのかわからない、自分が理想とするアルコール度数のカクテルを作ることが難しいなど、課題があると思います。特に、アルコール度数を正確にお酒を作る場合、リキュールのアルコール濃度から暗算や電卓での計算をしなければなりません。


2. Web アプリ「Drink@home」

上記のような問題を解決するために、名付けて「Drink@home」を開発しました。以下が「Drink@home」の機能一覧です。

  • 作りたいアルコール度数や使用するコップの容量に応じて、リキュールとミキサーの分量を教えてくれる
  • カクテル名、カクテルの味、リキュール名からカクテルを検索することが可能
  • 「Drink@home」には存在しないリキュールとミキサーの組み合わせのカクテルを、自分だけのオリジナルカクテルとして登録することが可能
  • 一回作ったカクテルの情報を履歴情報として保存することで再度同じ条件のカクテルを作成可能

3. アーキテクチャ概要

アーキテクチャは下のようになっています。

img_cocktail-create-app_01

私自身 Web アプリを作成すること、AWS を使うことが初めてだったので、手軽に Web アプリが開発できる AWS Amplify を使用しました。データは画像系を Amazon S3 に、ユーザー情報やカクテル情報を Amazon DynamoDB に格納しています。

またアプリの認証には Amazon Cognito を使っており、そこで登録したユーザー名を AWS Lambda を通して DynamoDB に格納しています。肝心のリキュールとミキサーの分量を計算する際は、AWS AppSync から Lambda を呼び出し、Lambda 内で計算を行なっています。この際の計算結果は履歴情報として DynamoDB に格納しています。また検索機能は、AppSync から DynamoDB にクエリを投げることで行います。

Amplify では、AWS Amplify CLI を使用してバックエンドの AWS 上のリソースをプロビジョニングを行ったり、Web アプリのホスティングを行いました。

ここからは、DynamoDB、AppSync、Lambda に分けて説明していきます。


4. アーキテクチャ詳細:Amazon DynamoDB 設計

画像系以外のデータは DynamoDB に格納しています。

DynamoDB を選んだ理由は、

  • Easy to start  (ネットワーク設計を行う必要がないなど)
  • 運用負荷が低い
  • 可用性や拡張性に優れている

の 3 点になります。

4-1. DynamoDB に対するクエリ一覧

スキーマを設定するにあたって、まず、フロントエンドで想定される DynamoDB に対するクエリを考えました。

ユーザ情報を確認、変更する機能

  • ユーザーごとのお気に入りのカクテル名とコップ名を取り出すクエリ
  • お気に入りのカクテル名とコップ名を登録するクエリ

分量の計算、結果出力機能

  • ユーザーが入力したリキュール名からリキュールの度数を取り出すクエリ
  • ユーザーが選択したコップ名からコップの容量を取り出すクエリ
  • 計算結果を身の回りの容器の杯数として出す際ために、身の回りの容器の画像や容量を取り出すクエリ

カクテルの検索機能

  • ユーザーが入力したカクテル名から対応するカクテルを取り出すクエリ
  • ユーザーが選択した味の分類から対応するカクテル名のリストを取り出すクエリ
  • ユーザーが入力したリキュール名から対応するカクテル名のリストを取り出すクエリ

オリジナルカクテルの登録機能

  • 入力したカクテル名、リキュール名、ミキサー名や選択した味の分類をレシピとして登録するクエリ

カクテル作成履歴を登録、表示する機能

  • 作成したカクテル名や使用したリキュール名、ミキサー名、作成した時間、作成者を登録するクエリ
  • ユーザーごとに作成したことのあるカクテル名を時間順に表示するためのクエリ

4-2. DynamoDBのテーブル一覧

今回、テーブルは合計 6 つ作成しています。初めてのスキーマ設計だったため、テーブルをデータタイプごとに分けるなど RDBMS のようなアプローチになってしまっていますが、実業務で設計される際はぜひ ドキュメントのベストプラクティス をご確認ください。

UserData :
ユーザー情報のためのテーブルです。パーティションキーを username (ユーザー名)、ソートキーを order (番号) としてその他に favorite_cupname (お気に入りのコップ名)、favorite_cocktail (お気に入りのカクテル名) を格納します。favorite_cupnameとfavorite_cocktail はそれぞれ複数登録できる。ユーザー名のみがキーだと何番目にお気に入りのカクテルやコップなのかが判断できないため、ソートキーとして order を設定しました。

type UserData @model @key(fields: ["username", "order"]) {
    username: String!
    order: Int!
    favorite_cocktail: String
    favorite_cupname: String
}

CupData :
コップ情報のためのテーブルです。パーティションキーを cupname (コップ名) としてその他に cupcapacity (コップの容量)、cuppicture (コップの写真) を格納します。

type CupData @model @key(fields: ["cupname"]){
  cupname: String!
  cupcapacity: Int
  cuppicture: String
}

CocktailData :
カクテル情報のためのテーブルです。パーティションキーを cocktailname (カクテル名)、ソートキーを cocktailcreator (カクテルのレシピを登録した人) として、その他に cocktailpicture (カクテルの写真)、cocktailfeature (カクテルの特徴)、cocktailtaste (カクテルの味)、liqueur (カクテルを作る際に使うリキュール)、mixer (カクテルを作る際に使うミキサー) を格納します。

ユーザーが作成したオリジナルのカクテルと元々データとして入っているカクテルを区別するために cocktailcreator をソートキーとして設定しました。またこのテーブルは 4 つのインデックスを持ちます。CocktailTasteIndex、CocktailMixerIndex、CocktailLiqueurIndex の 3 つのインデックスは検索機能のために使用され、CocktaiLliqandMixIndex はオリジナルカクテルを登録する際にすでに登録されているカクテル名と被りがないかどうかを判定するために使用されます。

type CocktailData @model 
  @key(fields: ["cocktailname", "cocktailcreator"]) 
  @key(name: "CocktailTasteIndex", fields: ["cocktailtaste", "cocktailcreator"], queryField: "CocktailtasteIndexQuery")
  @key(name: "CocktaiLliqandMixIndex", fields: ["liqueur", "mixer"], queryField: "CocktaiLliqandMixIndexQuery")
  @key(name: "CocktailMixerIndex", fields: ["mixer", "cocktailcreator"], queryField: "CocktailmixerIndexQuery")
  @key(name: "CocktailLiqueurIndex", fields: ["liqueur", "cocktailcreator"], queryField: "CocktailliqueurIndexQuery")
  {
    cocktailname: String!
    cocktailcreator: String!
    cocktailpicture: String
    cocktailfeature: String
    cocktailtaste: String
    liqueur: String
    mixer: String
}

LiqueurData :
リキュール情報のためのテーブルです。パーティションキーを liqueurname (リキュール名) としてその他に liqueurdegree (リキュールの度数) を格納します。

type LiqueurData @model @key(fields: ["liqueurname"]){
  liqueurname: String!
  liqueurdegree: Int
}

ContainerData :
身の回りにある容器の情報のためのテーブル。リキュールとミキサーの分量の計算結果を出す際に、ペットボトルのキャップやおたまのような身の回りの容器だと何杯分になるのかを表示するために使用されます。パーティションキーを containername (身の回りにある容器名) としてその他に containercapacity (身の回りにある容器の容量)、containerpicture (身の回りにある容器の写真) を格納します。

type ContainerData @model @key(fields: ["containername"]){
  containername: String!
  containercapacity: Int
  containerpicture: String
}

HistoryData :
ユーザーがどのカクテルを作ったことがあるのかの履歴を格納するためのテーブルです。パーティションキーを username (ユーザー名)、ソートキーを unixtime (カクテルを作った日時) としてその他に cocktailname (作ったカクテル名)、cocktaildegree (作った際のカクテルの度数)、cupcapacity (作った際のコップの容量)、liqml (作った際のリキュールの量)、mixml (作った際のミキサーの量) を格納します。履歴情報を日時順に表示するために unixtime をソートキーとしています。

type HistoryData @model 
  @key(fields: ["username", "unixtime"]) 
  {
    username: String!
    unixtime: Int!
    cocktailname: String
    cocktaildegree: Int
    cupcapacity: Int
    liqml: Int
    mixml: Int
}

5. アーキテクチャ詳細 : AWS AppSync

AppSync は GraphQL API の開発を容易にする完全マネージドなサービスです。

今回 DynamoDB からデータを取り出す際やユーザー情報、自分だけのカクテルの情報、作ったカクテルの履歴情報を DynamoDB に格納するのに使用しています。Amplify を使用しているため、スキーマ定義の際に「@model」を記入することで自動で作成された Query と Mutation を使用しています。


6. アーキテクチャ詳細 : AWS Lambda

6-1. 認証情報を登録する Lambda

認証情報の登録用 Lambda では、ユーザー名を Cognito から取ってきて DynamoDB に登録する作業を行なっています。Cognito の post confirmation Lambda trigger によって、トリガーされます。以下に実際に作成した Lambda 関数のコードを示します。今回のランタイムは Node.js 14.x になります。

var AWS = require('aws-sdk');

exports.handler = (event, context, callback) => {
    var docClient = new AWS.DynamoDB.DocumentClient();
    var table = "UserData";
    var username = event.userName;
    var params = {
        TableName:table,
        Item:{
            "username": username,
            "order": 1,
        }
    };
    docClient.put(params, function(err, data) {
        if (err) {
            console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2));
        } else {
        }
    });
    console.log(event)
    callback(null, event);
};

6-2. 分量計算用 Lambda

分量計算用 Lambda では、リキュールとミキサーのそれぞれの分量を計算するために、「作りたいカクテルのアルコール度数」「使用するリュール名」「使用するコップ」を取り込みます。

次に、DynamoDB にクエリを投げることで、リキュール名からはそのリュールの度数を、使用するコップからはそのコップの容量を取り出します。

そして最後に、それらの値からリキュールとミキサーの分量を計算し、DynamoDB にそのデータを格納します。この計算したデータを AppSync が DynamoDB から取り出すことで、ユーザはアルコール度数に応じたリキュールとミキサーの分量を知ることができます。以下に実際に作成した Lambda 関数のコードを示します。今回のランタイムは Node.js 14.x になります。

const aws = require('aws-sdk');
const AWSAppSyncClient = require('aws-appsync').default;
const gql = require('graphql-tag');
global.fetch = require('node-fetch');

let graphqlClient;

exports.handler = async (event, context, callback) => {
    let env;
    let graphql_auth;
    //AppSync Clientのセットアップ
    env = process.env;
    graphql_auth = {
        type: "AWS_IAM",
        credentials: ()=> aws.config.credentials
    };
    if (!graphqlClient) {
        graphqlClient = new AWSAppSyncClient({
            url: env.API_AMPLIFYAPP_GRAPHQLAPIENDPOINTOUTPUT,
            region: env.REGION,
            auth: graphql_auth,
            disableOffline: false, 
        });
    }
    //リキュールとミキサーの分量の計算
    var liqdegee = event.arguments.liqdegree;
    var cockdegree = event.arguments.cockdegree;
    var cupcapa = event.arguments.cupcapacity;
    var liqmx  = cupcapa * cockdegree / liqdegee;
    var mixml = cupcapa - liqmx;

    //HistoryDataテーブルに計算結果を格納
    const HistoryDataInput = {
        mutation: gql(createHistoryData),
        variables: {
            input: {
                username: event.arguments.username,
                unixtime: Math.floor(Date.now() / 1000),
                cocktailname: event.arguments.cocktailname,
                cocktaildegree: cockdegree,
                cupcapacity: cupcapa
            },
        },
};

7. 工夫した点

フロントエンドの工夫としてはまず、一度作ったカクテル一覧をホーム画面に出力しています。これによりアプリにログインしたらワンクリックで前回と同じカクテルを作ることができます。またカクテルを作る際のリキュールとミキサーの分量の計算結果を身の回りの容器 (ペットボトルのキャップやおたまなど) の杯数で表現しています。これによって、家に計量カップがない方でも簡単にカクテルを作ることができます。

バックエンドの工夫は、まず、どの UI でどのようなクエリが発生するかを洗い出してからスキーマデザインを始めました。DynamoDB は Primary Key や index を初期段階でどのように設計するかが重要なため、そもそもどのような問い合わせが何故発生するのか、を明確にしておく必要があります。

そして、今回は初めてデータベースの設計に挑戦したので、あえてテーブルをデータの種類ごとに分けて、理解しやすくしました。また、使用する AWS サービスを少なく、アーキテクチャをシンプルにすることで、AWS 初心者や Web アプリ開発初心者でも簡単にアプリを作れるようにしました。

さらにビジネスロジックの実行に Lambda を用いることで全てサーバーレスで統一し、高い拡張性と可用性を実現しています。


8. まとめ

今回、自分が作りたい度数のカクテルを作成するためのリキュールとミキサーの分量を教えてくれる「Drink@home」というアプリを開発しました。「Drink@home」には他にも検索機能や自分オリジナルのカクテルレシピを作成する機能もあります。今後、AI によってユーザーごとにおすすめなカクテルを知らせてくれる機能や自分のレシピを他のユーザーにも公開できるような機能をつけていきたいです。

初めてのアプリ開発で躓くことも多かったのですが、メンターの方や他の社員の方に質問することでここまで完成させることができました。特に AWS サービスよりもフロントエンド開発に利用した React のエラーに悩まされる方が多く、「AWS を使用することで本当にやりたい作業に集中できる !」ということを身をもって体感できました。また、Amplify のアプリ開発のしやすさに驚きました。自動でバックエンドを作成して複数のサービスを AppSync で繋げてくれたり、スキーマ定義を行うだけで自動でクエリを作成してくれた時は感動しました。これからも、アプリ開発をする際は重宝していきたいです。

ぜひ皆さんも、Amplifyでのアプリ開発を試してみてください !


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

photo_takematsu-mirai

武松未来 (たけまつ みらい)
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト 長期インターン

ネットワーク系の研究室で「複数の端末がある際の周波数帯域の効率的な使用」について機械学習を用いて研究している大学院生です。趣味はテニスとお酒を飲むことで、テニスをした後のお酒がたまらなく好きです。

監修者プロフィール

photo_kanasugi_yumiko

金杉 有見子 (かなすぎ ゆみこ)
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト

インターネットメディアのお客様のクラウド活用をサポートするアーキテクト。旅とゲームが好き。

AWS のベストプラクティスを毎月無料でお試しいただけます

さらに最新記事・デベロッパー向けイベントを検索

下記の項目で絞り込む
絞り込みを解除 ≫
1

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する