「Image Uploader」作成記録 - React + Firebaseで作るWebアプリケーション開発

# 「Image Uploader」作成記録 - React + Firebaseで作るWebアプリケーション開発

devchallengesのImage Uploaderに取り組んだ記録です。

## 何を作ったか？

画像アップロードが行えるWebアプリケーションを作成しました。下記の機能が利用できます。

- 任意の画像を指定してアップロードできる。
- 画像のダウンロードURLを取得できる。

ただし、下記の制約があります。

- 特に判定をしていないため、画像以外でもアップロードできてしまう。
- ストレージのセキュリティ設定が不安だったため、書き込みを競合させている。そのため、他の画像をアップロードすると前の画像が削除される。(全ユーザーで1画像のみ)

### リポジトリ

成果物は下記のリポジトリにコミットしています。使い方は[README.md](https://github.com/charon1212/ImageUploader_devchallenges/blob/main/README.md)を参照してください。

- リポジトリURL：<https://github.com/charon1212/ImageUploader_devchallenges>

### アプリケーション

下記の画像は、本アプリケーションのスクリーンショットです。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/70018f0b-8958-bf6a-81d6-31af9b0054f8.png)

- アップロード画面から、画像のアップロードが行えます。アップロードの方法は、下記の2通りがあります。
  - 青点線の中にドラッグ＆ドロップすると、画像のアップロードが行えます。
  - 「Choose a file」ボタンを押すと、ファイル選択のダイアログが表示されます。ファイルを選択すると、アップロードが行えます。
- 画像をアップロードすると、プログレスバーが表示されます。
- 画像のアップロードが完了すると、完了画面が表示されます。
- 「COPY LINK」ボタンを押すと、ダウンロード用のURLをクリップボードにコピーします。

### 自動テスト

seleniumに触ってみたかったため、各種自動テストのサンプルも作成しました。ただし、CI&CDの設定とかはやってないです。カバレッジも計測していません。

- React部品テスト
  - react-testing-libraryを使った基本的な単体テスト。
- E2Eテスト
  - seleniumを使ったE2Eテスト。

## どうやって作ったか？

Webアプリケーションの中身や、作成時に調べたことをまとめています。

### 採用した技術/ライブラリ

