React.js & Next.js, Material UI + AWS Lambdaでお問合せフォームを作ってみた

# はじめに
今回の記事は以下2冊の書籍の復習を兼ねて作成した内容になります。

* React.js & Next.js 超入門（掌田津耶乃さん 著）
* AWS Lambda実践ガイド（大澤文孝さん 著）

# 何を作ったのか
今回は上記2冊で学んだ内容を総動員ということで以下のようなお問合せフォームを作成してみました。
こちらに内容を記載し、「送信」ボタンをクリックすると管理人へメールが飛ぶ仕様です。
まあこれだけですとわざわざライブラリ等を利用するほどでもないのですが、一応復習ということで敢えて利用して作りました。
フロントエンドではReact.js & Next.js, CSS, Material UIを利用し、バックエンドではLambda(Python), S3, DynamoDB, API Gatewayを利用しています。
![お問合せフォーム.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538249/1c4031e8-0ca8-917c-2003-2e3ae2dce7e8.gif)

# 実装内容
簡単ではありますが実装内容を振り返っていきたいと思います。

### 画面系

こちらはコンポーネントとして「ヘッダー」「フォーム画面」「フッター」「レイアウト」とそれぞれ分けてファイルを作成しています。
https://github.com/Oooooomin2/ContactForm/tree/main/components

個人的にはMaterial UIの破壊力といいますか、便利さとデザインの綺麗さに圧倒されてしまいました。
今までは「簡単にデザイン等を作りたい」という時はBootstrapを使っていたのですが、こちらの方がデザインが今風で綺麗ですね。
以下のコードのみでテキストフィールドやヘッダーが作れちゃいました(送信ボタンとフッターは別途CSSで作成しています。)

```Javascript:ContactForm.js
import React, { Component } from 'react';
import { TextField, Input } from '@material-ui/core';
import Grid from '@material-ui/core/Grid';

class ContactForm extends Component {
    render() {

        return (
            <Grid container alignItems="center" justify="center">
                <form id="ContactForm" action="">
                    <Grid className="gridItems" item md={12}>
                        <TextField
                            required
                            fullWidth
                            id="Username"
                            label="お名前"
                            variant="outlined"
                            color="secondary"
                        />
                    </Grid>
                    <Grid className="gridItems" item md={12}>
                        <TextField
                            required
                            fullWidth
                            id="MailAddress"
                            label="メールアドレス"
                            variant="outlined"
                            color="secondary"
                        />
                    </Grid>
                    <Grid className="gridItems" item md={12}>
                        <TextField
                            fullWidth
                            id="PhoneNumber"
                            label="電話番号"
                            variant="outlined"
                            color="secondary"
                        />
                    </Grid>
                    <Grid className="gridItems" item md={12}>
                        <TextField
                            required
                            fullWidth
                            rows={15}
                            multiline
                            id="Contact"
                            label="お問合せ内容"
                            variant="outlined"
                        />
                    </Grid>
                    <Grid id="SubmitButtonGrid" className="gridItems" item md={12}>
                        <Input
                            id="SubmitButton"
                            type="submit"
                            variant="outlined"
                        />
                    </Grid>
                </form>
            </Grid>
        );
    }
}
export default ContactForm; 
```

```Javascript:Header.js
import React, { Component } from 'react';
import { AppBar, Toolbar, Typography } from '@material-ui/core';

class Header extends Component {
    render() {
        return (
            <header>
                <AppBar position="static">
                    <Toolbar>
                        <Typography variant="h6">
                            お問合せフォーム
                        </Typography>
                    </Toolbar>
                </AppBar>
            </header>

        );
    }
}
export default Header;
```

今回のお問合せフォームでは感じませんでしたが、UIを「コンポーネント」という部品で細かく管理していくReactは大きなサービスを作るにあたってはかなり有用だなと感じました。次回以降趣味等でアプリを作る際はReactが大活躍するよう大きめのアプリを作ってみようと思います。

### サーバ側
サーバ側の処理の流れは以下になります。
（アプリはS3の静的ウェブサイトホスティング機能を用いて配信しています。）

1. 「送信」ボタンを押します。
1. API Gatewayを通してLambda関数へフォームデータが渡ります。
1. DynamoDBに問い合わせ内容を格納します。
1. Amazon SESを用いて管理者へ問い合わせ通知メールを送ります。

