💁‍♂️

AWS Amplify + React で作るチャットアプリ

2022/12/07に公開1

バージョン

モジュール バージョン
node v16.17.0
npm 9.1.3
AWS CLI 2.3.6
Amplify CLI 10.5.1
TypeScript 4.9.3
React 18.2.0

概要

何を作るか

チャットや SNS のように、今投げた投稿が内容がすぐに画面上に反映されるような web アプリを作成する。

ざっくり要件は以下の通り。

  • 投稿がすぐに画面に反映されること
    • CRUD 操作のうち、C と R ができること
  • 投稿が DB に保存されること
  • アプリの利用に認証を必要とすること

アーキテクチャ概要

準備

事前準備

以下を利用できる環境であること。

  • NodeJS
  • AWS CLI

Amplify CLI のインストール(必要な場合)

以下のコマンドを実施

npm install -g @aws-amplify/cli

フロントエンド準備

CRA でフロントエンドの作成

npx create-react-app <任意のプロジェクト名> --template typescript

プロジェクト名はケバブケース (kebab-case) 推奨っぽい
--template typescript をつけないと JavaScript 利用になっちゃうので注意。

本ケースのプロジェクト名は chat-app-demo とした。

Amplify をフロントエンドで利用するためのモジュールインストール

CRA で作成したプロジェクトのディレクトリ直下に移動する。

cd chat-app-demo

以下コマンドを実行して、Amplify をフロントエンドで利用するためのモジュールをインストールする。
なお、インストールは yarn を利用してもよい。

npm install aws-amplify @aws-amplify/ui-react aws-amplify-react

フロントエンドの挙動確認

以下コマンドを実行すると、開発モードでアプリが立ち上がる。

npm start

localhost:3000 で以下のような画面が表示されれば成功。

確認がとれたらctrl + c でアプリを停止する。

AWS 準備

IAM ユーザー作成(必要な場合)

AWS マネジメントコンソール > IAM > ユーザー > ユーザーを追加 と遷移する。

以下のように入力する。

  • ユーザー名: <任意>
  • アクセスキー - プログラムによるアクセス: チェック
  • パスワード - AWS マネジメントコンソールへのアクセス: チェックなし

次のステップ: アクセス権限」をクリック。

AdministratorAccess-Amplify にチェック。

次のステップ: タグ」をクリック。

何もせずに「次のステップ: 確認」をクリック。

問題なければ「ユーザーの作成」をクリック。

.csv のダウンロード」をクリックして認証情報を取得しておく。

認証情報の登録

AWS CLIAmplify CLI が AWS リソースにアクセスする際に利用する認証情報を登録する。
以下のコマンドを実行する。

aws configure --profile <任意のプロファイル名>

今回はプロファイル名を chat-app-demo-profile とする。

対話形式で以下とおり 4 つの項目を入力する。

AWS Access Key ID [None]: <ダウンロードした csv に記載のアクセスキー>
AWS Secret Access Key [None]: <ダウンロードした csv に記載のシークレットアクセスキー>
Default region name [None]: ap-northeast-1 
Default output format [None]: json

リージョンは任意だが今回は ap-northeast-1 とした。

なお、認証情報は以下のファイルに登録されている。

Mac

  • ~/.aws/credencials
  • ~/.aws/config

中身はこんな感じ

credencials
[default]
aws_access_key_id=XXX
aws_secret_access_key=XXX
[chat-app-demo-profile]
aws_access_key_id = XXX
aws_secret_access_key = XXX
config
[default]
region=ap-northeast-1
[profile chat-app-demo-profile]
region = ap-northeast-1
output = json

Windows

  • %USERPROFILE%\.aws\config
  • %USERPROFILE%\.aws\credentials

参考: 共有の場所configそしてcredentialsファイル

これらは必要な場面で AWS CLI、Amplify CLI から呼び出される。

Amplify による AWS リソース作成

初期設定

本コマンドでは対話形式で初期設定(初期の CloudFormation template 作成)を行う。
設定される項目や質問される事項と今回の設定値は以下の通り。

  1. プロジェクト名 → デフォルトでOK
  2. 設定は以下の通りで間違いないか → OK
