🐶

React, GraphQLで リアルタイムチャットアプリを作成する

2022/12/25に公開

この記事は Money Forward Engineering 1 Advent Calendar 2022 25日目の投稿です🎄

はじめに

GraphQL Subscriptionの学習も兼ねてリアルタイムチャットアプリを作成しました。
本当はGoとGraphQLでバックエンドを作成していたのですが時間が足りずに断念し、AWS Appsync, Amplifyにお任せしちゃいました。。。認証機能も簡単に作れるそうなのでそれもつけてみました🙇‍
Go×GraphQLについては今後ちゃんと記事を書こうと思います。

React プロジェクト作成

まずはReactのプロジェクトを作成します。

npx create-react-app chatapp --template typescript

作成できたら動くか動作確認をします。

cd chatapp
npm start

Amplify 環境構築

Amplify CLI インストール

Amplify を使うために Amplify CLI をインストールします。

npm install -g @aws-amplify/cli

Amplify アカウント設定

AWS Profile の設定ができている人は次のバックエンド初期化に進んでください。
できていない人は下記コマンドでAWS アカウントの設定を行います。

amplify configure

Amplify 初期化

Amplify CLIがインストールできたら、初期化を行います。

# Reactプロジェクト内で実行していきます
cd chatapp
amplify init

実行すると下記のように色々と聞かれますで答えていきます。

? Enter a name for the project (chatapp) 

プロジェクト名はこのまま(chatapp)でいいのでEnter

The following configuration will be applied:

Project information
| Name: chatapp
| 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) 

プロジェクト情報が表示されます。 この構成で初期化するかどうか聞かれます
ここもEnter

? Select the authentication method you want to use: (Use arrow keys)
❯ AWS profile 
  AWS access keys 

使用する認証方法を選択します
今回はAWS profileを使用するので、このままEnter

? Please choose the profile you want to use (Use arrow keys)
❯ default

次に設定している AWS profile からどれを使用するか選択してEnter

認証機能の作成

Amplify 認証機能の追加

amplify add auth

こちらも回答していきます

 Do you want to use the default authentication and security configuration? (Use arrow keys)
❯ Default configuration 
  Default configuration with Social Provider (Federation) 
  Manual configuration 
  I want to learn more.

Enter
デフォルトの認証、セキュリティ構成を使用するかどうかを選択。
Default configurationを選択します

 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. 

Enter
ユーザーのサインイン方法を選択します。Usernameを選択します

 Do you want to configure advanced settings? (Use arrow keys)
❯ No, I am done. 
  Yes, I want to make some additional changes. 

Enter
詳細設定を構成するか聞かれるのでNo, I am done.を選択します

amplify status を実行してみると Auth設定が追加されていることが確認できます。

Amplify 認証機能をデプロイ

認証機能の設定ができたらデプロイします

amplify push

...

Deployment completed.
Deployed root stack chatapp [ ======================================== ] 2/2
        amplify-chatapp-dev-202458     AWS::CloudFormation::Stack     UPDATE_COMPLETE
        authchatapp6e214a87            AWS::CloudFormation::Stack     CREATE_COMPLETE
Deployed auth chatapp6e214a87 [ ======================================== ] 10/10
        UserPool                       AWS::Cognito::UserPool         CREATE_COMPLETE
        UserPoolClientWeb              AWS::Cognito::UserPoolClient   CREATE_COMPLETE
        UserPoolClient                 AWS::Cognito::UserPoolClient   CREATE_COMPLETE
        UserPoolClientRole             AWS::IAM::Role                 CREATE_COMPLETE
        UserPoolClientLambda           AWS::Lambda::Function          CREATE_COMPLETE
        UserPoolClientLambdaPolicy     AWS::IAM::Policy               CREATE_COMPLETE
        UserPoolClientLogPolicy        AWS::IAM::Policy               CREATE_COMPLETE
        UserPoolClientInputs           Custom::LambdaCallout          CREATE_COMPLETE
        IdentityPool                   AWS::Cognito::IdentityPool     CREATE_COMPLETE
        IdentityPoolRoleMap            AWS::Cognito::IdentityPoolRol… CREATE_COMPLETE

AWS コンソールから確認

Cognitoが作成されていることがわかります

React 認証画面作成

React 認証画面を作成します。

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

App.tsxを下記のように書き換えます

