Cloud Storage for Firebaseに画像をアップロードしてみる

## はじめに

どこかで初心者フロントエンドエンジニアはImage Uploaderを作ると良いですよ!
というのを見かけたので、初心者フロントエンドエンジニアってわいのことやん！これは作るしかない！と思っていたのですが、気づいたら年末になっていました。(時が過ぎるのは早いですね)アドベントカレンダーという良い機会を頂いたのでせっかくなので作ってみました。

因みにフロントはReactで作ろうとは思っていたのですが、画像はどこに保存しようかなぁと思っていたんですよ。エンジニアの友達に相談したら「Firebaseしかないやろ」って言われたので今回はFirebaseを使ってみました。ちょうどCloud Storage for Firebaseは使ったことなかったので良い勉強になるかなぁ&今時っぽいなという理由で選定しました。

ということで、Cloud Storage for Firebaseを用いて画像をアップロードする仕組みを作成したので、その作成においての軌跡を書いていきたいと思います。

## この記事で書くこと
- Cloud Storage for Firebaseに画像をアップロードする方法
- フロント側で画像を取り込みCloud Storage for Firebaseと接続する方法

です。
Reactの導入や詳しいコード説明、CSSについては主題ではないので割愛しています。

## Cloud Storage for Firebase とは

公式によると
> Cloud Storage for Firebase は、Google 規模で構築された、強力かつシンプルでコスト効果の高いオブジェクト ストレージ サービスです。
参考: https://firebase.google.com/products/storage?hl=ja

だそうです。つまり、写真や動画など、ユーザーが作成したコンテンツを保管することができるサービスですね。今回作成したImage Uploaderといったものの保存先として使えたり、また保存したコンテンツをどこかに出力し、提供することも可能です。
また、アプリケーションのモバイル接続の切断と回復に合わせてデータ転送の一時停止や再開を自動的に行うことで、ユーザーの時間と帯域幅を節約したりもしてくれるそうです。便利ですね！