設定項目 設定値
Name chatappdemo
Environment dev
Default editor Visual Studio Code
App type javascript
Javascript framework react
Source Directory Path src
Distribution Directory Path build
Build Command npm run-script build
Start Command npm run-script start
  1. Amplify CLI の認証方法 → AWS profile
  2. どのプロファイルを利用するか → chat-app-demo-profile (認証情報の登録で登録したもの)
    ここまで設定されるとプロジェクトのルートディレクトリ直下に amplify というディレクトリが作成されているのがわかる。
    ここに Amplify CLI を利用したときに追加、編集される諸々のファイルができており、例えば amplify ディレクトリ直下の .config ディレクトリ内に作成されている project-config.json には、質問 2 で確認された設定が記載されている。
  3. amplify の改善に協力するため、センシティブでない設定や失敗を共有してくれるか → 親切な人は yes

ちなみに、ここまでで amplify ディレクトリ内のディレクトリ構成は以下の通り。

上記を踏まえた上で以下コマンドを実行しよう。

amplify init

すると以下のような対話形式で設定が進められる。

Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project chatappdemo
The following configuration will be applied:

Project information
| Name: chatappdemo
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: react
| Source Directory Path: src
| Distribution Directory Path: build
| Build Command: npm run-script build
| Start Command: npm run-script start

? Initialize the project with the above configuration? (Y/n)Yes
Using default provider  awscloudformation
? Select the authentication method you want to use: (Use arrow keys)
❯ AWS profile 
  AWS access keys 
  
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Please choose the profile you want to use 
  default 
❯ chat-app-demo-profile 
Adding backend environment dev to AWS Amplify app: xxx...

Deployment completed.
Deployed root stack chatappdemo [ ======================================== ] 4/4
        amplify-chatappdemo-dev-12101  AWS::CloudFormation::Stack     CREATE_COMPLETE                Sun Dec 04 2022 01:21:42…     
        AuthRole                       AWS::IAM::Role                 CREATE_COMPLETE                Sun Dec 04 2022 01:21:39…     
        UnauthRole                     AWS::IAM::Role                 CREATE_COMPLETE                Sun Dec 04 2022 01:21:39…     
        DeploymentBucket               AWS::S3::Bucket                CREATE_COMPLETE                Sun Dec 04 2022 01:21:30…     

✔ Help improve Amplify CLI by sharing non sensitive configurations on failures (y/N) · no
Deployment bucket fetched.
✔ Initialized provider successfully.
✅ Initialized your environment successfully.

Your project has been successfully initialized and connected to the cloud!

Some next steps:
"amplify status" will show you what you've added already and if it's locally configured or deployed
"amplify add <category>" will allow you to add features like user login or a backend API
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify console" to open the Amplify Console and view your project status
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

Pro tip:
Try "amplify add api" to create a backend API and then "amplify push" to deploy everything

記載の通り、ここまでで CloudFormation Template に追加されたリソースは以下のとおり。

  • CloudFormationStack
  • AuthRole
  • UnauthRole
  • DeploymentBucket

これらは、 amplify > backend > awscloudformation/build > root-cloudformation-stack.json に記載されている。

authの追加

以下コマンドを実行して、 Cognito ユーザープールをテンプレートに追加する。

amplify add auth

以下のように対話形式で設定を進める。

Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? 
❯ Default configuration 
  Default configuration with Social Provider (Federation) 
  Manual configuration 
  I want to learn more. 
Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? (Use arrow keys)
❯ Username 
  Email 
  Phone Number 
  Email or Phone Number 
  I want to learn more.
 Do you want to configure advanced settings? (Use arrow keys)
❯ No, I am done. 
  Yes, I want to make some additional changes. 
✅ Successfully added auth resource chatappdemoxxx locally

✅ Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

上記コマンドを実行すると、amplify > backend 直下に auth/chatappdemoXX.. というディレクトリが追加され、直下の build/chatappdemoXX..-cloudformation-template.json にリソースの設定などが記載されています。

AWS 環境への反映

以下コマンドを実行して、 作成された cloudformation template を AWS 環境にデプロイすると、リソースが作成される。

amplify push -y

amplify init で作成したリソースのスタックが作成され、amplify add auth で作成したリソースはネストされたスタックとして作成されている。

API追加

以下コマンドを実行して、AppSync、DynamoDB をテンプレートに追加する。

amplify add api

