「Authentication App」作成記録 - React + Firebaseで作るWebシステムの認証機能

# 「Authentication App」作成記録 - React + Firebaseで作るWebシステムの認証機能

devchallengesの「Authentication App」に取り組んだ記録です。

## devchallenges

[devchallenges](https://devchallenges.io/)は、様々なソフトウェアのお題を提供してくれるサイトです。お題には、そのソフトウェアが満たすべきユーザーストーリーと、画面イメージが記載されています。何を作るかは説明がありますが、どう作るかは基本的に書いていないため、アプリケーション開発の学習に最適です。

今回は、[Authentication App](https://devchallenges.io/challenges/N1fvBjQfhlkctmwj1tnw)というお題に取り組みました。

## 何を作ったか？

認証機能を持つWebアプリケーションを作成しました。下記の機能があります。

- 認証機能。予めメールアドレスとパスワードを登録しておき、それでログインできます。
- SNS認証機能。Googleアカウントを使用してログインできます。Twitter等もできるようにしたかったのですが、Twitter側でのApp登録が面倒で諦めました。。。
- プロフィール編集機能。ユーザーの名前、アバター画像などが編集できます。

また、一応レスポンシブ対応しているので、スマホで見ることもできます。
(ちょっとトップ画面が崩れてますが…(汗))

### リポジトリ・デモ

成果物は下記のリポジトリにコミットしています。使い方はREADME.mdを参照してください。

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

また、作成したアプリケーションはデプロイしています。下記のURLからお試しできます。

- Authentication App：<https://authenticationapp-dc.web.app>

### 画面紹介

- ログイン画面

![login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/9c03240a-011a-30a3-af0d-0c6d47ee88bf.png)

- トップ画面

![userinfo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/ebeb5f7c-c5f9-74a8-ca02-7d0742d7c500.png)

- 編集画面

![userinfoedit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/901bac75-a064-748c-c1f7-732d332d7c9d.png)

### React部品紹介

作成したReact部品のうち、他のアプリにも流用できそうな部品を紹介します。

#### Auth.tsx

- リポジトリ：[Auth.tsx](https://github.com/charon1212/authentication_app_devchallenges/blob/master/src/components/common/Auth.tsx)

今回のアプリで一番頑張った部品です。React-Router-Domと組み合わせることで、認証に関するロジックをすべてここへと集約しています。

基本的なつくり・アイデアは、こちらの記事を参考にしました。これをFirebase/Redux用に少しカスタマイズしています。

<https://qiita.com/ginban22/items/a36d01b41deaeedd581e>

この部品は、下記のように利用します。認証が必要な画面へのRouteはAuthコンポーネントのchildrenに指定します。必要ない画面へのRouteはAuthコンポーネントの外に指定します。すると、firebase上で認証済みの場合は各画面を表示し、未認証状態の場合はloginUriで指定した画面(下の場合はLoginコンポーネント)を表示します。

```js:AppRouter.tsx
      <Router>
        <Switch>
          <Route exact path={pathLogin} component={Login} />
          <Route exact path={pathSignUp} component={SignUp} />
          <Auth loginUri={pathLogin} loadingComponent={LoginLoading}>
            <Switch>
              <Route exact path={pathUserInfoEdit} component={UserInfoEdit} />
              <Route component={UserInfo} />
            </Switch>
          </Auth>
        </Switch>
      </Router>
```

#### AvatarEdit.tsx

- リポジトリ：[AvatarEdit.tsx](https://github.com/charon1212/authentication_app_devchallenges/blob/master/src/components/AvatarEdit.tsx)

画像をアップロードするための部品です。アップロードすると、Avatarタグにプレビュー表示します。プロフィールの編集画面で利用しています。

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

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

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

- [React](https://ja.reactjs.org/)
  - SPAフレームワークとして採用。Vueよりも単純に好き。
- [TypeScript](https://www.typescriptlang.org/)
  - 型付けできるTypeScriptを採用。
- [Firebase](https://firebase.google.com/)
  - バックエンド。ホスティング・アップロードデータの保存に利用。
- [Material-UI](https://material-ui.com/)
  - 画面のUI用ライブラリ。
- [React Redux](https://react-redux.js.org/)
  - 状態管理用のライブラリ。
- [React Router Dom](https://reactrouter.com/web/guides/quick-start)
  - ルーティングのライブラリ。

### デザイン

#### Material-UIでCSSセレクタのhoverやnth-of-typeを使う

Material-UIではcss-in-javascriptを採用しているため、直接CSSセレクタを使うことはありません。しかし、Material-UI側でちゃんと用意してくれています。

- 参考：<https://awesome-linus.com/2020/01/03/react-material-ui-css-nth-of-type-hover/>

#### 画像ボタンの作り方

CSSを使うと、画像をボタンのように見せかけることができます。本アプリでは、ログイン画面のSNS認証のところで使ってます。

- 参考：<https://webparts.cman.jp/button/image/>

#### material-UI Grid itemの中央寄せ

- 参考：<https://qiita.com/ryogo_niwa/items/456e2854288dd16fbab5>

#### レスポンシブ対応

入門的な内容ですが、今の自分にはこれで十分でした。

- 参考：<https://www.kagoya.jp/howto/webhomepage/responsive/>

#### 複数のクラス名を結合する・動的なクラス名の指定

clsxというライブラリが、Material-UIにくっついてきます。これを使うと楽でした。また、clsxを使うと動的にクラスを切り替えることができます。

- 参考：<https://qiita.com/taqm/items/c38855d8158cdd9d5a3e>

### Webアプリ

#### textFieldでEnterが押されたときにイベントを実行する

onKeyDownイベントを使うとできます。イベントハンドラーのKeyプロパティで押されたキーを判別します。
検索するとKeyCodeを使うサンプルが多いですが、これは非推奨でWeb標準から削除されています。

- 参考：<https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/key>

#### 画像ファイルのプレビュー表示

Javascriptに標準でついているFileReader APIが使えます。

- 参考：<https://blog.capilano-fw.com/?p=2574>

#### 複数の非同期処理

Firebaseへのアップロード処理を非同期で行いますが、最大で4つのPromiseを並列実行しました。4つの処理を並列実行し、それらがすべて終わった後でやりたい処理があるときは、標準で用意されているPromise.allが使えます。

- 参考：<https://qiita.com/im36-123/items/c0678a46ee0f8e44e150>

### FirebaseのAPI

今回利用したFirebaseの各機能の使い方をまとめます。(後で自分が使うとき思い出せるように…)

#### 事前準備

どのFirebase Projectのアプリケーションなのか、コード上で設定する必要があります。下記参照。

- ドキュメント：<https://firebase.google.com/docs/web/setup>

#### auth:サインイン

- ドキュメント：<https://firebase.google.com/docs/auth/web/password-auth>

```typescript
let promise: Promise<firebase.auth.UserCredential>;
// メールアドレス & Passwordの場合
promise = firebase.auth().signInWithEmailAndPassword(mail, password);
// Google認証の場合
promise = firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider());
// 認証後の後処理
promise.then((userCredential) => {
  const user = credential.user;
  if(user){
    const {displayName, email, phoneNumber, photoURL, uid} = user; // この辺のプロパティが利用できます。
  }
}).catch((error) => {
  const { code, message } = error; // この辺のプロパティが利用できます。
});
```

#### auth:サインアップ

- ドキュメント：<https://firebase.google.com/docs/auth/web/start#sign_up_new_users>

```typescript
firebase.auth().createUserWithEmailAndPassword(mail, password).then((userCredential) => {
  const user = credential.user;
  if(user){
    const {displayName, email, phoneNumber, photoURL, uid} = user; // この辺のプロパティが利用できます。
  }
}).catch((error) => {
  const { code, message } = error; // この辺のプロパティが利用できます。
});
```

#### auth:認証状態のオブザーバーを設定する

- ドキュメント：<https://firebase.google.com/docs/auth/web/start#set_an_authentication_state_observer_and_get_user_data>

```typescript
firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    // User is signed in, see docs for a list of available properties
    // https://firebase.google.com/docs/reference/js/firebase.User
    const {displayName, email, phoneNumber, photoURL, uid} = user; // この辺のプロパティが利用できます。
  } else {
    // User is signed out
  }
}).catch((error) => {
  const { code, message } = error; // この辺のプロパティが利用できます。
});
```

#### auth:サインアウト

- ドキュメント：<https://firebase.google.com/docs/auth/web/password-auth>
  - ※一番下にちょこっと書いてある

```typescript
firebase.auth().signOut().then(() => {
  // サインアウト成功時
}).catch((error) => {
  const { code, message } = error; // この辺のプロパティが利用できます。
});
```

#### firestore:オブザーバーを設定してデータを取得する

- ドキュメント：<https://firebase.google.com/docs/firestore/query-data/listen>

```typescript
firebase.firestore().collection('CollectionName').doc('id').onSnapshot(
  (snapshot) => {
    if(snapshot.exists){
      const fieldValue = snapshot.get('FieldName');
    }
  },
  (error) => {}
);
```

- RDBとFirestoreのイメージ対応(個人の感想)

| Firestore | RDB |
| ---------- | ---- |
| collection | table |
| doc | record |
| field | column |

#### firestore:データを保存する

- ドキュメント：<https://firebase.google.com/docs/firestore/manage-data/add-data>

```typescript
firebase.firestore().collection('CollectionName').doc('id').set({
  field1: 'field1',
  field2: 'field2'
})
```

- 補足：setを使うと、ドキュメントがなければ作成し、ドキュメントがあれば上書き更新する。他にもupdate等があり、挙動が若干違う。

#### storage:ファイルをアップロードする

- ドキュメント：<https://firebase.google.com/docs/storage/web/upload-files>

```typescript
firebase.storage().ref().child('親フォルダ').child('子フォルダ').child('ファイル名').put(file).then((snapshot) => {
  const url = await snapshot.ref.getDownloadURL(); // ファイルのURL。
})
```