Reactとスプレッドシート(GAS)を使って3日でウェブサイトを作る

# 概要
2020年4月から独立してフリーランスになったので、事業用のウェブサイトを3日で作成しました。まぁ正確には4日なんですが、4/6(月)は仕事でほとんど作業できなかったんで、実質3日です。嘘はついてません。4日？記憶にございません。

# というわけで、作ったサイト

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">事業用のサイト作っちゃった(`・ω・´)<br>React+TypeScript+GoogleAppsScript(スプレッドシート)を主に作って3日で作成しました(`・ω・´)<a href="https://t.co/dg4cuhqX5m">https://t.co/dg4cuhqX5m</a></p>&mdash; ⛩branch@個人アプリ開発者⛩ (@br_branch) <a href="https://twitter.com/br_branch/status/1247460190917513217?ref_src=twsrc%5Etfw">April 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

とりあえず、ネットに散らばってるぼくの情報をかき集めてくれるようなポートフォリオサイトを作りました。

## 使ってるサービス・フレームワーク
* React + TypeScript ：フロントエンド実装
* BULMA: CSSフレームワーク
* Google Apps Script (Google スプレッドシート)： DB兼APIサーバーとして
* Firebase Cloud Functions: メッセージ送信用のAPIサーバーとして
* reCAPTCHA v3: なんとなく
* Slack Incoming webhook: メールの代わりに
* Qiita feed / Hatena RSS feed: ブログとってくるよ

アーキテクチャはこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/1eee135a-de61-24cc-fee3-f55c4b5e7003.png)

今のぼくのお仕事状況や、個人的な成果物などはGoogle Spread Sheetで管理して、こんな感じに取得させて表示しています。データの管理画面を作らなくっていいから楽ちん！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/0ad60b5a-1c71-ffba-7bce-4d176c2c686f.png)

その他の情報も基本的には外のサービスから取得をしてるんで、Webサイトのデプロイは基本しなくてもコンテンツが充実していくようにしてます。メールはぼくがあんまり利用しないんで、ContactでもらったものはSlackに届くようにしています。

GASは本当はサーバーじゃないんで当然普通のサーバーより遅い（といっても測ってないけど）んですけど、個人のポートフォリオ用のウェブサイトに来る人なんて全然いないと思うのできっと無問題。
（本当はこのPost部分もGASにやらせたかったけど、CORS対策されてて無理だった…)

# 今週中に作る必要があった
なんでGAS使おうと思ったかというと、今週中にウェブサイトを作っておきたかったんですよね。

独立した直後に事業用の名刺を作ったんですが、

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">あと事業用の名刺作ってきた(`･ω･´)<br>ホントは全然別の屋号を考えてたんだけど酔っ払いながら税務署の書類作っちゃったから屋号はBRBranchです(´･_･`) <a href="https://t.co/8yIbDgKPqY">pic.twitter.com/8yIbDgKPqY</a></p>&mdash; ⛩branch@個人アプリ開発者⛩ (@br_branch) <a href="https://twitter.com/br_branch/status/1245655069422305280?ref_src=twsrc%5Etfw">April 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

上記の隠してる箇所に、まだ作ってもいない(ドメインしかとってない)サイトのURLを載せてました。

「まぁいつか作ればいいし」

って当初はぬるく考えていたのですが、今週早速名刺を渡す用事ができちゃって、**これはまずい**と…。
ただウェブサイトはポートフォリオにもなりうるんで、外っ面だけでもちゃんと作っておかなきゃなぁと思い、バックエンドを極力実装せずにデータの更新などもできるようにしたいと考えた時、「まぁもうGASでいいや」ってなりました。

結果的にバックエンド側はreCAPTCHAの検証やSlackへの通知処理も含めて**1時間ちょいで実装し終わった**んで、かなり高速にやりたい機能作れたなと思ってます。
(フロント、というよりCSSが苦手なので、それに一番時間かけてた…）

# もうちょい詳しく紹介
そんなわけで、作った中でいくつか機能をピックアップして紹介します。

## React + TypeScript
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/68fa6641-3eed-0f4b-9afc-c71ecef93500.png)

フロント側はこれで作りました。
Reactは色々な書き方があるっぽいのですが、ぼくは`FunctionComponent` をメインに使って書きました。 `useState` とか色々使えて楽ちん。

たとえば、ブログの一覧を出すコンポーネントは以下みたいな感じです。

