【100行で出来る】在宅でもブレストがしたいので、オンラインホワイトボード（付箋アプリ）を作ろう stayhome-board

エンジニアとしては最近の在宅勤務ブームは割とハッピーですが、ブレストはやっぱりやりにくいなぁという感想です。とはいえ、出社してしまうと感染リスクもあるので、ブレストもオンラインで出来た方が人類のためになりそうだったので、作ってみました。（色々オンラインコラボレーションツールはありますが、モダンなフロント技術でサクッと作れないかなぁと思って試したところ、思った以上にオンライン付箋アプリを作るのは簡単だったので、作り方をシェアできればと思って書いてます。本当のオンラインホワイトボードにするには、もうちょっと修行が必要そうでした・・・）

## 追記 2020/04/30
- 思ったより記事が伸びたので、firebase だけでなく socket-io での作り方も用意してみました。社内でホスティングしたい場合などはそちらのバージョンを参考にしてください。 [EXTRA STEP] に詳細は記述しています。

## ぬるぬる動く、成果物
- デモ： https://stayhome-board.firebaseapp.com/demo (デモページは予告なく消すかもしれません。)
- コード： https://github.com/TsuyoshiNumano/stayhome-board 

![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/45b8c294-38ea-27ba-0dcd-0d7671793dc1.gif)

できること

- 付箋の追加、削除、テキストの編集、＊カードの色変更（＊作り方には含めていません）
- Drag and Drop で付箋の移動
- **リアルタイムに複数人で編集**
- `/` 以降の url 変更で新しいボードを作れる

技術スタック

- React
- Firebase Realtime Database

firebase の realtime database を設定し、 git clone して、 .env に firebase の設定を書き換えるとローカルでもすぐに動きます。

# 作り方を練習用リポジトリを見ながら解説
Demo は少しリッチに作っていますが、ビルド環境などを用意するのが面倒な私のために、index.html １枚 & 100行程度に収まる練習用コードを書いてみました。
React がわからなくても、特に難しい機能は使っていないので Vue や Angular に読み替えて（できれば作り変えたりして）もらうと、良いと思います。

github に練習用のコードも全部置いてます。１機能づつプルリクエスクとを作ってるので、順に追っていくとわかりやすいと思います。

1. [STEP1 React のビルドレス構成](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/1)
2. [STEP2 最低限の Drag and Drop の実装](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/2)
3. [STEP3 最低限の Text 編集の実装](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/3)
4. [STEP4 複数のカードへの対応](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/4)
5. [STEP5 リアルタイムにするための設定](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/5)
6. [EXTRA STEP firebase ではなく socket-io を利用する](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/6) 