Lambda関数は以下です。
こちらのコードは上記の「AWS Lambda実践ガイド」に記載されたコードへ少々変更を加えて作成しています。
Lambda関数を作る際はAWS側が提供してくれる設計図（1からコードを書かなくてもいいように用途ごとに用意されている大まかなサンプルコード）がありますが僕にとっては本のコードが設計図になりました。
凄く勉強になりました。ありがとうございます。

```Python:Contact.py
import json
import boto3
import urllib.parse
import time
import decimal
import base64

dynamodb = boto3.resource('dynamodb')

MAILFROM='メールアドレス'
def sendmail(to, subject, body):
    client = boto3.client('ses')
    
    response = client.send_email(
        Source=MAILFROM,
        ReplyToAddresses=[MAILFROM],
        Destination={
            'ToAddresses': [ to ]
        },
        Message={
            'Subject': {
                'Data': subject,
                'Charset': 'UTF-8'
            },
            'Body' :{
                'Text': {
                    'Data': body,
                    'Charset': 'UTF-8'
                }
            }
        }
    )
    
def next_seq(table, tablename):
    response = table.update_item(
        Key={
            'tablename': tablename
        },
        UpdateExpression="set seq = seq + :val",
        ExpressionAttributeValues={
            ':val' : 1
        },
        ReturnValues='UPDATED_NEW'
    )
    return response['Attributes']['seq']

def lambda_handler(event, context):
    try:
        seqtable=dynamodb.Table('sequence')
        nextseq = next_seq(seqtable, 'contact')
        
        body = base64.b64decode(event['body'])
        param = urllib.parse.parse_qs(body.decode('utf-8'))
        username=param['username'][0]
        mailaddress=param['mailaddress'][0]
        phonenumber=param['phonenumber'][0]
        contact=param['contact'][0]
        
        host=event['requestContext']['http']['sourceIp']
        
        now=time.time()
        
        contacttable = dynamodb.Table("ContactInfo")
        contacttable.put_item(
        Item={
            'id': nextseq,
            'username': username,
            'mailaddress': mailaddress,
            'phonenumber' :phonenumber,
            'contact': contact,
            'accepted': decimal.Decimal(str(now)),
            'host': host
            }
        )
        
        mailbody = """
        
        {0}さん({1})からお問合せメッセージが届いたよ！
        
        {2}
        """.format(username, mailaddress, contact)
        
        sendmail(MAILFROM, 'お問合せ', mailbody)
    
        return {
            'statusCode': 200,
            'headers' : {
                'content-type': 'text/html'
            },
            'body':'<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>お問合せありがとうございました。</body></html>'
        }
    
    except:
        import traceback
        traceback.print_exc()
        return {
            'statusCode': 500,
            'headers': {
                'content-type': 'text/html'
            },
            'body': '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>内部エラーが発生しました。</body></html>'
        }
```

※エラーが発生した場合はCloudWatchにログが出力されますので確認してみてください。僕は何度もエラー出しました。

### 使ってみる
いざお問合せフォームを入力して「送信」ボタンを押すとメールも届いてDynamoDBにもしっかりと情報が格納されていました。

![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538249/9ba8be3f-d684-5574-7fd2-25992f3c46c5.png)

![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538249/f16d5e9d-e315-f897-ce2b-b81303ea4225.png)

## さいごに
作成したアプリのコード類は以下のGithubにて公開しております。
https://github.com/Oooooomin2/ContactForm

今後の展望としましては、自分が普段から使っているASP.NET Core MVCにReactやMaterial UIを組み込んでアプリを作ってみたいです（いつもフロントはjQueryとBootstrapなので）。Lambdaを使ったサーバレスアーキテクチャもだいぶイメージがつきましたので引き続き色々なものを作ったりして知識を定着させていこうと思います。

## おまけ
今回、お問合せフォームをS3に配置するにあたってNext.jsの機能であるStatic HTML ExportにてHtmlを作成して配置しました。
その際に対象のエンドポイントにアクセスしても毎回403エラーが出力されたんですよね...。

解決策としまして、S3のバケットのアクセス許可だけではなく、その中に配置したオブジェクト一つ一つをアクセス許可することで解消できました。盲点でした。次からはハマらないように気をつけます(;^_^A