```typescript
const initialState: any = null;
export const Blogs : React.FC<{}> = () => {
    // stateとそのsetterを作れる
    const [blogs, setBlogs] = useState<any>(initialState);

    // blogsがinitialStateの時だけ発動する
    // componentDidMountと同じような1度だけの処理ができる
    useEffect(() => {
        receiveData();
    }, [initialState]);

    function receiveData() {
        // データ取得処理
        $.ajax("url", { type: "get" })
         .done(data => { 
            // 取得した内容をblogsに設定する
            // stateが変更されたらReactは描画しなおす
            setBlogs(data);
         });
    }

    function showBlogs() {
        return blogs.map((e, i) => {
           // blogのデータひとつひとつでReactElementを作成
        );
    }

    // 描画時に呼ばれる最終的なReactElement
    return (
        <div className={`container works-root`}>
            <h1 className={"title"}>Blog</h1>
            {showBlogs()}
        </div>
    );
};
```
本当はjQueryは使わずにFetchAPI使おうと思ったんですが、IE対応されてないんでやめました。
一応はポートレートのサイトだし、IE使ってらっしゃる方が見たりするかもしれないですし。
といっても、IEだとうちのサイト見れないです。以下をやらないと駄目みたい。

React.jsがIE11で動かない問題を解決する
https://qiita.com/t-motoki/items/6d8476c93b49dc2582c4

ありがとうございました。

## BULMA
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/a5a913c8-33ed-9d5a-e11d-872f9d5921e8.png)
https://bulma.io/

CSSフレームワークはBULMAを使いました。
ぼくは言語の中ではCSSが一番苦手で、なんで苦手かというと**エラー吐いてくれない**し**意図しない表示になった時にどこで影響受けてるのかすぐわからない**しで、だからなんかアレなんです(´・ω・`)

特にReactのCSSフレームワークを使うと、中で色んなクラスが知らず知らずのうちに呼ばれたりしてるんで、**なんか思てたんとちゃう**ってなった時、カスタマイズがすごい面倒だし、結構たくさんコンポーネントがあって覚えること多くなるしで、なるべくシンプルなものを探してたどり着きました。
(といっても、BULMAはReactのフレームワークじゃないんですが)

このフレームワークのいいところは、**クラスの命名がとてもシンプルでわかりやすく、かつCSSオンリーな部分＆カスタマイズしやすい部分**ですね。コンポーネントも必要最低限のものだけがある感じなのもいい！

たとえば影付きのパネルを作りたければ以下で実装できます。

```html
<div className="panel">
   <div class="panel-block">
      パネルになるよー
   </div>
</div>
```

また、Scssで作成されており、変数が定義されてるんでカスタマイズもやりやすいです。

もちろん [シンプルな故の逆説的弊害](https://papadays.com/post/5inybk0imz6r2ycmeoquwn/) もあるようですが、3日程度で作れる規模のものを作るなら全然無問題でした。

## Google Apps Script (Spread Sheet)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/e3745712-2b9f-a4b2-ae77-8e6cf350fafe.png)

Googleで無料で使えるスプレッドシートですが、Google Apps Scriptを使うと簡易的なサーバーレスAPIとしても利用できます。データ入力や更新はそのままスプレッドシートでできちゃうので、管理画面を作る手間なども省けます。
また、最近GASはv8になってかなり使いやすくなりました。

設定など詳しいものは以下に記載されてますので、割愛。

今から10分ではじめる Google Apps Script(GAS) で Web API公開
https://qiita.com/riversun/items/c924cfe70e16ee3fe3ba

そして、お仕事状況を返す実装は以下の感じ。

```javascript
function doGet(e) {
  const spread = SpreadsheetApp.getActive();
  const curent = spread.getSheetByName("SheetName");
  
  let result = {};
  result.body = curent.getRange("B1").getValue();
  
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({status: 200, data: result}));
  return output;
}

```

簡単！(`・ω・´) あとはフロント側からAjax使って取得すれば簡単に動的なページが作れちゃいます。

### ただ、AjaxでPostはできないっぽい？
本当はGASでPostしてごにょごにょしようと思ってたんですが、以下のようなエラーが発生してできませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/cc4ef85b-2954-4e58-9da0-2232fb2df719.png)

