React & Firebaseを使ったWebサービス開発入門

## 開発経緯
1年ほど開発から遠ざかっていたので、勘を取り戻すために何か作ってみようということになったところ
TwitterでNetflixクローンの開発記事が注目を浴びており、これなら簡単に作れそうだし向こうは有料で公開してるっぽいから、無料にして出したらインプも稼げそうだ
という発想に至ったわけです。
実際やってみるとWeb開発の基本機能を網羅できるだけでなく、Reactの根本にある動きなども学習できるため、なんとなくの理解の向こう側に行ける優良素材だったのです。
(注)上述の方の記事は買ってないので、他人の有料記事を無料で転載しているわけではないのでご安心を！

## ターゲット読者
- Reactの初学レベルを終えた方
- 文法はマスターしたがデータベース接続などができてない方
- 久しぶりにWeb開発をするので勘を取り戻したい方

## サービス概要
世界中の映画情報を扱うAPIを用いて、旬の映画やNetflixオリジナルな映画をピックアップしてオススメしてくれる「映画紹介サービス」
紹介文は勿論、トレイラー動画もすぐに視聴可能！
ユーザー毎にお気に入りリストを登録することもできる！

## 下準備
### TMDBについて
TMDbとは映画やTV作品のデータベースを提供するWeb APIで、作品のタイトルやポスター画像、出演者、スタッフ以外に、ユーザーによる評価やレビューなどもサイト上で公開しており、それらの情報もAPIとして利用することが可能です。また、一部日本語でも提供されています。

**簡単Webサービスの開発にこれほどうってつけなAPIは他にあるのか！！**

#### APIキー取得方法
TMDBホームページからアカウントを作成

![tmdb-api_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659630/70d66654-474c-2e0d-2c9c-a3a9570ae036.png)

アカウント作成後、右上のプロフィールアイコンから Profile and Settings > Settings を開き、サイドメニューにある「 API 」をクリックして申請！

![tmdb-api_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659630/55bd1196-8dee-675f-d27f-cdfcc7e8b5b9.png)

### Postmanの利用方法
昨今のWeb開発の現場でAPIを使わないことはほぼあり得ず、何なら自社でAPIも開発したりするので、ちょうどいい機会なので「Postman」というAPI開発サポートツールでTMDB APIのテストをしてみましょう。

PostmanはWeb APIの設計・開発・テストをサポートするツールで、Googleアカウントがあれば無料で利用を始められる。

![スクリーンショット 2024-05-10 15.39.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659630/b2e7c02d-be83-c4bc-f432-7885ef49857f.png)

このようにGETのフォームにAPIのエンドポイント(APIにアクセスするURL)を書いてSendしてみると下にレスポンスが返ってくる流れだ。めっちゃ簡単！
これでAPIデータの構造など簡単にわかるため、開発も捗りそう。