- [React](https://ja.reactjs.org)
  - SPAのフレームワークのように利用。
- [TypeScript](https://www.typescriptlang.org)
  - せっかくなので、型付けができるTypeScriptを選択。
- [Firebase](https://firebase.google.com)
  - バックエンド。ホスティング・アップロードデータの保存に利用。
- [Material-UI](https://material-ui.com)
  - 画面のUI。
- [react-dropzone](https://github.com/react-dropzone/react-dropzone)
  - ドラッグ＆ドロップ用の画面要素を提供するライブラリ。
- [copy-to-clipboard
](https://www.npmjs.com/package/copy-to-clipboard)
  - クリップボードへのコピー機能を提供するライブラリ。
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
  - React部品の単体テストフレームワーク。
- [selenium-webdriver](https://www.selenium.dev/selenium/docs/api/javascript/index.html)
  - E2Eテストのための、ブラウザ操作の自動化ツール。

### 前提知識

ReactとTypeScriptに関する知識は多少ありましたが、それ以外の知識(主にバックエンドとホスティング)は皆無でした。そのため、下記の講座を受講しました。

この講座では、React+FirebaseでWebアプリケーションをリリースするハンズオンを学べます。また、TypeScript、Redux、MaterialUIといった、各種技術の導入も学べます。ただし、簡単なハンズオンのみなので、Reactの知識がない方は別で学んでおくことをおすすめします。

- [Firebase + React Hooks(TypeScript)によるWebアプリ開発](https://www.udemy.com/course/firebase-react-hookstypescriptweb/)
- [最速で学ぶFirebase + React Hooks (TypeScript)](https://www.udemy.com/course/firebasereact-hookstypescript-todo/)

(後者の無料講座の最後にクーポンがついていて、いつでもお安く買えます。)

### アプリ

#### 初期環境構築

[前提知識](#前提知識)で紹介した講座でも使っていた、下記のテンプレートで組みました。そのため、自動的にRedux-toolkitもついてきましたが、使ってはいません。

```bash
$ npx create-react-app <project-name> --template redux-typescript
```

これだけで、React + Redux + TypeScriptのテンプレートが自動的に構築されます。

#### 画面遷移

今回は最上位のコンポーネントでstateを見て描画するコンポーネントを変更しています。（react-router-domを使ってもよかったのですが、それほど大きくなかったので...）

最上位のコンポーネント「App.tsx」には、下記の2つのstateがあります。

- hasUploadedFile:ファイル選択前はfalse、ファイル選択後はtrue。
- fileUrl: アップロードしたファイルのURL。初期値は空文字。

上記のstateに応じて、下記のように画面描画を切り分けています。

- hasUploadedFileがfalseの場合、アップロード画面。
- hasUploadedFileがtrueで、fileUrlが空文字の場合、プログレスバーの画面。
- hasUploadedFileがtrueで、fileUrlが空文字でない場合、アップロード完了画面。

#### スタイリング

CSS等もほぼ初心者だったため、手探りでいろいろ設定しました。

- センタリング
  - left-marginとright-marginに`'auto'`を設定する。
    - 参考：[CSSで要素を上下や左右から中央寄せする7つの方法 | 株式会社グランフェアズ](https://www.granfairs.com/blog/staff/centering-by-css)
  - BoxのjustifyContent属性を使う。
    - 参考：[Flexbox - Material-UI](https://material-ui.com/system/flexbox/#justify-content)
- 枠線
  - border属性を利用。
  - 丸みはborder-radiusで設定。

#### ドラッグ＆ドロップ

[react-dropzone](https://github.com/react-dropzone/react-dropzone)を知っていたので、そのまま利用しました。GitHubのUsageに従ってHookを利用しました。

#### クリップボードへのコピー

[copy-to-clipboard](https://www.npmjs.com/package/copy-to-clipboard)を利用しました。とても手軽にクリップボードを利用できる。

#### ボタン入りテキストボックス

[Input Adornments](https://material-ui.com/components/text-fields/#input-adornments)という機能を利用しました。成果物は、下記のURLです。

- <https://github.com/charon1212/ImageUploader_devchallenges/blob/main/src/components/InButtonInput.tsx>

#### バックエンド

FirebaseのStorageを利用しました。基本的な使い方は、[前提知識](#前提知識)で紹介した講座で学べます。

セキュリティルールはすべて許可とかやると怖かったので、下記のように設定しています。設定方法は、公式ドキュメントの[Firebase セキュリティ ルール スタートガイド](https://firebase.google.com/docs/rules/get-started)で調べました。

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

また、ホスティングもFirebaseで行っています。

### React単体テスト

初期環境構築時のテンプレートにReact Testing Libraryが自動でくっついてきます。ほとんどこれを利用しているだけです。

#### waitFor

なぜか、初期構築のテンプレートからだとwaitForが利用できませんでした。。。いったん、自前で「@testing-library/dom」をnpmからインストールして、それ経由で利用しています。

### E2Eテスト(selenium)

seleniumを使ってみたかったため、E2Eの自動テストを作成しました。seleniumの使い方自体は、公式ドキュメントがしっかりしているため特に困らずに実装が進められました。

#### 絶対パスへの変換

Node.JSの標準機能でPathというモジュールがあり、そこから使えます。注意点として、渡す相対パスはプロジェクトのルートディレクトリからの相対パスみたいです。

- ドキュメント：<https://nodejs.org/api/path.html#path_path_resolve_paths>
- 使用例：

```javascript
import { resolve } from 'path';
const filePath = resolve('<プロジェクトルートからの相対パス>')
```

#### ファイルダウンロード

seleniumのWebDriverからダウンロードしたかったのですが、いまいち調べきれませんでした。今回は認証/認可が絡まないダウンロードのため、普通にfetch APIを使ってダウンロードを実装しました。

- 参考：[javascript - How can I download and save a file using the Fetch API? (Node.js) - Stack Overflow](https://stackoverflow.com/questions/37614649/how-can-i-download-and-save-a-file-using-the-fetch-api-node-js)

#### ファイルのハッシュ値を取得

アップロードしたファイルと、ダウンロードリンクからダウンロードしたファイルが等価であることを検証するために、ハッシュ値で比較しました。ハッシュ値の取得は下記を参考にしています。

- 参考：[Node.jsでSHA256ハッシュ値生成 - キリウ君が読まないノート](https://note.kiriukun.com/entry/20190822-generating-a-sha256-hash-in-nodejs)

#### Jestの設定

普通に「jest」コマンドをたたくと、React単体テストもテスト対象になってしまいます。そのため、設定ファイル「jestSeleniumTest.config.js」を使ってテストファイルの検索対象を絞っています。

設定ファイルを有効にする場合、jestコマンドの「--config」オプションを使って、下記のように設定します。参考：[Jest CLI Options · Jest](https://jestjs.io/docs/ja/cli#--configpath)

```json:package.json
{
  "scripts": {
    "jesttest": "jest --config jestSeleniumTest.config.js",
  },
}
```

設定ファイルでは、下記のようにして検索対象を絞っています。参考：[Configuring Jest · Jest](https://jestjs.io/docs/ja/configuration#testmatch-arraystring)

```javascript
module.exports = {
  "testMatch": ["<rootDir>/selenium_test/test/**/*.test.ts"]
};
```

## 謝辞

- [Image Uploader - DevChallenges](https://devchallenges.io/challenges/O2iGT9yBd6xZBrOcVirx)
  - とても良い題材を提供していただき、ありがとうございました。今後も他の項目にチャレンジしたいです。(...飽きなければ...)
- [フルスタックエンジニアになるためのモダンな8つのプロジェクト - Qiita](https://qiita.com/kanye__east/items/77a9746e9b98abbb97e6)
  - DevChallengesを知るきっかけとなった記事です。取り組むきっかけをくださり、ありがとうございました。