新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜

### まえがき
先週末、仕事で東京から岡山まで行く機会があり、新幹線の中で一人でハッカソンしてみたら、意外と０から作ってサービスを公開するところぐらいまではできました。当日の経過のログが誰かの助けになればと思い、投稿。[TsuyoshiNumano/emojishare](https://github.com/TsuyoshiNumano/emojishare) にコードは公開してます&要所要所で [Pull Request](https://github.com/TsuyoshiNumano/emojishare/pulls?q=is%3Apr+is%3Aclosed) を分けました。　サービスづくりの雰囲気だけでも感じ取ってもらえれば幸いです。
サービス自体は流行らないと思うので、そのうち消すと思います（ドメイン代とかもかかるし汗）

当日の [twitter 実況](https://twitter.com/i/moments/1066247553388228608)。

### 対象読者
- さくっと web サービスを作って公開してみたい方
- 割とフロントエンドよりの技術スタックなので、そのへんに興味ある方

 
*細かい技術的なところはお話しません。

### 使った技術スタック

- [create-react-app](https://github.com/facebook/create-react-app)
React でサクッとアプリ作るならオススメ

- [Google Apps Script](https://developers.google.com/apps-script/) & [Google Spread Sheet](https://www.google.com/intl/ja_jp/sheets/about/)
貧者のためのデータベースとスクリプト。これなしでは生きていけない。

- [Firebase Hosting](https://firebase.google.com/docs/hosting/?hl=ja)
create-react-app との相性も良い。Google Domain でドメインを取って SPA をホスティングするのにちょうどよい。

- [material-ui](https://material-ui.com)
かんたんにそれっぽい ui 作るならこれ。

- [Google Domains beta](https://domains.google/)
firebase とかで設定するのが楽



### 作ったもの
https://emojishare.app

Slack 絵文字の共有サービス|
---|
![reacord10.gif](https://qiita-image-store.s3.amazonaws.com/0/26095/ac947df2-36ba-0fd2-7f05-8dd2e29d1a10.gif)|

#### モチベーション

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">週末フリーランスでいろんな会社の slack に入ってるんだけど、会社独自の絵文字とか面白いのがあるよね！<br><br>そういうのをシェアする場がほしいなと思って、サービス作ってみました。<a href="https://t.co/wRL02bk5QB">https://t.co/wRL02bk5QB</a><br><br>ぜひ、御社の絵文字登録して slack 絵文字界隈を盛り上げましょう！<br><br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064102180653719553?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>



### さっそく制作過程
#### 1. 製作開始
品川駅からスタート
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">新幹線で品川出発した。<br>最近新幹線内プログラミングが自分の中でブームなので、なんかwebサービス開発しよかな。<br>アイデアは何個かあるのでどれかやる。<a href="https://twitter.com/hashtag/%E7%A4%BE%E5%93%A1%E3%81%95%E3%81%99%E3%82%89%E3%81%84%E3%83%AF%E3%83%BC%E3%82%AF?src=hash&amp;ref_src=twsrc%5Etfw">#社員さすらいワーク</a> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064042232414498817?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>

#### 2. リポジトリを作成
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Slack の絵文字を共有するサービスをつくることにした。<br>とりあえず、このオープンなリポジトリにコミットしていく。<a href="https://t.co/8TmeT8tCiv">https://t.co/8TmeT8tCiv</a><br>岡山駅までいくのであと２時間ちょいぐらいあるかな〜。できるかなー。<br><br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064046261727223808?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>


#### 3. おもむろに [create-react-app](https://github.com/facebook/create-react-app)
https://github.com/TsuyoshiNumano/emojishare/pull/1

```bash
yarn create react-app fron-react
```
#### 4. List を表示するベースを作る


`fron-react/src/App.js` を編集



```jsx:App.js

import React, { Component } from 'react';

class App extends Component {
  constructor() {
    super();
    this.state = {
      emojiList: [
        {
          url:
            'https://emoji.slack-edge.com/T02HHLFPR/lgtm/393f650bcf7d2b0a.png',
          likeCount: 0
        }
      ]
    };
  }

  render() {
    return (
      <div>
        <div>emojishare</div>
        <div>
          {this.state.emojiList.map((emoji, i) => (
            <img alt={emoji.url} key={'emoji' + i} src={emoji.url} width="20px" />
          ))}
        </div>
      </div>
    );
  }
}

export default App;

```

結果|
---|
![image.png](https://qiita-image-store.s3.amazonaws.com/0/26095/ca44f68f-fb3e-6d49-3d25-9d35bcf0f065.png)|

https://github.com/TsuyoshiNumano/emojishare/pull/2

このへんで富士山が見えた。

#### 5. データを入力して List に追加する
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">とりあえず入力して、リストに追加するまで作った。<a href="https://t.co/LStF9kj4Ts">https://t.co/LStF9kj4Ts</a><br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a> <a href="https://t.co/ehtvFU9JR8">pic.twitter.com/ehtvFU9JR8</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064053473384091648?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>


```jsx:App.js
...
    this.state = {
      inputValue: ''
    };
  }

  hadleChange = e => {
    this.setState({ inputValue: e.target.value });
  };

  handleSubmit = e => {
    this.setState({
      emojiList: [
        ...this.state.emojiList,
        { url: this.state.inputValue, likeCount: 0 }
      ]
    });
    this.setState({ inputValue: '' });
  };

  render() {
    return (
      <div>
        <div>emojishare</div>
        <input
          type="text"
          placeholder="Please add your Emoji's url"
          value={this.state.inputValue}
          onChange={this.hadleChange}
        />
        <button onClick={this.handleSubmit}>add</button>
        <div>
          {this.state.emojiList.map((emoji, i) => (
            <img alt={emoji.url} key={'emoji' + i} src={emoji.url} width="20px" />
          ))}
        </div>
      </div>
    );
...
```
https://github.com/TsuyoshiNumano/emojishare/pull/3/commits/5330e4334d5ff9e459789485c4555d76910addb1

#### 6. GASで簡易 API サーバーを作る
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">DB は時間もないので Google Spreadsheet ＆ GAS  の構成で。<a href="https://t.co/wrtQmueY4P">https://t.co/wrtQmueY4P</a><br><br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064058905301417985?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>

WEB API としての公開などはこちらを参考に: [3分で API を作って世の中にデプロイするライブコーディング〜今日から君もスピードスターエンジニア〜](https://qiita.com/numanomanu/items/7bb6542961a2c567f05c)

```js:api_server.gs
function doGet(e) {
  var response = {
    data: [],
    meta: { status: 'success' }
  };

  var sheet = SpreadsheetApp.getActiveSheet();
  var sheetData = sheet.getRange('A2:B' + sheet.getLastRow()).getValues();

  var responseList = [];

  sheetData.map(function(d) {
    responseList.push({ url: d[0], likeCount: d[1] });
  });

  response.data = responseList;

  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(
    ContentService.MimeType.JSON
  );
}
```
https://github.com/TsuyoshiNumano/emojishare/pull/3/commits/05d6deed7a2ad7887c28eda66368f5ff5dc120e5

DB|
---|
![image.png](https://qiita-image-store.s3.amazonaws.com/0/26095/d5572c75-34c8-e786-bb91-43679f8cd331.png)|

#### 7. 簡易 API サーバーからのデータの取得
[axios](https://github.com/axios/axios) をつかって GAS の　API にフロント側からリクエストを行う

```bash
yarn add axios
```

```jsx:App.js
import axios from 'axios';

...

  componentDidMount() {
    axios
      .get(
        'https://script.google.com/macros/s/AKfycbyw9qQBpO7rm89iFWFjaslKcEqm4C72Z2smPh0rtcC68hMVGXI/exec'
      )
      .then(response => {
        console.log(response);
        this.setState({
          emojiList: response.data.data
        });
      });
  }

...
```
https://github.com/TsuyoshiNumano/emojishare/pull/3/commits/cd666ec40efee6a02e5c9e3c85c4a773a98ba4c8

#### 8. API にデータを送り、フロントと連動するようにする
- 入力したデータを API サーバーに送りつけて
- スプレッドシートで作った DB に追加

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">入力を GAS のデータベースと連携することまではきた。<br>もうすぐ名古屋。<a href="https://t.co/n9PNemZAdk">https://t.co/n9PNemZAdk</a><br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a> <a href="https://t.co/2QfYblmv9B">pic.twitter.com/2QfYblmv9B</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064062014425718784?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>


```diff:api_server.gs
-  var sheetData = sheet.getRange('A2:B' + sheet.getLastRow()).getValues();
-
-  var responseList = [];

-  sheetData.map(function(d) {
-    responseList.push({ url: d[0], likeCount: d[1] });
-  });
+  if (e.parameter.url) {
+    sheet.appendRow([e.parameter.url, 0]);
+  }

+  var responseList = getData(sheet);
   response.data = responseList;

   return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(
     ContentService.MimeType.JSON
   );
 }
+
+function getData(sheet) {
+  var sheetData = sheet.getRange('A2:B' + sheet.getLastRow()).getValues();
+
+  var responseList = [];
+
+  sheetData.map(function(d) {
+    responseList.push({ url: d[0], likeCount: d[1] });
+  });
+  return responseList;
+}
```

```diff:App.js
   handleSubmit = e => {
-    this.setState({
-      emojiList: [
-        ...this.state.emojiList,
-        { url: this.state.inputValue, likeCount: 0 }
-      ]
-    });
+    if (!this.state.inputValue) {
+      return;
+    }
+
+    axios
+      .get(
+        'https://script.google.com/macros/s/AKfycbyw9qQBpO7rm89iFWFjaslKcEqm4C72Z2smPh0rtcC68hMVGXI/exec?url
+          encodeURI(this.state.inputValue)
+      )
+      .then(response => {
+        this.setState({
+          emojiList: response.data.data
+        });
+      });
     this.setState({ inputValue: '' });
   };
```
https://github.com/TsuyoshiNumano/emojishare/pull/4

ここで、　axios.post を使っていないのは CORS 問題でブラウザから　GAS へは GET しかできないからです。
GETのクエリパラメータに無理やり送りたいデータをつけて送ってます。

名古屋あたり。

#### 9. [material-ui](https://material-ui.com/) を入れて ui をリッチにする

![48670185-840d9e80-eb56-11e8-9f2e-db6367a79a26.gif](https://qiita-image-store.s3.amazonaws.com/0/26095/3a2086bc-a7da-5602-9476-65680082b108.gif)


```bash
yarn add @material-ui/core
```

```jsx:App.js
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';

...

        <div className="button-and-text">
          <TextField
            className="text-field"
            type="text"
            label="Slack Emoji url"
            placeholder="Please add your Emoji's url"
            value={this.state.inputValue}
            onChange={this.hadleChange}
            variant="outlined"
          />
          <Button
            onClick={this.handleSubmit}
            variant="outlined"
            color="primary"
          >
            add
          </Button>
          <div>{this.state.loadingFlag && <CircularProgress />}</div>
        </div>

...
```

https://github.com/TsuyoshiNumano/emojishare/pull/5/

#### 10. 簡単なバリデーションを入れる。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">なんだかんだサービス作るのって、単純なアプリケーションのコーディングよりも、バリデーションとかセキュリティとかでめっちゃ時間使う気がする。<br><br>京都についた。もうすぐ新大阪。岡山までに公開できるのか！？<a href="https://t.co/a86MHudhR8">https://t.co/a86MHudhR8</a><br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a> <a href="https://t.co/7YvzevnyDr">pic.twitter.com/7YvzevnyDr</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064075970578067456?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>

https://github.com/TsuyoshiNumano/emojishare/pull/6

京都に着。

#### 11. Firebase にデプロイ

この辺を参考に [Firebase Hosting でWebサイトを公開する方法](https://qiita.com/gupuru/items/25a6722f6f802d3a5250)


```bash
npm install -g firebase-tools
firebase init
```
ホスティングを選択して、deploy するフォルダを　`build`　にする。←地味に大事。

```bash
~/work/emojishare/front-react firebase init                 (git)-[feat/emojilist]

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /work/emojishare/front-react

? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to conf
irm your choices. Hosting: Configure and deploy Firebase Hosting sites

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add,
but for now we'll just set up a default project.

? Select a default Firebase project for this directory: [create a new project]

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? build
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? File build/index.html already exists. Overwrite? No
i  Skipping write of build/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

✔  Firebase initialization complete!

Project
```

ここまで来たらビルドしてデプロイする

```bash
yarn build
firebase deploy
```

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">firebase で deploy するとこまで来た。。。<br>もう結構疲れた。<br>新神戸。<br>なんとか品川から岡山までには WEB サービスを作ってデプロイできた。<a href="https://t.co/cUJCvoEgT2">https://t.co/cUJCvoEgT2</a><br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064080910096621568?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>

https://github.com/TsuyoshiNumano/emojishare/pull/7

#### 12. OGP を設定する
ogp 作るならこのサイトがオススメ
https://pablo.buffer.com

![image.png](https://qiita-image-store.s3.amazonaws.com/0/26095/f6bd5c74-29b5-2845-d554-ec3da2310849.png)

https://github.com/TsuyoshiNumano/emojishare/pull/8


#### 13. Google Domain でドメインを取得する

こちらの記事がとてもわかり易いので割愛！ 
[.appドメインはFirebase hostingとの相性が抜群](https://qiita.com/fromkk/items/c2361747733125ca36d0)

#### 14. アイデアをもらったランキングシステムを実装
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">簡単にインポートできて、ランキングあれば使うサービス！</p>&mdash; たばてぃしゃちょー (@gahara_p) <a href="https://twitter.com/gahara_p/status/1064055262523183104?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>

細かいので詳細は以下
https://github.com/TsuyoshiNumano/emojishare/pull/9


#### 15. 完成
https://emojishare.app

ゴール!
最後ちょっと延長戦しましたが、なんとか品川から岡山までで、 API 通信するようなサービスを作って OGP を設定しドメインを取って公開するところまで行きました。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">岡山ついたけど、延長戦で駅構内で開発。<br>駅構内では新幹線到着の音が心臓にめちゃくちゃ悪い笑<br> <a href="https://twitter.com/hashtag/%E6%96%B0%E5%B9%B9%E7%B7%9A%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#新幹線ハッカソン</a></p>&mdash; numa🏅さすらいのライターエンジニア@Lancers, inc. (@numanomanu) <a href="https://twitter.com/numanomanu/status/1064100602576199682?ref_src=twsrc%5Etfw">2018年11月18日</a></blockquote>

### まとめ

- 割とフロントエンド技術だけでウェブサービスを作ってさっさと公開できる。
- 新幹線の移動時間でも簡単な WEB アプリは思ったより簡単に作れる！
- ぱっと作るのは簡単だけど、このあとセキュリティとか、バリデーションとか、パフォーマンスを考え出すと、運用は色々工夫が必要。

今回のは react でつくりましたが、別に vue.js でも jQuery でもできると思います。(単に自分が最近この構成に慣れてるだけ)

みなさんも、是非オススメのプロトタイピングな開発スタックを教えてください。

### Tips
- 新幹線は窓側に電源ある。
- できれば進行方向最前列だとちょっとスペースが広い。
- この記事はだいたい帰りの新幹線で。
- 東京から関西方面だと、神戸ぐらいにいくなら実は西明石までの往復きっぷの方が安い