TMDBの[APIドキュメント](https://developer.themoviedb.org/reference/intro/getting-started)を見ながら実験してみよう！

### Firebaseの立ち上げ
2010年代後半のSaaSブームの火付け役であり、スタートアップ企業Web開発に欠かせない存在となったクラウドサーバーサービス。
AWSがやはりシェア一位であるが、正直機能が多いのと自由度が高すぎてスタートダッシュには向かない。
そこで今回はサーバーとデータベースおよびログイン機能が一体となったハッピーセットのようなクラウドサーバーGoogleの「Firebase」を使う。

プロジェクトの立ち上げの手順を以下にまとめるが、簡単！楽チン！


まずFirebaseにGoogleアカウントを使ってログインする
![1-640x356.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659630/c1be5dcd-2f23-a289-0879-b2ec744d0b97.png)

次にプロジェクト名を入力して作成
![スクリーンショット 2024-05-10 15.53.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659630/132c7d38-4af4-40fb-bc8c-9583389dd774.png)

するとどうでしょう、もう出来た！
![スクリーンショット 2024-05-10 15.53.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659630/0c1b2aac-a33f-cc2b-3955-2b7d5511ab99.png)

早すぎてトイレに行く暇もなかった。

### Reactプロジェクト立ち上げ
これはこの記事の対象読者の皆さんはご存知だと思うので割愛しますが、
```
npx create-react-app {プロジェクト名} --template typescript
```
プロジェクトを置きたいディレクトリでこれを実行すればいいだけですね。(勿論、ReactやNode.jsがインストール済みであることが前提)

しかし、なぜか今回私の作ったプロジェクトはtypescriptが反映されてなかったので、JavaScriptで開発しました。まー、分家と宗家だし大目に見ていただければ。

### Githubによる開発
個人開発であれば、いや個人開発でも今大抵の人がGithubを使っていると思うので、ここでも当たり前な注意事項を書いときます。

Web開発、特にAPIを使った開発においてはAPIキーを絶対に載せないように気をつけましょう。
あとReact開発ではnode_modulesもGitに載せるとかなり面倒なので気をつける。
これらの対処法は簡単で「.gitignore」を書くだけ
```
#firebase
.firebase/*.cache
.emulator
firebase-export-*/

# dependencies
node_modules

# testing
coverage

# production
build

# misc
.DS_Store
/.env
*.idea
features.html

npm-debug.log*
yarn-debug.log*
yarn-error.log*

*.log
```
まー、例えばこんな感じ
ちなみに今回使うAPI「TMDB」と「Firebase」について
Firebaseのキーは絶対に載せてはいけないが、「TMDB」は正直載せてもそんなに問題はない
実際私のGithubには書いてあるので、暇な人は見つけてみては？

#### これで下準備は終了したのでいよいよ開発に移行する

## 開発開始

### API周りの実装
まずTMDB APIを叩くところを作っていく、これらは使いやすさと保守のしやすさを兼ねてモジュール化する。
src下にutilフォルダを作成
```request.js
const apiKey = 'Your API Key'

const requests = {
  fetchById: `/movie/movieId?api_key=${apiKey}&language=ja-JP`,
  fetchTrending: `/trending/all/week?api_key=${apiKey}&language=ja-JP`,
  fetchNetflixOriginals: `/discover/tv?api_key=${apiKey}&witg_network=213`,
  fetchTopRated: `/movie/top_rated?api_key=${apiKey}&language=ja-JP`,
  fetchActionMovies: `/discover/movie?api_key=${apiKey}&with_genres=28`,
  fetchComedyMovies: `/discover/movie?api_key=${apiKey}&with_genres=35`,
}

export default requests
```
これらはNetflix作品の情報を取ってきたり、トレンド映画の情報を取ってきたりと機能毎に分かれたAPIエンドポイントの配列だ。

次に同フォルダ内に
```movieApi.js
import axios from 'axios'

const movieApi = axios.create({
  baseURL: 'https://api.themoviedb.org/3',
});

export default movieApi;
```
axiosを用いたAPI情報の取得関数を定義。axiosはnpmやyarnでインストールしてくる。
これのメリットはrequestsのエンドポイントを引数として渡すだけで簡単にAPIにアクセスできることだ。
実際の使用例は後述する。

### メインページの実装
この画面ではNetflix作品・トレンド作品・往年の人気作品など5つのジャンルで映画を取得するため、5つの同じレイアウトが使われる。
このため、これらを<Row>としてコンポーネント化するといいだろう。

まずApp.js(メイン画面)を記述する
```App.js
import './App.css';
import Row from './component/Row';
import Front from './component/Front';

import requests from './util/requests';
import { Helmet } from 'react-helmet-async';
import { Link, useNavigate } from "react-router-dom";


const App = () => {

  return (
      <div className="app"> 
        <Helmet>
          <title>Movie Guide</title>
        </Helmet>
        <Front />
    
        <h1 className="app_description">Movie Guideへようこそ、あなたの次に見てみたい映画を探して行ってくださいな</h1>
    
        <Row title='Netflixオリジナル' fetchUrl={requests.fetchNetflixOriginals}/>
        <Row title='今話題' fetchUrl={requests.fetchTrending} />
        <Row title='大人気' fetchUrl={requests.fetchTopRated} />
        <Row title='アクション映画' fetchUrl={requests.fetchActionMovies} />
        <Row title='コメディ映画' fetchUrl={requests.fetchComedyMovies} />
    
      </div>
  );
}

export default App;
```
見た目のイメージはこのようだ
![スクリーンショット 2024-05-10 16.25.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659630/9cbce194-16f5-9579-50a8-7cb4b9050dbd.png)

上のコードを見てわかると思うが大画像は<Front>が一覧画像は<Row>で表示している。
そして、それぞれのpropsにはtitleと**APIのエンドポイント**を投げている。

"Helmet"に関してはtitleなどページのメタデータを扱うパッケージなのでインストールして適宜使ったらいいと思う。
あと、どうせ使うので"react-router-dom"もインストールしとこう。

### Row要素の実装
それでは映画一覧を並べて表示するRowの部分の実装を始める。
あと今回はあくまでReactの記事なのでCSSについては別で調べながら調整していただきたい。
```Row.js
import '../App.css';

import { useState, useEffect } from 'react';
import api from '../util/movieApi';
import YouTube from 'react-youtube';
import movieTrailer from 'movie-trailer';

// ポスターイメージを表示するためにパスの元となる部分
const posterBaseUrl = 'https://image.tmdb.org/t/p/original';

const Row = ({ title, fetchUrl }) => {
  const [movies, setMovies] = useState([]);
  const [trailerUrl, setTrailerUrl] = useState('');

  useEffect(() => {
    // useEffect自体ではasyncの関数を受け取れないので内部で関数を定義して呼び出す
    const fetchData = async () => {
      const request = await api.get(fetchUrl);
      setMovies(request.data.results);
    };
    fetchData();
  }, [fetchUrl]);

  const handleClick = (movie) => {
    if (trailerUrl) {
      setTrailerUrl('');
    } else {
      movieTrailer(movie?.name || movie?.title)
        .then((url) => {
          const urlParams = new URLSearchParams(new URL(url).search);
          // YouTube URLはこのような形になっている「https://www.youtube.com/watch?v=4dTzktjYIx4」
          setTrailerUrl(urlParams.get('v'));
        })
        .catch((error) => console.error(error.message));
    }
  }

  return (
    <div className="row">
      <h2>{title}</h2>

      <div className="row_posters">
        {movies.map(movie =>(
          <img 
            key={movie.id}
            onClick={() => handleClick(movie)}
            className="row_poster"
            src={posterBaseUrl+movie.poster_path} 
            alt={movie.name}
          />
        ))}
      </div>
      {trailerUrl && <YouTube videoId={trailerUrl} />}
    </div>
  );
}

export default Row;
```
コードを読めばやってることは大体わかるので、あまり解説も必要ないと思うが、
useEffectでレンダリング時にAPIを叩き(api.get(fetchUrl)の部分)、useState要素のmoviesにセットする。

この時、apiはmovieApi.jsでexport defaultで定義しているため、{}は必要なく、
この関数は**非同期処理**であるため、awaitをつけないとundefinedがmoviesにセットされる異常事態が発生する。

そして、気になる映画をクリックした時に表示されるトレイラー動画の部分であるが、
movieTrailerモジュールとYouTubeモジュールを組み合わせる。
movieTrailerモジュールはキーワードを渡すことでYouTubeから該当動画のURLを渡すもので、YouTubeモジュールはそのURLを実際に動画として表示するものである。

**if (trailerUrl)**で存在確認するなど細かいところも抜かりなく！

### Front要素の実装
では、メインページのど真ん中の要素を記述していきたい。
```Front.js
import '../App.css';

import { useState, useEffect, useRef } from 'react';

import api from '../util/movieApi';
import requests from '../util/requests';
import YouTube from 'react-youtube';
import movieTrailer from 'movie-trailer';


const Front = (props) => {
  const [movie, setMovie] = useState();
  const [trailerUrl, setTrailerUrl] = useState('');

  const watchRef = useRef(null);

  useEffect(() => {
    const fetchData = async () => {
      const request = await api.get(requests.fetchTopRated);
      const frontMovie = request.data.results[Math.floor(Math.random() * request.data.results.length - 1)];

      setMovie(frontMovie);
      return frontMovie;
    };

    fetchData()
    }, []);

  const handleView = (movie) => {
    if (trailerUrl) {
      setTrailerUrl('');
      watchRef.current.textContent = "視聴";
    } else {
      watchRef.current.textContent = "閉じる";
      movieTrailer(movie?.name || movie?.title)
        .then((url) => {
          const urlParams = new URLSearchParams(new URL(url).search);
          setTrailerUrl(urlParams.get('v'));
        })
        .catch((error) => console.error(error.message));
    }
  }

  return (
    <header 
      className="head" 
      style={{
        backgroundSize: "cover",
        backgroundImage: `url(${'https://image.tmdb.org/t/p/original/'+movie?.backdrop_path})`,
        backgroundPosition: "center",
      }}>
      <div className="head_contents">
        <h1>{movie?.name || movie?.title}</h1>
        <div className="head_buttons">
          <button 
            className="head_button" 
            onClick={() => handleView(movie)}
            ref={watchRef}>
            視聴
          </button>
        </div>
        <p className="head_description">{movie?.overview}</p>
      </div>
      <div className="head_video">
        {trailerUrl && <YouTube videoId={trailerUrl} />}
      </div>
    </header>
  );
}

export default Front;
```
Rowコンポーネント同様にuseEffect内で映画情報を取得するが、ランダムで選び出した特定の一枚を全画面で表示するため、配列の指定(Math.floor(Math.random() * request.data.results.length - 1))を工夫する。
ちなみに全画面表示はCSSのstyleで{backgroundImage}を使うことで実現する。

## デプロイ
Firebaseのデプロイは思ってるより難しくはないが、所々細かい注意点などがある。

#### まずFirebaseパッケージのインストール
これがないとFirebaseコマンドが叩けない
```
npm install firebase
```

#### 次にFirebase Toolsのインストール
パーミッションエラーが出ることがあるので、その場合「sudo npm install -g firebase-tools」を入力
```
npm install -g firebase-tools
```

#### コマンドからFirebaseにログインする
```
firebase login
```

#### Firebaseのプロジェクト初期設定
デプロイするディレクトリを選択するところだけ「build」に設定してあとは適宜説明文を読みながら設定
```
firebase init
```

#### 開発プロジェクトをBuildする
このコマンドによりbuildフォルダが作られ、先に設定したFirebaseでデプロイ時にそれを参照する。つまり、ユーザーが実際に見る画面はbuildフォルダなのである。
```
npm run build
```

#### いよいよデプロイ！
アップデートしたアプリをデプロイする場合「npm run build」と「firebase deploy」を繰り返すが、アップデート後のアプリがうまく表示されない場合、ブラウザのキャッシュを削除する
```
firebase deploy
```

## 総括
以上がReactで作る簡単Webサービスであるが、Web開発において上達のためには何でも手を動かして作ってみることが大切である。
次回はWebサービスの最も需要な技術である「データベース」と「ログイン」を交えて開発していく。