? Select from one of the below mentioned services: (Use arrow keys)
❯ GraphQL 
  REST 
? Here is the GraphQL API that we will create. Select a setting to edit or continue 
  Name: chatappdemo 
❯ Authorization modes: API key (default, expiration time: 7 days from now) 
  Conflict detection (required for DataStore): Disabled 
  Continue 
? Choose the default authorization type for the API 
  API key 
❯ Amazon Cognito User Pool 
  IAM 
  OpenID Connect 
  Lambda 
Use a Cognito user pool configured as a part of this project.
? Configure additional auth types? (y/N) N
? Here is the GraphQL API that we will create. Select a setting to edit or continue (Use arrow keys)
  Name: chatappdemo 
  Authorization modes: Amazon Cognito User Pool (default) 
  Conflict detection (required for DataStore): Disabled 
❯ Continue 
? Choose a schema template: 
  Single object with fields (e.g., “Todo” with ID, name, description) 
  One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”) 
❯ Objects with fine-grained access control (e.g., a project management app with owner-based authorization) 
  Blank Schema
⚠️ WARNING: owners may reassign ownership for the following model(s) and role(s): PrivateNote: [owner]. If this is not intentional, you may want to apply field-level authorization rules to these fields. To read more: https://docs.amplify.aws/cli/graphql/authorization-rules/#per-user--owner-based-data-access.
✅ GraphQL schema compiled successfully.

Edit your schema at /Users/tanashoe/Desktop/work/ChatAppDemo/chat-app-demo/amplify/backend/api/chatappdemo/schema.graphql or place .graphql files in a directory at /Users/tanashoe/Desktop/work/ChatAppDemo/chat-app-demo/amplify/backend/api/chatappdemo/schema
? Do you want to edit the schema now? (Y/n) › Y

ここまで入力するとエディタで schema.graphql というファイルが開かれる。
これを以下のように編集する。

shcema.graphql
# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!

type ChatMessage
  @model
  @auth(
    rules: [
      { allow: groups, groups: ["Users"], operations: [create, update, read, delete] }
    ]
  ) {
  id: ID!
  message: String!
}

pushして AWS 環境に反映しておく。

amplify push

以下のような対話で追加の設定を行う。

? Are you sure you want to continue? (Y/n) Y
⚠️  WARNING: Global Sandbox Mode has been enabled, which requires a valid API key. If
you'd like to disable, remove "input AMPLIFY { globalAuthRule: AuthRule = { allow: public } }"
from your GraphQL schema and run 'amplify push' again. If you'd like to proceed with
sandbox mode disabled, do not create an API Key.

? Would you like to create an API Key? (y/N) › N
・・・
? Do you want to generate code for your newly created GraphQL API (Y/n) Y
? Choose the code generation language target 
  javascript 
❯ typescript 
  flow
? Enter the file name pattern of graphql queries, mutations and subscriptions (src/graphql/**/*.ts) <エンター>
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions (Y/n) Y
? Enter maximum statement depth [increase from default if your schema is deeply nested] 3
? Enter the file name for the generated code (src/API.ts) <エンター>
? Do you want to generate code for your newly created GraphQL API (Y/n) Y

ここでCognitoにユーザーを作成しておく。

Cognito ユーザー作成

AWS マネジメントコンソールから Cognito > Amplify で作成したユーザープール > ユーザーを作成 と進んで、以下のように入力する(メールアドレスも入力する)。

作成後、登録したメールアドレスに、Cognito 側で自動生成されたパスワードが届くので保管しておく。

ユーザーをグループに追加

API追加の章で、GraphQL のスキーマ定義をした際に Users グループに登録されたユーザーからの操作しか受け付けていなかったので、このユーザーを Users グループに追加する必要がある。
Amplify で作成したユーザープール > グループタブ > グループを作成 と進んで、以下のように入力し、グループを作成。

ユーザータブから先ほど作成したユーザーを選択して、ユーザーをグループに追加 をクリック。
Users グループにチェックして 追加 をクリック。

フロントエンドの編集

ここから、フロントエンドのコードを書いていく。

プロジェクトのルートディレクトリ直下で以下のコマンドを実行して、フロントエンドの挙動確認で実行した時と同じような画面が表示されることを確認する。

npm start