調査に時間あんまりかけたくなかったんでちゃんと調べてないので、本当はできるのかもしれませんが(´・ω・`)
とはいえGetは普通にできるんで、データの更新や取得はSpread Sheetですることにしました。

## QiitaとHatenaのRSSフィード
ブログの取得は、QiitaとHatenaそれぞれのRSSフィードを使ってAjaxで取得をしています。

HatenaのRSSフィードは、ブログのURLに`/rss` をつけると簡単にできちゃいます。

https://brbranch.hatenablog.jp/rss

Qiitaも同様で、マイページに `/feed.atom` をつけることでできます。

https://qiita.com/br_branch/feed.atom

ただ、QiitaはCORSの許可がされてないので、そのままだとAjaxで取得できません。そのため、`CORS Proxy`を利用します。

CORS Anyware
https://cors-anywhere.herokuapp.com/

具体的には、上記の後にQiitaのRSSフィードのURLを付け加えることで、Ajaxでの取得が可能になります。

```
// AjaxのURLに以下を指定する
https://cors-anywhere.herokuapp.com/https://qiita.com/br_branch/feed.atom
```

あとは、XMLDocumentとして取得ができるのでそのまま `getElementsByTagName` などを使ってコンテンツを読み込むだけで自分のウェブサイトに自分の書いた記事を埋め込むことができます。

## reCAPTCHA + CloudFunctions + Slack Incoming Webhooks
メッセージ送信部分は CloudFunctionsとSlack Incoming Webhooksを使って実現してます。あと、なんとなくスパム対策としてreCAPTCHA v3も使ってます。

こんな感じで、メッセージを贈ろうとした際に可愛く表示されます。v3の場合、あの鬱陶しい「信号を探せ」みたいなものも表示されず、利用者が何も操作しなくてもスパム対策をしてくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/1d7cf652-3ed7-4fae-c881-b0976dab9f63.png)

reCAPTCHAは以下から無料で簡単に導入できます。
https://www.google.com/recaptcha/intro/v3.html

導入記事：

reCAPTCHAは、クライアント側で払い出されたTokenを、サーバー側で検証することでスパム対策ができます。クライアント側はReactならコンポーネントライブラリがあるし、サーバー側の検証も、単にそのトークンをエンドポイントに投げるだけです。

```javascript
import * as Request from 'request'
// 省略
Request.get("https://recaptcha.google.com/recaptcha/api/siteverify?secret=<シークレットキー>&response=" + request.body.token, (error, resp, body) => {
   if (error) {
       // エラー処理
   }
   const responseBody = JSON.parse(body);
   if (response.success) {
       // 検証OK
   }
   // 不正なトークン
});
```

また、Cloud Functionsもめちゃ楽にデプロイができます。

Firebase で Cloud Functions を簡単にはじめよう
https://qiita.com/tdkn/items/2ed2b01f2656fc50da8c

ただ、ここで注意しないといけないのは、Firebase Cloud Functions のSpark プランを使ってる場合、reCAPTCHAのエンドポイントのドメインは `recaptcha.google.com` でないといけません。 `www.google.com` を使うと、 `EAI_AGAIN` というエラーが発生しちゃいます。

* https://www.google.com/recaptcha/api/siteverify ×
* https://recaptcha.google.com/recaptcha/api/siteverify ○

とはいえ、結局 Firebase Spark プランではぼくがやりたいことできなかったんですけどね(´・ω・`)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/563c1753-31aa-08cd-1613-3bb01e847181.png)

Googleサービス専用なんで、Slackに送れないじゃないやだー。。。
さすがGoogle、マネタイズの方法わかってらっしゃる。。。

ちなみにSparkプランだとSlackに送ろうとする時にやはり `EAI_AGAIN` が発生します。
まぁ、 Blaze プランも無料枠あるんで、アップデートして使うことにしました。

あと上に書き忘れてたけど、フロント側は Firebase Hostingに乗せてます。これもすんごいデプロイ簡単。

Firebase Hosting でWebサイトを公開する方法
https://qiita.com/gupuru/items/25a6722f6f802d3a5250

### Slack Incoming Webhooks
これは、外部からSlackの特定のチャンネルに投稿するためのSlack Appです。

Slack での Incoming Webhook の利用
https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8

Slack AppからWebhooksを使うように設定するだけで、Postリクエストで送りたいメッセージを送れます。今回作ったウェブサイトではreCAPTCHAの認証通ったらリクエストを投げるようにしています。

# 補足：今回使ったReactライブラリ
まぁ、バックエンド側はさておき、今回はフロント側そこそこ頑張ったので、その際に使った便利なライブラリをいくつか紹介して終わりにしたいと思います。

## react-twitter-embed: Twitter埋め込み
https://www.npmjs.com/package/react-twitter-embed

Twitterのタイムライン埋め込みをReactで簡単にできるライブラリです。
ねぇ奥様、こんな感じでかけちゃうんですのよ。

```react
 <TwitterTimelineEmbed sourceType="profile" screenName="br_branch"/>
```

## react-google-recaptcha-v3: reCAPCHAライブラリ
https://www.npmjs.com/package/react-google-recaptcha-v3

reCAPCHAv3のトークンを払い出すためのライブラリです。
これもこんな簡単。

```react
<GoogleReCaptchaProvider reCaptchaKey="<クライアントキー>">
   <GoogleReCaptcha onVerify={(token)=> { /* トークンもらえる */ }} />
</GoogleReCaptchaProvider>
```

## react-loading-skeleton: Skeleton作成
https://www.npmjs.com/package/react-loading-skeleton

ねぇ奥様、 `react-loading-skeleton` があればロード中のスケルトンだって簡単につくれちゃうんですの。

![Apr-07-2020 21-43-27.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/ff32fc42-2dbd-00fe-d700-0db41bd92dda.gif)

# 最後に
そんな感じで、小規模のサイトなら無料で簡単につくれるので良い時代になりましたねぇ。