import './App.css';
import { Amplify } from 'aws-amplify';
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import awsConfig from "./aws-exports";
import { Button } from '@material-ui/core';

Amplify.configure(awsConfig);

function App() {
  return (
    <Authenticator>
      {({ signOut, user }) => (
        <>
          <div>名前:{user?.username}</div>
          <Button variant="contained" color="secondary" onClick={signOut}>サインアウト</Button>
        </>
      )}
    </Authenticator>
  );
}

export default App;

Amplifyの設定を下記のコード部分で行なっています

Amplify.configure(awsConfig);

Authenticatorコンポーネントを使用することでAmplifyライブラリを使用した認証を組み込んでいます

  <Authenticator>
    ...
  </Authenticator>

起動して http://localhost:3000 で確認すると下記のような画面が表示されます

補足

hideSignUpオプションでサインアップメニューを消すことも可能です

<Authenticator hideSignUp={true}>
</Authenticator>

チャット機能の作成

Amplify バックエンドの追加

バックエンドの追加を行なっていきます

amplify add api

今回も色々と聞かれるので回答していきます

? Select from one of the below mentioned services: (Use arrow keys)
❯ GraphQL 
  REST 

Enter
今回バックエンドに選択するのはGraphQLにします

? Here is the GraphQL API that we will create. Select a setting to edit or continue (Use arrow keys)
  Name: chatapp 
  Authorization modes: API key (default, expiration time: 7 days from now) 
  Conflict detection (required for DataStore): Disabled 
❯ Continue 

Enter
GraphQLの設定を確認します。これでよければ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”) 
  Blank Schema 

Enter

? Do you want to edit the schema now? (Y/n)

Y
今すぐ編集しますか? と聞かれるのでYを入力します

GraphQL スキーマ編集

今すぐ編集しますか?の問いにYを入力するとschema.graphqlの編集画面が表示されます
表示されない人はamplify/backend/api/chatapp/schema.graphqlを修正します

input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!

type Message @model {
  id: ID!
  uuid: String!
  text: String!
}

Amplify バックエンドデプロイ

amplify push

実行時に生成する言語を聞かれるので、typescriptを選択してEnter
そのEnterまたはYで大丈夫です

? Choose the code generation language target 
  javascript 
❯ typescript 
  flow 

デプロイが成功するとディレクトリ構造が下記のようになります

  • amplify
  • src
    • graphqlrimasu
      • mutations.ts
      • queries.ts
      • subscription.ts
    • API.ts

src配下にgraphqlディレクトリやAPI.tsが作成されていると思います

次にReactから使用するModelを自動作成します

amplify codegen models

src配下にmodelsディレクトリが作成されていると思います

React チャット投稿機能作成

Material UI Install

npm i @material-ui/core

App.tsxを下記のように書き換えます
(GraphQLの使い方がわかればいいので、、ちょっと甘えたコードです🙇‍♂️)

import './App.css';
import { useEffect, useState } from 'react';
import { Amplify, graphqlOperation, API } from 'aws-amplify';
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import awsConfig from "./aws-exports";
import { Button, TextField, Container, Box, AppBar, Toolbar, Typography, Card, CardContent, Grid } from '@material-ui/core';
import { Message } from "./models";
import { listMessages } from "./graphql/queries";
import { createMessage } from "./graphql/mutations";
import { onCreateMessage } from './graphql/subscriptions';
import { ListMessagesQuery, CreateMessageInput } from './API';

Amplify.configure(awsConfig);