今回スタイリンングにはMUIを利用する。
以下コマンドでインストールする。

npm install @mui/material @emotion/react @emotion/styled

以下コマンドを実行することで、スキーマに対応する ChatMessage というオブジェクトを自動生成してくれる。これをメッセージ作成の時に利用する。

amplify codegen models

生成されたオブジェクトは src > models で確認できる。

src > App.tsx を開く。

以下のようにコードを変更。

App.tsx
import React, { useState, useEffect } from "react";
import { Amplify, API, graphqlOperation } from "aws-amplify";
import { Authenticator } from "@aws-amplify/ui-react";
import awsconfig from "./aws-exports";
import "@aws-amplify/ui-react/styles.css";
import { ChatMessage as ChatMessageInstance } from "./models";
import { listChatMessages } from "./graphql/queries";
import { createChatMessage } from "./graphql/mutations";
import { onCreateChatMessage } from "./graphql/subscriptions";
import {
  ListChatMessagesQuery,
  OnCreateChatMessageSubscription,
  ChatMessage,
} from "./API";
import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";

import { GraphQLResult } from "@aws-amplify/api";

Amplify.configure(awsconfig);

type SubscriptionEvent = {
  value: {
    data: OnCreateChatMessageSubscription;
  };
};

const styles = {
  main: {
    margin: 16,
    height: 504,
    overflow: "auto",
  },
  footer: {
    margin: 16,
    marginLeft: 24,
    height: 64,
  },
  message: {
    margin: 8,
    padding: 8,
    display: "flex",
    width: 300,
  },
  messageInput: {
    width: 300,
    marginRight: 8,
  },
};

function App() {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
  const [inputMessage, setInputMessage] = useState<string>("");

  async function fetchData() {
    const chatMessageData = (await API.graphql(
      graphqlOperation(listChatMessages)
    )) as GraphQLResult<ListChatMessagesQuery>;

    if (chatMessageData.data?.listChatMessages?.items) {
      const messages = chatMessageData.data.listChatMessages
        .items as ChatMessage[];
      setChatMessages(sortMessage(messages));
    }
  }

  function sortMessage(messages: ChatMessage[]) {
    return [...messages].sort(
      (a, b) =>
        new Date(a.createdAt!).getTime() - new Date(b.createdAt!).getTime()
    );
  }

  async function saveData() {
    const model = new ChatMessageInstance({
      message: inputMessage,
    });
    await API.graphql(
      graphqlOperation(createChatMessage, {
        input: model,
      })
    );
    setInputMessage("");
  }

  function onChange(messge: string) {
    setInputMessage(messge);
  }

  useEffect(() => {
    fetchData();
    const onCreate = API.graphql(graphqlOperation(onCreateChatMessage));

    if ("subscribe" in onCreate) {
      const subscription = onCreate.subscribe({
        next: ({ value: { data } }: SubscriptionEvent) => {
          const newMessage: ChatMessage = data.onCreateChatMessage!;
          setChatMessages((prevMessages) =>
            sortMessage([...prevMessages, newMessage])
          );
        },
      });

      return () => {
        subscription.unsubscribe();
      };
    }
  }, []);

  return (
    <Authenticator>
      {({ signOut, user }) => (
        <main>
          <h2>Hello {user?.username}</h2>
          <button onClick={signOut}>Sign out</button>
          <Box style={styles.main}>
            {chatMessages &&
              chatMessages.map((message, index) => {
                return (
                  <Chip
                    key={index}
                    label={message.message}
                    color="primary"
                    style={styles.message}
                  />
                );
              })}
          </Box>
          <Box style={styles.footer}>
            <TextField
              variant="outlined"
              type="text"
              color="primary"
              size="small"
              value={inputMessage}
              style={styles.messageInput}
              onChange={(e) => onChange(e.target.value)}
              placeholder="メッセージを入力"
            />
            <Button
              variant="contained"
              color="primary"
              onClick={() => saveData()}
            >
              投稿
            </Button>
          </Box>
        </main>
      )}
    </Authenticator>
  );
}

export default App;

エラーが出ていなければ、localhost:3000 で実行されているアプリは以下のような画面を表示しているはず。

