React, GraphQLで リアルタイムチャットアプリを作成する
この記事は 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
- graphqlrimasu
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