## Firebaseの導入手順
ではさそっく導入したいと思います。まずはFirebaseを導入するところからになります。
[こちらの公式ページ](https://firebase.google.com/?hl=ja)から始めることができます。Googleアカウントが必要になるのでもしなければ作成してください。Googleアカウント認証が終わればすぐに始められます。

### Firebaseプロジェクトの作成
- 「プロジェクトを追加」から新規プロジェクト作成。
- 今回は画像を保存するために使いたいので「image-store」にしています。名前は何でも良いです。

![スクリーンショット 2020-12-06 16.42.45 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/aa9a4696-e14c-f821-dd0a-60638283589e.png)

もしGoogle Analyticsがあれば、選択しておきましょう。
![スクリーンショット 2020-12-06 16.43.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/7c50fc54-5f72-0d25-b0d6-116abf6f0f1e.png)

「プロジェクトを作成」を押下して作成したら、ホームにプロジェクトが追加されます。

### Cloud Storage for Firebaseを作成
まず Storegeを選択します。「始める」 を押下するとCloud Storage の設定が出てきます。

![スクリーンショット 2020-12-06 16.50.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/d617ec8b-736d-635a-10bf-1168798ca931.png)

最初は Cloud Storageのセキュリティ保護ルールの設定になります。
![スクリーンショット 2020-12-06 16.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/e3f2cd7b-646f-0372-3764-b9c5a15be855.png)
あとで変更しますが、一旦このまま続行で大丈夫です。

次に Cloud Storage のロケーションの設定になります。
`asia-northeast1` は 東京 なのでこちらを選択しました。
`asia-northeast2` は 大阪 なのでそちら付近の方であればこっちを選択すると良さそうです。

設定が終わるとデフォルトバケットが生成されます。

### Cloud Storageのセキュリティ保護ルールの設定をいじる

以下のように変更することで、認証していないユーザーでもアップロードできるようになります。

```
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}
```

今回はローカルで画像をアップロードするまでを主眼に置いていますので、認証周りの設定は行いません。本来であれば認証の仕組みを構築して、セキュリティ保護ルールを良しなにする必要があります。なので今回は誰でも認証を突破できるようにruleを設定していますが、認証構築後に正しく変更してください。

### ウェブアプリにFirebaseを追加する
メニューのところから、「プロジェクトを設定」 を選択します。
![スクリーンショット 2020-12-06 17.04.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/23517bd9-a202-a2e9-9761-09d29053ece8.png)

今回はReact で アプリケーションを作るので
ウェブアプリ を選択します。
![スクリーンショット 2020-12-06 17.05.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/f3bbc87c-c885-12ee-4b44-23d6e95baebb.png)


「ウェブアプリにFirebaseを追加」のところで Firebase Hosting を選択できます。こちらを使いたい場合はチェックを入れておきましょう。因みに今回書いていませんがめっちゃ簡単にdeployできるのでおすすめです。僕は個人的にVercelばかり使いますが、Firebase Hostingもわかりやすくて良いなぁ〜って感想です。

![スクリーンショット 2020-12-06 17.06.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/dee2b121-c704-c6c0-ae22-ce62fe84d92c.png)

諸々の設定が終わったらアプリを登録して終わりです。ここまででFirebaseの管理画面でやることはおしまいです。次はFirebaseをinitializeします。がその前にFirebase SDK snippet はあとで使うことになるのでコピーするかどこにあるか確認しておくと良いです。`「設定」/「全般」`の所に snippetはあります。

### `firebase init` を行う

既にReactでアプリケーションを作成していることを前提で話していきます。
まず firebase-tools をインストールするか、既にインストールしている場合は最新バージョンの firebase-tools に更新しましょう。

```bash

npm install -g firebase-tools
```

次に Google へログインします。

```bash

firebase login
```

Firebaseと同じアカウントで行います。
ログインできたら、initコマンドを実行します。

```bash

firebase init
```
上記のコマンドを叩けばinitializeを始めれます。対話形式で設定が進むので該当するものを選んでいきます。いくつかピックアップしたので以下に書いていきます。



```
? Which firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices.
```

```bash

// Firebaseを使う上で必要なものを選択しましょう。

? Which firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices.
  ❯◉ Database: Deploy firebase Realtime Database Rules
  ◯ Firestore: Deploy rules and create indexes for Firestore
  ◯ Functions: Configure and deploy Cloud Functions
  ❯◉ Hosting: Configure and deploy firebase Hosting sites
  ❯◉ Storage: Deploy Cloud Storage security rules

```
- Database
- Storage
- Hosting

を僕は選択しました。

ちなみに今回は選択していないので起こりえないですが、FirestoreとStorageを同時選択し、errorが出た場合は https://github.com/firebase/firebase-tools/issues/1988 を参考してみてください。

```bash

// 今回は既にプロジェクトは作っているので既存のプロジェクト選択を選びます。
Please select an option: Use an existing project

// プロジェクトを選択します。
? Select a default firebase project for this directory: [自分の作成したプロジェクト]

// 今回はReactなのでbuildを選択します。
? What do you want to use as your public directory?

// 同じくReactなのでSPAです。ここはyesで。
?Configure as a single-page app (rewrite all urls to /index.html)?

// デフォルトの storage.rules で大丈夫です。
?What file should be used for Storage Rules?

```

`✔  firebase initialization complete!` が出たら完了です。
指定したアプリケーション内にFirebase関連のファイルが生成されます。

## フロントエンド側でやること

### アプリとFirebaseの接続

ここまででCloud Storage for Firebaseのバケットの設定、アプリのFirebaseのinitializeが完了しました。あとはアプリ側とFirebaseを繋いであげましょう。
アプリのディレクトリ内にfirebase ディレクトリを作成しその配下に`firebase.js`を作成します。ここに`firebaseConfig`を定義します。ここのConfigには、先ほどコピーしておくと良いと言ったsnippetを記述します。

```js:firebase/firebase.js

import firebase from "firebase/app";
import "firebase/storage";

const firebaseConfig = {
  // config
  apiKey: "AIzaSyCeDfY.............EQjvC6miPGTY",
  authDomain: "image-store.......seapp.com",
  databaseURL: "https://image-store-9........rebaseio.com",
  projectId: "image-st.....6",
  storageBucket: "image-store-9.....pot.com",
  messagingSenderId: "756.....450",
  appId: "1:7564.............4c241152ea3",
  measurementId: "G......0KQ"
};
// Initialize firebase
firebase.initializeApp(firebaseConfig);
export const storage = firebase.storage();
export default firebase;

```

因みこれでも良いですが、これらのConfigは実際にGithub等にpushしてしまうと問題なので、環境変数を用いて隠蔽しておきたいですね。
.envファイルを作成し、以下のように記述してください。

```plaintext:.env
API_KEY=AIzaSyCeDfY.............EQjvC6miPGTY
AUTH_DOMAIN=image-store.......seapp.com
DATABASE_URL=https://image-store-9........rebaseio.com
PROJECT_ID=image-st.....6
STORAGE_BUCKET=image-store-9.....pot.com
MESSAGING_SENDER_ID=756.....450
APP_ID=1:7564.............4c241152ea3
MEASUREMENT_ID=G......0KQ

```

そして、dotenvというモジュールをインストールします。インストールできたら、`firebase.js`を以下のように書き換えます。

``` js:firebase.js
import firebase from "firebase/app";
import "firebase/storage";

require("dotenv").config();

const firebaseConfig = {
  // config
  apiKey: process.env.API_KEY,
  authDomain: process.env.AUTH_DOMAIN,
  projectId: process.env.PROJECT_ID,
  storageBucket: process.env.STORAGE_BUCKET,
  messagingSenderId: process.env.MESSAGING_SENDER_ID,
  appId: process.env.APP_ID,
  measurementId: process.env.MEASUREMENT_ID,
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
export const storage = firebase.storage();
export default firebase;

```
これで環境変数を参照できるようになりましたね。あとは、`.envファイル`をignoreしておけば大丈夫です。
この`firebase.js`で定義した`storage`を用いて実際に受け取るファイルデータをCloud Storage for Firebaseへ送信することができるようになります。

### Reactでファイルをアップロードする機能を作成する
今回はUploadコンポーネントとしてアップロードする機能を切り出しています。

```tsx:src/App.tsx

import React from "react";
import "./App.css";
import Upload from "./components/Upload";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Upload />   // ←これ
      </header>
    </div>
  );
}

export default App;
```

Uploadコンポーネントのコードは以下になります。一部`material-ui`を使っているので必要であればインストールしておいてください。

```tsx:components/Upload.tsx

import React, { useState, useCallback } from "react";

import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { Typography, Button } from "@material-ui/core";

import { useDropzone } from "react-dropzone";
import firebase, { storage } from "../firebase/firebase";

const Upload: React.FC = () => {
  const [myFiles, setMyFiles] = useState<File[]>([]);
  const [clickable, setClickable] = useState(false);
  const [src, setSrc] = useState("");

  const onDrop = useCallback(async (acceptedFiles: File[]) => {
    if (!acceptedFiles[0]) return;

    try {
      setMyFiles([...acceptedFiles]);
      setClickable(true);
      handlePreview(acceptedFiles);
    } catch (error) {
      alert(error);
    }
  }, []);

  const onDropRejected = () => {
    alert("画像のみ受け付けることができます。");
  };

  const { getRootProps, getInputProps } = useDropzone({
    onDrop,
    onDropRejected,
  });

  const handleUpload = async (accepterdImg: any) => {
    try {
      // アップロード処理
      const uploadTask: any = storage
        .ref(`/images/${myFiles[0].name}`)
        .put(myFiles[0]);

      uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, next, error);
    } catch (error) {
      console.log("エラーキャッチ", error);
    }
  };

  const next = (snapshot: { bytesTransferred: number; totalBytes: number }) => {
    // 進行中のsnapshotを得る
    // アップロードの進行度を表示
    const percent = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    console.log(percent + "% done");
    console.log(snapshot);
  };

  const error = (error: any) => {
    alert(error);
  };

  const handlePreview = (files: any) => {
    if (files === null) {
      return;
    }
    const file = files[0];
    if (file === null) {
      return;
    }
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      setSrc(reader.result as string);
    };
  };

  return (
    <Card>
      <CardContent>
        <Typography variant="h6">Upload your image</Typography>
        <p>File should be Jpeg, Png,...</p>
        <div {...getRootProps()}>
          <input {...getInputProps()} />
          {myFiles.length === 0 ? (
            <p>Drag&Drop your images here</p>
          ) : (
            <div>
              {myFiles.map((file: File) => (
                <React.Fragment key={file.name}>
                  {src && <img src={src} />}
                </React.Fragment>
              ))}
            </div>
          )}
        </div>
        <Button
          disabled={!clickable}
          type="submit"
          variant="contained"
          fullWidth
          style={{ marginTop: "16px" }}
          onClick={() => handleUpload(myFiles)}
        >
          UOLOAD
        </Button>
      </CardContent>
    </Card>
  );
};
export default Upload;
```
フロント側では、画像をDnDできるように react-dropzoneというモジュールを使用しています。今回は話の主題ではないので割愛しますが、[公式ドキュメント](https://react-dropzone.js.org/)がわかりやすいので気になった方はぜひ見てください。

#### handleUploadでアップロード処理を実装する

```tsx:components/Upload.tsx

  const handleUpload = async (accepterdImg: any) => {
    try {
      const uploadTask: any = storage
        .ref(`/images/${myFiles[0].name}`)
        .put(myFiles[0]);

      uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, next, error);
    } catch (error) {
      console.log("エラーキャッチ", error);
    }
  };

```
DnDもしくはクリックからの画像選択を行った後に、Buttonを押下すると、`handleUpload`が発火するようにしています。この時引数に`myFiles`を受け取るようにしています。`myFiles`はreact-dropzoneの`onDrop`によって画像ファイルを受け取り、useStateで保持しているものになります。つまり画像ファイルそのもののデータですね。

```tsx
const uploadTask: any = storage
        .ref(`/images/${myFiles[0].name}`)
        .put(myFiles[0]);
```

上記はアップロード処理になります。`storage.ref()`を用いて、`画像のアップロード先のフォルダ名/ファイル名`を指定して、`put()`で画像のファイルを渡すことでCloud Storage for Firebaseにアップロードしています。

```tsx:Upload.tsx
uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, next, error);
```

uploadTaskはFirebaseのstorageなので`.on`が使えます。このイベントはnext、error、completeの3つのコールバックを呼ぶことができ、イベントのみが渡された場合のみコールバックの登録に使用できる関数を返してくれます。それ以外の場合だとイベントの後にコールバックを渡してくれます。コールバックは一つ指定されていればOKで、残りはオプションとして扱われます。詳しくはFirebaseの[公式](https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask?hl=ja#on)を見てください。今回はnextとerrorを指定しました。

```tsx:Upload.tsx

  const next = (snapshot: { bytesTransferred: number; totalBytes: number }) => {
    // 進行中のsnapshotを得る
    // アップロードの進行度を表示
    const percent = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    console.log(percent + "% done");
    console.log(snapshot);
  };
```
nextでは、進行度合いを`console.log`に出力しています。次はプレビューの関数を見てみましょう。

#### handlePreviewでプレビュー処理を実装する


```tsx:Upload.tsx

  const handlePreview = (files: any) => {
    if (files === null) {
      return;
    }
    const file = files[0];
    if (file === null) {
      return;
    }
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      setSrc(reader.result as string);
    };
  };
```
DnDもしくは画像を選択すると、`handlePreview`が発火します。やっていることは、画像ファイルが取り込まれれば、そのデータを`useState`を用いて管理しているだけです。こうすることで以下のdivタグ内のsrcに取り込んだ画像データが渡されます。

```tsx:Upload.tsx

<div>
  myFiles.map((file: File) => (
    <React.Fragment key={file.name}>
      {src && <img src={src} />}
    </React.Fragment>
  ))}
</div>
```

これで実装は完了です。


## 完成

今回、CSSは書いていませんが、styled-componentsでstyleを書いた場合の完成版は以下のようになります。
![02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528613/36cfa3c7-dd47-935f-931e-e849d4f6bba3.gif)
UPLOADボタン押下すると、Cloud Storage for Firebaseの方に画像が格納されているのがわかると思います。


## おわりに

Image Uploaderを作ってみましたが、意外と勉強になることが多く、学びが深かったです。
特にCloud Storage for Firebaseは初めて使ったのですが、設定からアプリの接続まで公式のドキュメント等が充実しているのでやりやすかったです。フロント部分についても詳しくは説明できませんでしたが、react-dropzoneを用いたり、画像のアップロードやプレビューの仕組みを作るといったところは勉強になりました。もちろんCSSも実際は書いているのでここも勉強になります。初心者フロントエンドエンジニアの方にはぜひ作ってみてほしいです。