ここに[Cognito ユーザー作成](#Cognito ユーザー作成)で追加したユーザーのユーザーネームと、登録したメールアドレスに届いたパスワードを入力する。
するとパスワード変更画面に遷移するので、任意のパスワードに変更する。

変更して再アクセスすると以下のような画面が表示されている。

自由にメッセージを入力して「投稿」をクリックすると、画面にメッセージが即時反映される。

以上でチャットアプリ作成の Phase.1 を完了とする。

メモ

基本的な型定義は全て API.ts に記述されている。
なのでメッセージに型をつける場合、利用するのは、API.ts で定義されているもの。

amplify codegen models で生成されるのは、ChatMessage クラスで、メッセージ作成時に利用する。

出力されたエラー

内部で色々使ってるみたいだけど、たぶん MUI コンポーネント内の style タグでスタイリングしてるからだと思う。

Failed to compile.

Module not found: Error: Can't resolve '@emotion/react' in '/Users/tanashoe/Desktop/work/ChatAppDemo/chat-app-demo/node_modules/@mui/styled-engine/GlobalStyles'
WARNING in [eslint] 
src/App.tsx
  Line 114:6:  React Hook useEffect has a missing dependency: 'fetchData'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

ERROR in ./node_modules/@mui/styled-engine/GlobalStyles/GlobalStyles.js 3:0-40
Module not found: Error: Can't resolve '@emotion/react' in '/Users/tanashoe/Desktop/work/ChatAppDemo/chat-app-demo/node_modules/@mui/styled-engine/GlobalStyles'

ERROR in ./node_modules/@mui/styled-engine/StyledEngineProvider/StyledEngineProvider.js 3:0-47
Module not found: Error: Can't resolve '@emotion/react' in '/Users/tanashoe/Desktop/work/ChatAppDemo/chat-app-demo/node_modules/@mui/styled-engine/StyledEngineProvider'

ERROR in ./node_modules/@mui/styled-engine/index.js 7:0-39
Module not found: Error: Can't resolve '@emotion/styled' in '/Users/tanashoe/Desktop/work/ChatAppDemo/chat-app-demo/node_modules/@mui/styled-engine'

ERROR in ./node_modules/@mui/styled-engine/index.js 35:0-62
Module not found: Error: Can't resolve '@emotion/react' in '/Users/tanashoe/Desktop/work/ChatAppDemo/chat-app-demo/node_modules/@mui/styled-engine'

ERROR in ./node_modules/@mui/system/esm/ThemeProvider/ThemeProvider.js 11:27-60
export 'ThemeContext' (imported as 'StyledEngineThemeContext') was not found in '@mui/styled-engine' (possible exports: GlobalStyles, StyledEngineProvider, default, internal_processStyles)

ERROR in ./node_modules/@mui/system/esm/index.js 1:0-88
export 'css' (reexported as 'css') was not found in '@mui/styled-engine' (possible exports: GlobalStyles, StyledEngineProvider, default, internal_processStyles)

ERROR in ./node_modules/@mui/system/esm/index.js 1:0-88
export 'keyframes' (reexported as 'keyframes') was not found in '@mui/styled-engine' (possible exports: GlobalStyles, StyledEngineProvider, default, internal_processStyles)

webpack compiled with 7 errors and 1 warning
Files successfully emitted, waiting for typecheck results...
Issues checking in progress...
No issues found.

以下をインストールして解消。

  • @emotion/styled
  • @emotion/react

参照サイト様

基本的な進め方: Amplifyで簡単に作れるリアルタイムチャット機能
API リクエストへの型付け: AmplifyのAPIリクエストをTypeScriptでちゃんと型をつける
公式チュートリアル: Amplify Dev Center Tutorial
GraphQL API について: Amplify Dev Center API(GraphQL)

Discussion

tatesuketatesuke

こんにちは。

  async function saveData() {
    const model = new ChatMessageInstance({
      message: inputMessage,
    });
    await API.graphql(
      graphqlOperation(createChatMessage, {
        input: model,
      })
    );
    setInputMessage("");
  }

この部分、new ChatMessageInstance(...)としてしまうと私の環境ではエラーとなってしまいました。

"The variables input contains a field that is not defined for input object type 'CreateChatMessageInput' "

以下のように単なるオブジェクト定義にしたらおそらく狙い通りに動きました。

const model = {
  message: inputMessage,
};