AWS Amplifyを使ったオンラインハッカソンに英語発音練習Webアプリを作って入賞、$1,000ゲットした話


## オンラインハッカソンへの応募

Hashnodeという海外のQiitaやZennのような開発者向けブログ投稿サイトが主催で、AWSのAmplifyを使うことを条件にしたハッカソンが9月末締め切りで開催されているので、[英語発音練習Webアプリ](https://voitrain.tanosugi.com/)を作って応募してみました。

### 結果
上位5位に入賞して$1,000ゲットしました！　（上位5位は順番はつかず入賞だけ発表される仕組みのようです）

https://townhall.hashnode.com/aws-amplify-x-hashnode-hackathon-winners

[↑をGoogle翻訳で読むにはここをクリック](https://townhall-hashnode-com.translate.goog/aws-amplify-x-hashnode-hackathon-winners?_x_tr_sl=en&_x_tr_tl=ja&_x_tr_hl=ja&_x_tr_pto=wapp)

### 提出した開発内容の記事
https://tanosugi.hashnode.dev/voitrain-english-words-pronouncing-training-service-made-with-aws-amplify

### 開催要項
https://townhall.hashnode.com/aws-amplify-hackathon

[↑をGoogle翻訳で読むにはここをクリック](https://townhall-hashnode-com.translate.goog/aws-amplify-hackathon?_x_tr_sl=en&_x_tr_tl=ja&_x_tr_hl=ja&_x_tr_pto=wapp)


### 提出記事の日本語訳
ブログをアプリの機能や開発についてのブログにタグをつけて公開することで提出する仕組みです。
以下応募の際に提出した内容の日本語訳です。（英語で書いたものをDeepLで和訳して少し直しましたが、不自然な表現残っていてもご容赦ください。）

---
## はじめに
[Voitrain](https://main.d1h4jgcdasqwid.amplifyapp.com/)は、単語の発音トレーニングサービスです。用意された単語帳や自分で作ったカードを選び、発音を聞いて練習し、録音して確認することができます。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663592672647/cbMdj_RIG.png">

[AWS Amplifyで作る英単語発音トレーニングサービス「Voitrain」をチェック](https://voitrain.tanosugi.com/)

## 1. 解決したい課題
市場には既に多くのカードアプリがあります。単語を表示するだけのものや、単語と絵を表示するものもあります。しかし、これらのアプリに欠けているのは、ユーザに単語を発音させ、その発音を確認させるという点です。間違った発音で覚えてしまうと、それを修正するのは非常に難しいことなので、単語と発音を同時に覚えることが必要です。

## 2. 解決策とモチベーション
単語を見て、発音を聞いて、自分の発音を確認できるアプリがあれば、英単語を覚える人にとって有用なはずです。更に、自分だけの単語カードを作成できれば、もっと有用なはずです。ということで、自分でアプリを作ることにしました!

2022年9月のHashnode AWS Amplify ハッカソンで、アプリとコードをこの記事を通じて提出する予定です。Amplify StudioのFigma to Reactの機能を紹介したビデオを見たとき、とても驚き、使い方を学んで、プロトタイピングに使えるかどうか試してみたいと感じました。試すちょうどよい機会として、AWS Amplifyでアプリを作り、ハッカソンに参加しました。

## 3. デモと GitHub リポジトリ
Demo: (https://voitrain.tanosugi.com/)

GitHubリポジトリ：(https://github.com/tanosugi/voitrain)

## 4.Voitrainの特徴
英単語を覚えるには、繰り返し発音するのが一番です。Voitrainでは、用意された単語帳と自分で作成した単語帳を使って、単語を覚えることができます。

### 4.1 プリセット単語帳
果物や体の部位などの作成済みの単語帳ですぐに始められます。発音を聞いた後、自分で発音して、自分の発音が正しいかどうか確認することができます。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663592770909/ZqICsQvW6.png">

### 4.2 自分だけの単語帳
練習したい単語の単語帳を作成することができます。作成した単語帳は、プリセットと同じように使用することができます。単語の発音を聞いた後、自分で発音して一致するかどうか確認することができます。
写真はURLを入力するか、"update image "アイコンをクリックするとunsplash.comからランダムに取得し、変更することができます。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663592734407/tsfhUk2sV.png">

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663592752826/7PiSqrz9b.png">

## 5. Voitrainで使っているAmplifyの機能
Figma to react機能を試しているだけのブログはたくさんありますが、そのほとんどがカードコレクションとして写真を表示しているだけです。Figma to Reactはプロトタイピングに有効か確認したかったのですが、Voitrain を作ってみて、MVP や MSP には十分すぎるほどであることを確認出来ました。

### 5.1. FigmaからAmplify Studio上のreact(UIライブラリ)へ

まず、Figmaのファイルを作成しました。FigmaはGUIソフトで、Webやアプリのデザインに使える機能が多いので、自分の考えていることを視覚化するのにとても便利です。Reactの[Material UI library](https://mui.com/)を使うと、自分の考えていることをビジュアル化するのはCSSがあまり得意でない自分は時間がかかってしまいますが、Figma to Reactはずっと簡単です。
Figma to Reactの唯一の弱点は、ユーザーがデザインの知識を持っておらず、変なデザインをしてしまうと、それがアプリに直接影響してしまうことです。他のUIライブラリを使ったときは、デザインイメージからコーディングするステップでデザインが修正されました。それゆえ、Figma to Reactを使う際には、もっとデザインについて勉強する必要があります。


[Figmaファイル](https://www.figma.com/file/tNYYu3AH4QIP2Iqjt1o3AA/voitrain?node-id=3674%3A2984)

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663593980801/NcrTzYFVQ.png">

また、Figmaのプロトタイプ機能も使っています。Figmaに慣れている人なら、まずFigmaでプロトタイプを作ってから、Reactでコーディングするのですが、私はFigmaに慣れていないので、Figmaでプロトタイプを作った後、Reactでコーディングしています。しかし、正直なところ、Figmaに慣れていないので、Figmaのファイルを作ってから、Reactでコーディングして、復習としてFigmaのプロトタイプを作りました。

[Figma Prototype](https://www.figma.com/proto/tNYYu3AH4QIP2Iqjt1o3AA/voitrain?page-id=3551%3A2497&node-id=3674%3A3129&viewport=1051%2C832%2C0.65&scaling=min-zoom&starting-point-node-id=3632%3A2562)

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663593999846/qiaWZFXJv.png">


データをカードとして表示したり、データの作成や更新のためにモーダルを表示するだけなら、Amplify Figma to Reactでラップされているので、Data Store等のAPIを意識する必要はありません。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663592734407/tsfhUk2sV.png">

上記のページは、以下の簡単なコードで表示されます。


```javascript
import { Authenticator } from "@aws-amplify/ui-react";
import { Hub } from "aws-amplify";
import { useState } from "react";
import Modal from "react-modal";
import useQueryCardSetFromId from "../src/hooks/useQueryCardSetFromId";
import Center from "../src/layout/center";
import Layout from "../src/layout/layout";
import { customStyles } from "../src/layout/modalStyle";
import {
  CardSetCreateView,
  CardSetViewCollection,
  Pluscircle
} from "../src/ui-components";
import TabbarMyCardsChosenView from "../src/ui-components/TabbarMyCardsChosenView";

const Home = () => {
  const [modalToOpen, setModalToOpen] = useState("");
  const { cardSet } = useQueryCardSetFromId("");
  Hub.listen("ui", (capsule) => {
    if (
      [
        "actions:datastore:create:finished",
        "actions:datastore:update:finished",
      ].includes(capsule.payload.event)
    ) {
      setModalToOpen("");
    }
  });
  return (
    <Authenticator>
        <Layout>
          <>
            <Center>
              <TabbarMyCardsChosenView />
            </Center>
            <Center>
              <Pluscircle
                overrides={{
                  Pluscircle: {
                    onClick: () => {
                      setModalToOpen("CardCreateView");
                    },
                    margin: "20px 0px 20px 0px",
                  },
                }}
              />
            </Center>
            <Modal
              isOpen={modalToOpen == "CardCreateView"}
              style={customStyles}
            >
              <Center>
                {cardSet && (
                  <CardSetCreateView
                    cardSet={cardSet}
                    overrides={{
                      close: {
                        onClick: () => setModalToOpen(""),
                      },
                    }}
                  />
                )}
              </Center>
            </Modal>
            <Center>
              <CardSetViewCollection />
            </Center>
          </>
        </Layout>
    </Authenticator>
  );
};

export default Home;

```


### 5.2. Amplifyスタジオでのデータモデリング

Amplify Studioを使わずにAmplifyでコーディングベースでデータモデリングをしようとすると、簡単なものならともかく、データへのアクセス権まで設定しようとすると習得には時間がかかりそうです。Amplify Studioでのデータモデリングは、コーディングベースのデータモデリングに比べれば、はるかに簡単です。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663591959263/v02fBNwBl.png">

各ユーザーが作成・編集する単語帳に使用するCardSetモデルで、他のユーザーが他のユーザーの単語帳にアクセスできないように「所有者権限を有効にする」機能を使用しています。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663598046767/PD5Sey1p3.png">

PresetCardSetのデータモデルには、"Cognito User Poolで認証されたすべてのサインインユーザーがPresetCardSetを読むことができる "を選択しました。


<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663598246337/TE2VvnfDt.png">

### 5.3. Amplify Studio でのデータコンテンツ管理
20種類の果物の名前と20種類の体の部位の名前のプリセット単語帳を作成する場合、Amplify StudioのData Content Managementを使用します。各項目を作成・編集し、CSVファイルをダウンロードすることができます。20のファーストネームや通常のサンプルでデータ項目を生成したい場合は、項目を自動生成することができます。将来的にはCSVからの更新やエクセルで表のように編集できる機能があればもっと便利だと思います。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663592014493/Br2tXIzJV.png">

### 5.4. Amplify Studioでの認証
認証の導入はとても簡単でした。Amplify studioのGUIをクリックし、チュートリアルにあるコードを貼り付ければOKです。
OAuth 2を使ったgoogleログインは少し複雑で、以下の3つのステップが必要です。
1. Amplify Studio上でGoogleログインを追加
2. GCP上で認証情報を追加
3. Amplify StudioからGCPにリダイレクトURLをコピー
4. Web Client IDとWeb Client SecretをGCPからAmplifyにコピー
5. AWSマネジメントコンソールで環境変数を設定が必要です。Web Client IDをGCPからAMPLIFY_GOOGLE_CLIENT_IDに、Web Client SecretをGCPからAMPLIFY_GOOGLE_CLIENT_SECRETにコピー。

以下のスクリーンショットをご参照ください。
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663596566251/9U_FoDS9-.png">


### 5.5. テキストからスピーチ、スピーチからテキスト(Amplify Predictions)
以下の記事を参考に、Text to Speach (Amazon Polly), Speach to Text (Amazon Transcribe) に Amplify Prediction を使ってみました。AmplifyのデフォルトのIAMをPollyとTranscribeのアクセス権に変更する必要があります。
[リアルタイムSpeech to Text Reactアプリケーションの構築](https://towardsdatascience.com/building-a-real-time-speech-to-text-react-application-using-aws-amplify-8befac6aca2c)

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663594470085/twcPz39Eh.png">


### 5.6. Amplify Data Store
コーディングしてロジックを導入してデータを修正する場合、Data Storeを使う必要があります。ユーザーが「update image」アイコンをクリックすると、Unsplash APIを元に単語の画像が変更される機能を作りました。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663594950856/6JISm57gT.png">



```javascript
import { DataStore } from "aws-amplify";
import useAxios from "axios-hooks";
import { useEffect, useState } from "react";
import { CardSet } from "../models";
import { unsplashApiKeyRandom } from "../utils/envvar";

const useQueryCardsFromCardSetId = (cardSetId: string) => {
  const [cardSet, setCardSet] = useState<CardSet>();
  const [
    { data: unsplashData, loading: unsplashLoading, error: unsplashError },
    executeUnsplash,
  ] = useAxios({
    url:
      "https://api.unsplash.com/photos/random?query=" +
      cardSet?.name +
      "&client_id=" +
      unsplashApiKeyRandom,
  });
  const fetchCards = async () => {
    if (cardSetId) {
      const respCardSet = await DataStore.query(CardSet, cardSetId);
      if (respCardSet) {
        setCardSet(respCardSet);
      }
    } else if (cardSetId == "") {
      setCardSet(new CardSet({ name: "", image_url: "" }));
    }
  };
  const updateCardSetImageUrl = async () => {
    try {
      await executeUnsplash();
    } catch (e) {
      alert(e);
    }
    DataStore.save(
      CardSet.copyOf(cardSet || new CardSet({}), (updated) => {
        updated.image_url = unsplashData?.urls?.small;
      })
    );
  };
  useEffect(() => {
    fetchCards();
    const subscription = DataStore.observe(CardSet).subscribe(fetchCards);
    return () => {
      subscription.unsubscribe();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cardSetId]);
  return {
    unsplashData,
    cardSet,
    updateCardSetImageUrl,
    executeUnsplash,
  };
};

export default useQueryCardsFromCardSetId;

```

### 5.7. Amplify hosting と Amplify data model
驚くほど簡単です。
以前、ReactとGraphQLバックエンドのアプリを作る際は、フロントエンドのインフラにS3、CloudFront、バックエンドのインフラにApprunner、データベースにRDSを構築する必要があり、インフラとしてのコードはterraformで、デプロイフローはCircleCiで作りました。これらの構築には1ヶ月を要しました。
Amplifyのホスティングとデータモデルがそのほとんどを代替してくれています。テラフォームやCircleCiを使った手作業の方が柔軟なのは分かりますが、フロントエンドとバックエンドが必要なケースの半分以上はAmplifyでカバーできると思っています。

### 5.8. Amplifyのドメイン管理
オリジナルドメインを購入してRoute 53に登録し、AWSのマネジメントコンソールでAmplifyのドメイン管理機能を使って設定します。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663866195942/HrzDFZhUx.png">

## 6. その他Voitrainで使用している技術

### 6.1. Unsplash APIとAxios-hooksを組み合わせた独自フック
Unsplash API（ランダムな写真を取得する機能）を使いました。
[ランダムに写真を得る機能](https://unsplash.com/documentation#get-a-random-photo)で、単語帳やカードの写真を設定することができます。することができます。

### 6.2. Next js
Amplify HostingでNext jsを簡単にビルドしてデプロイすることができました。

### 6.3. JavaScriptのオーディオオブジェクト
アプリ上で発音を確認し、正解の場合は「ピンポン！」という音が聞こえます。答えが不正解の場合は「ブーブー！」という音が聞こえます。音は、JavaScriptのAudio Objectを使いました。お子様やお友達と一緒にVoitrainを楽しんでみてはいかがでしょうか。
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663592672647/cbMdj_RIG.png">

### 6.4. react-modal
Amplify Figma to React機能と組み合わせてreact-modalを使って、編集画面を表示させてみました。

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663597707515/xfszOCLOt.png">

### 6.5. Google Analytics、LogRocket、Sentryの利用
Google Analytics、LogRocket、Sentryを使用して、ユーザーの行動やエラーの分析を行っています。

## 8. まとめ

子供が寝た後に毎日コーディングしていたので、記事作成も含めると2週間で合計40〜50時間くらいの作業時間でした。一日中作業する集合型のハッカソンなら3〜4日換算です。

作りたかったアプリも作ることが出来、ハッカソンにも参加し、Amplify StudioやFigma to Reactの有用性も確認でき、非常に有意義な時間でした。