[練習用コードの最終形態](https://github.com/TsuyoshiNumano/stayhome-board-minimum/blob/master/index.html)


## STEP1: ミニマムな React 環境の準備(index.html だけ)

https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/1|
---|
<img width="821" alt="step1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/48f0e5e7-a4e5-b422-02a4-37adf1caa507.png">|


```html:コピペで使えるReact最小構成

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>StayHomeBoard</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
    <script type="text/babel">
      const Board = () => {
        return <div>Wash your hands clean!</div>;
      };
      ReactDOM.render(<Board />, document.getElementById("root"));
    </script>
  </head>
  <body>
    <div id="root" />
  </body>
</html>
```

ポイント

1. React を CDN から読み込む
2. babel を CDN から読み込んで `<script type="text/babel">` にすることで、 `<script>` タグ内で JSX が使える


## STEP2: Drag on Drop で位置を変更する

https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/2|
---|
![step2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/8d315619-69bc-054e-ab79-66f4d8730926.gif)|


```jsx:DragOnDropのコード

const Board = () => {
  return <div>Wash your hands clean!</div>;
  const [pos, setPos] = React.useState({ x: 0, y: 0 });
  return (
    <div
      style={{ width: "1000px", height: "1000px", position: "relative" }}
      onDrop={(e) => setPos({ x: e.clientX, y: e.clientY })}
      onDragOver={(e) => e.preventDefault()} // enable onDrop event
    >
      <div style={{ position: "absolute", top: pos.y + "px", left: pos.x + "px" }} draggable={true}>
        Wash your hands clean!
      </div>
    </div>
  );
};
```
ポイント

1. 全体を `position: "relative"` のスタイルで囲む
2. 移動させたい要素を `position: "absolute"` にして `left`, `top` を絶対位置指定して、1の要素の中に入れる
3. Drag で移動させたい要素を `draggable={true}` にする
4. 1で作った `div` の `onDrop` のイベントで、 Drop されたときのポジションが取れるので state に入れて利用する
5. `onDragOver={(e) => e.preventDefault()}` しないと、 `onDrop` イベントが発火しないので注意


## STEP3: text の中身を編集

https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/3|
---|
![step3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/0e28419d-2292-170b-4faa-021ab529a894.gif)|

```diff:textを編集できるようにする
+    const [text, setText] = React.useState("Wash your hands clean!");
+    const [editMode, setEditMode] = React.useState(false);

     <div style={{ position: "absolute", top: pos.y + "px", left: pos.x + "px" }} draggable={true}>
-      Wash your hands clean!
+      {editMode ? (
+         <textarea
+           onBlur={(e) => setEditMode(false)}
+           onChange={(e) => setText(e.target.value)}
+           defaultValue={text}
+         />
+      ) : (
+        <div onClick={(e) => setEditMode(true)}>{text}</div>
+      )}
    </div>
```

ポイント
1. 文字をクリックしたときに編集モードにする
2. `<textara>` の `defaultValue` に state を入れておくと、スムーズに編集できる
3. `onBlur` のイベントでマウスが別の場所をクリックしたときに、text を確定する


## STEP4: 複数枚のカードを編集したりうごかせるようにする

https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/4|
---|
![step4-3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/a5e7630a-013b-77a7-6374-863b149974b0.gif)|

少し多いので順番に
4-1 テキストを編集できるようにする

```diff
const Board = () => {
+  const [cards, setCards] = React.useState({
+    id1: { t: "Wash", x: 100, y: 100 },
+    id2: { t: "your hands", x: 200, y: 300 },
+  });
+  const update = (key, card) => setCards({ ...cards, [key]: card });
+
   const [pos, setPos] = React.useState({ x: 0, y: 0 });
-  const [text, setText] = React.useState("Wash your hands clean!");
-  const [editMode, setEditMode] = React.useState(false);
+  const [editMode, setEditMode] = React.useState({ key: "" });

return (
  <div
@@ -20,17 +25,23 @@
  onDrop={(e) => setPos({ x: e.clientX, y: e.clientY })}
  onDragOver={(e) => e.preventDefault()} // enable onDrop event
  >
-  <div style={{ position: "absolute", top: pos.y + "px", left: pos.x + "px" }} draggable={true}>
-    {editMode ? (
-      <textarea
-        onBlur={(e) => setEditMode(false)}
-        onChange={(e) => setText(e.target.value)}
-        defaultValue={text}
-      />
-    ) : (
-      <div onClick={(e) => setEditMode(true)}>{text}</div>
-    )}
-  </div>
+  {Object.keys(cards).map((key) => (
+    <div
+      key={key}
+      style={{ position: "absolute", top: cards[key].y + "px", left: cards[key].x + "px" }}
+      draggable={true}
+    >
+      {editMode.key === key ? (
+        <textarea
+          onBlur={(e) => setEditMode({ key: "" })}
+          onChange={(e) => update(key, { ...cards[key], t: e.target.value })}
+          defaultValue={cards[key].t}
+        />
+      ) : (
+        <div onClick={(e) => setEditMode({ key })}>{cards[key].t}</div>
+      )}
+    </div>
+  ))}
</div>
```
[差分を github で見る](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/4/commits/019c3fa99a6456f6f33bd6c723f6de044a71270a)


1. cards という state を作成し、テキストの内容とポジションをそれぞれ持たせる。 **配列ではなく object** にすることで update しやすくする
2. 今までは `editMode` をフラグ管理していたが、「どのカードを編集するか？」を明示出来るように、 cards の key で管理する
3. `Object.keys(cards).map(key => cards[key])` で、オブジェクトを配列のように回して、 editMode にセットされている key と一致したときに、テキストを編集できるようにする
4. 編集は `const update = (key, card) => setCards({ ...cards, [key]: card });` のようにして、 特定の key を指定して update する


4-2 Drag and Drop も、複数対応する

```diff
-const [pos, setPos] = React.useState({ x: 0, y: 0 });
+const [dragging, setDragging] = React.useState({ key: "", x: 0, y: 0 });

 return (
   <div
     style={{ width: "1000px", height: "1000px", position: "relative" }}
-    onDrop={(e) => setPos({ x: e.clientX, y: e.clientY })}
+    onDrop={(e) => {
+      if (!dragging || !cards) return;
+      update(dragging.key, { ...cards[dragging.key], x: e.clientX - dragging.x, y: e.clientY - dragging.y });
+    }}
     onDragOver={(e) => e.preventDefault()} // enable onDrop event
   >
     {Object.keys(cards).map((key) => (
       <div
        key={key}
        style={{ position: "absolute", top: cards[key].y + "px", left: cards[key].x + "px" }}
        draggable={true}
+       onDragStart={(e) => setDragging({ key, x: e.clientX - cards[key].x, y: e.clientY - cards[key].y })}
    >
```
[差分を github で見る](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/4/commits/17456e152ade4a652562a80707ce257f3d43f7ba)

1. `draggable={true}` の要素に対して `onDragStart` のイベントを取得し、どの key のカードを Drag 中かを state に保存 
2. このとき、要素のどの位置をクリックしたかも保存しているのは、Drop したときにオフセットを計算して、スムーズな配置を行うため




## STEP5: firebase でリアルタイムに送受信できるようにする

- https://console.firebase.google.com/ firebase にログインしてプロジェクトを作っていきます


画像|概要
---|---
<img width="239" alt="step5-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/775c3fda-09b3-e186-23a4-241372c8988c.png">|プロジェクトを追加
<img width="821" alt="step5-6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/eabd8758-5ba1-8a93-aa27-46374a93b049.png">|アナリティクスなどを設定したら、プロジェクトができる
<img width="821" alt="step5-7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/6b981234-8734-922b-e650-43825570e2a5.png">|database を選んで
<img width="821" alt="step5-8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/2d59e412-f3ea-96db-2e3e-5b19267fab18.png">| CloudFirestore ではなく RealtimeDatabse を選択。 （単純な JSON しか扱わないのと、料金体系的にリアルタイムに多くの変更が発生するケースに向いているため）
<img width="821" alt="step5-9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/24299c7b-122c-5c93-2018-4c259d7f68ff.png">|セキュリティルールをテストモードにして
<img width="820" alt="step5-10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/68fb4c55-6f55-8580-8171-3bf97f061078.png">|データベースが作成されたら完了


WEBアプリの設定


画像|概要
---|---
<img width="820" alt="step5-11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/e22dda2d-c375-d4f3-69ce-4751aac65c8b.png">|ウェブを選択
<img width="820" alt="step5-12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/fbe8cee8-54d2-436b-5816-2e11ed1a7e50.png">|アプリ名を入力.(hosting も必要であれば。今回はサンプル公開のために設定した)
<img width="820" alt="step5-13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/83fbfc02-b077-449a-b53d-2f228381692d.png">|firebase の cli も hosting などする場合はインストールしておく
<img width="820" alt="step5-14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/bd533677-bf5a-5b71-a9ca-9ae8f9c10e0b.png">|作ったアプリを選択して
<img width="820" alt="step5-15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/8f1a7a41-a753-cc22-fffc-490ab591b67b.png">|Settings の下まで行って、CDN の内容を index.html にコピーする


RealTimeDatabase を使った同期処理を実装する

https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/5|
---|
![step5-16.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/d9e4b63e-a7ec-6804-3334-999df0a5d829.gif)|


5-1 realtime database の設定を入れる

```html
<script src="https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.14.2/firebase-database.js"></script>
...
<script type="text/babel">
  // ご自身のプロジェクトでコピーした key を入れてください
  var firebaseConfig = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: "",
  };
  const firebaseApp = firebase.initializeApp(firebaseConfig);
  const firebaseDb = firebaseApp.database();
  let db = null;

  const Board = () => {
...

```


5-2 リアルタイムに同期するように、add と update を書き変える

```diff
       let db = null;
       const Board = () => {
+        React.useEffect(() => {
+          db = firebaseDb.ref("/room_url");
+          db.on("value", (value) => setCards(value.val()));
+        }, []);
+
         const [cards, setCards] = React.useState(null);
-        const add = () =>
-          setCards({
-            ...cards,
-            [Math.random().toString(36).slice(-8)]: {
+        const add = () => {
+          const newPostKey = db.push().key;
+          db.update({
+            [newPostKey]: {
               t: "wash",
               x: Math.floor(Math.random() * (200 - 80) + 80),
               y: Math.floor(Math.random() * (200 - 80) + 80),
             },
           });
-        const update = (key, card) => setCards({ ...cards, [key]: card });
+        };
+        const update = (key, card) => db.update({ [key]: card });
+        const remove = (key) => db.child(key).remove();


+        <button onClick={() => remove(key)}>x</button>
      
```

[差分をgithubで見る](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/5/commits/8bf2376a0466b7402d47c0444a1b6cce3ab38656)


必要な変更はたったこれだけですが、これでリアルタイムに通信できるようになりました。

ポイント
1. useEffect で、コンポーネントが最初にレンダリングした時、`firebaseDb.ref("/room_url")` で RealtimeDatabase を読み込み `db.on("value", (value) => setCards(value.val()));` で中身を取得する。 JSON をリアルタイムに配信してくれる受け皿だと思うとわかりやすい。 `db.on` すると、オンラインに保存されている JSON に変更があるたび、第二引数の関数を実行するので、データが変更されるたびに、 state を上書きしていく。(現状は、丸ごと置き換えているので特定の kye の変更だけを監視するとか、もっと頭の良い方法はありそう。) 
2. カードを追加する `add` では `const newPostKey = db.push().key;` で先にキーを追加したあとに `db.update({[newPostKey]{ 中身 })` でアップデートすることで対応できる
3. カードの中身を変更する `update` は `db.update({ [key]: card });` のように、特定のキーを指定して中身を書き換えればOK。 STEP4-1 で、配列ではなくオブジェクトで保存していた効果が発揮された。
4. ついでに削除も `db.child(key).remove();` にて、 key を指定すれば良い

おまけ
[5-3 ボードを複数作りたいので、url パラメータを利用する](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/5/commits/d8b5a9ca5bc1e0e4308607eb737ea70f52a3270e)


## STEP6 デプロイ

- `npm install -g firebase-tools` しておく
- `firebase init` する

画像|概要
---|---
<img width="820" alt="step6-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/2c4bb27f-4970-c6f7-b1ab-03a98d796729.png">|Hosting をスペースキーで選んでからエンター
<img width="820" alt="step6-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/20f1e8a7-3d4a-1f8b-fe9f-3da8138cb1b7.png">|既存のプロジェクトを選択する
<img width="820" alt="step6-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/e67fa1eb-8ed8-645a-3829-f2398b595b25.png">|デプロイ元は `public` フォルダを選択
<img width="820" alt="step6-4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/ca25085c-e9cd-a75f-cbde-5373cb0ec662.png">|single page application も yes にしておく。(どの url にきても index.html を返してくれる)

してから `firebase deploy` する前に index.html を修正

```diff
-    <script src="https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js"></script>
-    <script src="https://www.gstatic.com/firebasejs/7.14.2/firebase-database.js"></script>
+    <script src="/__/firebase/7.14.2/firebase-app.js"></script>
+    <script src="/__/firebase/7.14.2/firebase-database.js"></script>
+    <script src="/__/firebase/init.js"></script>
     <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
     <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
     <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
     <script type="text/babel">
-      var firebaseConfig = {
-        apiKey: "",
-        authDomain: "",
-        databaseURL: "",
-        projectId: "",
-        storageBucket: "",
-        messagingSenderId: "",
-        appId: "",
-        measurementId: "",
-      };
-      const firebaseApp = firebase.initializeApp(firebaseConfig);
-      const firebaseDb = firebaseApp.database();
+      const firebaseDb = firebase.database();
```

ポイント

- firebase の hosting を利用する場合は、 cdn からではなく `/__/firebase/` から読み込まないと、エラーになるので注意
- `<script src="/__/firebase/init.js"></script>` すると `firebase` という変数に初期化したライブラリが読み込まれる

デプロイする

```bash
# public フォルダにコピーする
$ cp index.html public/
# デプロイコマンド
$ firebase deploy
```

URLにアクセスすると、アプリが動作するはずです。お疲れ様でした。
（ちなみに、この設定だとローカルで動かなくなるので、github に commit はしていません。）

![result_minimum.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26095/3c315699-bac3-36f9-fa38-660e26f2b4aa.gif)

カードの色を変えたり、スタイリングなどは、是非オリジナルなものを作ってみてください。
create-react-app と typescript で書いたバージョンは以下からお試しください。
https://github.com/TsuyoshiNumano/stayhome-board 





## 注意点

- ざっくりと流れを説明しただけなので、セキュリティのルールがガバガバです。実運用する際にはお気をつけください。
- 内輪で使うにしても、最低限 firebase で basic 認証 をかけると良いと思います。（参考）[Firebaseでベーシック認証をかけるベストな方法](https://note.com/rubymade/n/nafa8c82bd4cc)
- DB自体に制限をかけるのがベストです、認証システムを通して組み込んでください （参考）[firebase realtime database でよく使う rule](https://qiita.com/mironal/items/b93742e8ef72b3268beb) 
- [Firebase apiKey ってさらしていいの? ほんとに?](https://qiita.com/hoshymo/items/e9c14ed157200b36eaa5)

[追記]セキリュティ周りが心配な方に、社内やローカルにホスティングできる socket-io のバージョンも用意しました。以下の EXTARA STEP を参照ください

## EXTRA STEP (追記: 2020/04/30)
firebase を使いたくない人に向けて、 socket-io を利用したバージョンも作りました。ローカルでホスティングできるので、こちらの方がとっつきやすいかもしれません。

node.js が動く環境があれば、以下のコートからすぐに試すことができます。
コード： https://github.com/TsuyoshiNumano/stayhome-board-minimum/tree/master/socket-io-version

- node.js が動く環境を準備
- socket-io, express の導入
- index.html を firebase から、socket-io に対応する

```js:socket-ioを使ったserverの実装

var app = require("express")();
var http = require("http").createServer(app);
var io = require("socket.io")(http);
var fs = require("fs");

// index.html をホスティング
app.get("/", (req, res) => {
  res.sendFile(__dirname + "/index.html");
});

io.on("connection", (socket) => {
  var socketId = socket.id;
  var roomId = socket.handshake.query.id;
  socket.join(roomId);
  try {
    // 接続があったら、ファイルからデータを読み込んで送信
    var data = fs.readFileSync(`data/${roomId}.json`, "utf-8");
    io.to(socketId).emit("server_item_update", JSON.parse(data));
  } catch (e) {
    console.log(e);
  }
  // クライアントからメッセージがきたら、自分以外のルームにデータを送信し、ファイルに保存
  socket.on("client_item_update", (data) => {
    socket.broadcast.to(roomId).emit("server_item_update", data);
    fs.writeFile(`data/${roomId}.json`, JSON.stringify(data), (err) => {
      if (err) throw err;
    });
  });
});

http.listen(3000, () => {
  console.log("listening on *:3000");
});

```

```diff:socket-ioを利用したクライアント側のコードの変更
+    <script src="/socket.io/socket.io.js"></script>
-      let db = null;
+      var socket = null;

       const Board = () => {
         React.useEffect(() => {
@@ -32,23 +19,33 @@
             let roomName = window.prompt("please input room's name");
             window.location.href = window.location.href + "?room_name=" + roomName;
           }
-          db = firebaseDb.ref(roomName);
-          db.on("value", (value) => setCards(value.val()));
+          socket = io();
+          socket.on("server_item_update", (msg) => setCards(msg));
         }, []);

         const [cards, setCards] = React.useState(null);
         const add = () => {
-          const newPostKey = db.push().key;
-          db.update({
-            [newPostKey]: {
+          const newCards = {
+            ...cards,
+            [Math.random().toString(36).slice(-8)]: {
               t: "wash",
               x: Math.floor(Math.random() * (200 - 80) + 80),
               y: Math.floor(Math.random() * (200 - 80) + 80),
             },
-          });
+          };
+          setCards(newCards);
+          socket.emit("client_item_update", newCards);
+        };
+        const update = (key, card) => {
+          const newCards = { ...cards, [key]: card };
+          setCards(newCards);
+          socket.emit("client_item_update", newCards);
+        };
+        const remove = (key) => {
+          delete cards[key];
+          setCards(cards);
+          socket.emit("client_item_update", cards);
         };
-        const update = (key, card) => db.update({ [key]: card });
-        const remove = (key) => db.child(key).remove();
```

[github での差分](https://github.com/TsuyoshiNumano/stayhome-board-minimum/pull/6)

ポイント

- express で index.html ごとホスティングするので localhost:3000 でアクセスし、ホスティングと　socket-io のサーバーが一緒に立てれる
- それによって、クライアント側では `<script src="/socket.io/socket.io.js"></script>` で socket-io のクライアントライブラリを読み込むことができる
- サーバー側でのデータの永続化には DB などは使わず、直接ファイルに json 形式で保存
- サーバー側では、クライアントから来たデータを丸ごと他のクライアントに返しているだけ
- クライアント側の socket-io でデータのやり取りは `useEffect` で最初にマウントした時に `socket.on("server_item_update", (msg) => setCards(msg));` とするとことで、 `server_item_update` のメッセージがあるたびに、新しい state に更新する。
- ロジックをシンプルにした結果、fireabase バージョンとほとんど変更がないものの、毎回全てのデータを転送するので、転送コストが大きいのが微妙なところ。



## あとがき
家に引きこもっていると、友人からメッセージが来て「こんな世の中だからこそ、自分たちに出来ることを探そうよ」と言われて、自分にできることはなんだろうと考えた結果、自分はサービスを作るよりも、サービスの作り方のようなものを伝搬して、より良いサービスを誰かが作るきっかけ自体を作る方が良さそうな気がしたので、記事を書いた次第です。役に立たなくても何かのきっかけになれば嬉しいです。
`#stayhome`

## おすすめのコラボレーションツール
わざわざ自分で作らなくても、良さそうなツールがたくさん出てきてるので紹介しておきます。

- [リモブレ（リモートブレスト）で創造的なアイデアを生み出す方法](https://qiita.com/aoinakanishi/items/f8074f91da54fb876ecb) で紹介されている [Miro](https://miro.com/) 
- [この際リモートでどこまででもやる Mural登場オンラインで付箋ワーク](https://note.com/hackcamp_kikui/n/ne80cc00127ff) で紹介されている [Mural](https://www.mural.co/)
- Good Patch の 「[Strap](https://goodpatch.com/ja/news/strap_%20beta_release)」
- （昔 kpt が流行ったときに自分が作ったものと全く同じような動きをするオンライン付箋アプリがあったが、名前が今だに思い出せない）