function App() {
  const [messages, setMessages] = useState<Message[] | []>([]);
  const [inputMessage, setInputMessage] = useState<string>("");

  const fetchListMessages = async () => {
    const items = await API.graphql(graphqlOperation(listMessages))
    if ("data" in items) {
      const list = items.data as ListMessagesQuery
      setMessages((list.listMessages?.items as Message[] | []).sort((a, b) => 
        new Date(a.createdAt!).getTime() - new Date(b.createdAt!).getTime()
      ));
    }
  }

  const postMessage = async (user: any) => {
    const m: CreateMessageInput = {
      uuid: user.sub,
      text: inputMessage,
    };
    await API.graphql(graphqlOperation(createMessage, {
      input: m,
    }));
    
    setInputMessage("");
  }
 
  useEffect(() => {
    fetchListMessages();

    const onCreate: any = API.graphql(graphqlOperation(onCreateMessage));
    const sub = onCreate.subscribe({
      next: ({ value: { data }}: any) => {
        const newMessage = data.onCreateMessage;
        setMessages((prevMessages) => ([...prevMessages, newMessage]).sort((a, b) => 
          new Date(a.createdAt!).getTime() - new Date(b.createdAt!).getTime()
        ))
      }
    });

    return () => {
      sub.unsubscribe();
    };
  })

  return (
    <Authenticator>
      {({ signOut, user }) => (
        <Box sx={{ flexGrow: 1 }} style={{ width: "50%", margin: "0 auto" }}>
          <AppBar position="static">
            <Toolbar>
              <Typography variant="h6" component="div" style={{ flexGrow: "1" }}>
                {user?.username}
              </Typography>
              <Button variant="contained" onClick={signOut}>サインアウト</Button>
            </Toolbar>
          </AppBar>

          {messages.map((message) => {
            return <Container>
              <Grid container spacing={2}>
                {user?.attributes?.sub === message.uuid ? (
                  <>
                    <Grid item xs={4}><Card><CardContent>{message.text}</CardContent></Card></Grid>
                    <Grid item xs={8}></Grid>
                  </>
                ) : (
                  <>
                    <Grid item xs={8}></Grid>
                    <Grid item xs={4}><Card><CardContent>{message.text}</CardContent></Card></Grid>                        
                  </>
                )}
              </Grid>
            </Container>
          })}

          <Container>
            <div style={{ display: "flex", justifyContent: "flex-end"}}>
              <TextField id="standard-basic" label="テキスト" variant="standard" onChange={(e) => {
                setInputMessage(e.target.value)
              }}/>
              <Button variant="contained" color="secondary" onClick={() => {
                postMessage(user?.attributes)
              }}>投稿</Button>
            </div>
          </Container>
        </Box>
      )}
    </Authenticator>
  );
}

export default App;

こちらはメッセージ一覧の取得処理です
API.graphql(graphqlOperation())に対して生成したgraphql/querisのlistMessagesを渡すだけでQueryを走らせてくれます

その後はsetMessagesでmessagesに対して格納しています

import { Message } from "./models";
import { listMessages } from "./graphql/queries";

// ...

  const [messages, setMessages] = useState<Message[] | []>([]);
  
  // ...

  const fetchListMessages = async () => {
    const items = await API.graphql(graphqlOperation(listMessages))
    if ("data" in items) {
      const list = items.data as ListMessagesQuery
      setMessages((list.listMessages?.items as Message[] | []).sort((a, b) => 
        new Date(a.createdAt!).getTime() - new Date(b.createdAt!).getTime()
      ));
    }
  }
  
  // ...
  
  useEffect(() => {
    fetchListMessages();
    
    // ...
  })

次にメッセージの投稿処理です
API.graphql(graphqlOperation())に対してcreateMessageを渡してあげて、第二引数にVariableを渡すだけでいいです

import { createMessage } from "./graphql/mutations";

  // ...

  const postMessage = async (user: any) => {
    const m: CreateMessageInput = {
      uuid: user.sub,
      text: inputMessage,
    };
    await API.graphql(graphqlOperation(createMessage, {
      input: m,
    }));
    
    setInputMessage("");
  }

最後に購読処理です
useEffectで実行しています
onCreate.subscribe()でメッセージを作成するmutationのcreateMessageを購読しています。そうすることで更新があったらメッセージを返す仕組みになっています

  useEffect(() => {
    // ...
    const onCreate: any = API.graphql(graphqlOperation(onCreateMessage));
    const sub = onCreate.subscribe({
      next: ({ value: { data }}: any) => {
        const newMessage = data.onCreateMessage;
        setMessages((prevMessages) => ([...prevMessages, newMessage]).sort((a, b) => 
          new Date(a.createdAt!).getTime() - new Date(b.createdAt!).getTime()
        ))
      }
    });

    return () => {
      sub.unsubscribe();
    };
  })

動作確認

テキストを入力して投稿すると自分が投稿した際は右側に、他の人が投稿した際は左側にテキストがリアルタイムで反映されます

最後に

とにかくAmplifyが便利すぎると思いました。インフラの知識があまりなくてもフロントエンドエンジニアがGraphQLのバックエンド開発に役立つと感じました。

ぜひ興味があったら使ってみてください。ありがとうございました!

Discussion