Reactの環境構築を段階的にやってみる

# はじめに
勉強目的なのでCreate React App相当の設定を実施するわけではないです！
参考にされる際はこの点ご留意下さい。

# モチベーション
Create React AppはサクッとReactアプリの雛形が作成できて便利ですよね。
Reactの環境構築が複雑だからこそ、このようなツールが提供されているのだと思います。
ただ、処理過程を理解しないのは些か不安でもあります。
段階的に実施すればそこまで難しくないのでは？と思い立ち、勉強を兼ねて０からの構築を試してみました。

# STEP 0 : Reactをインストールする
プロジェクトの初期化が済んでいない場合は、まず`npm init`コマンドで初期化しておいて下さい。

```
npm i react react-dom
```

# STEP 1 : Webpackでのバンドルを試す

まずはWebpackでのバンドルを試してみます。

```
npm i -D webpack webpack-cli
```
JSファイルのバンドルであればローダーは必要ありません。
そして、React自体はJSで記述できるので、この段階でReactアプリがバンドルできます。
エントリーポイントの`index.js`とReactコンポーネントの`App.js`をsrc配下に作成していきます。

```js:src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

const rootEl = document.getElementById('root')
const rootComponent = React.createElement(App)

ReactDOM.render(rootComponent, rootEl)

```

```js:src/App.js
import React from 'react'

const App = () => React.createElement('h1', null, 'React App')

export default App
```

`package.json`にビルド用のスクリプトを書きます

```json:package.json
"scripts": {
  "build": "webpack --mode=production"
}
```

`npm run build`を実行すると、distディレクトリ配下に`main.js`が作成されます。
distディレクトリに`index.html`を作成して`main.js`を読み込んでみます。

```html:dist/index.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React App</title>
</head>

<body>
  <div id="root"></div>
  <script src="main.js"></script>
</body>

</html>
```

そして`index.html`を直接ブラウザで開き、React Appと表示されていればOKです。

エントリーポイントが`src/index.js`出力先が`dist/main.js`というのはwebpackのデフォルト設定です。なので、webpackの設定ファイルを作成せずともバンドルができました。
とはいえ、流石にJSXなしは辛いのでJSXで記述できるようにします。

# STEP 2 : JSXでの記述を試す
JSXは標準のJSではないので、Babelを通して変換します。
まずは必要なパッケージをインストール

```
npm i -D @babel/core @babel/preset-env @babel/preset-react babel-loader
```

Babelの設定ファイルを作成し、使用するプリセットを明記します。
後ろに記載したプリセットから処理されるので、配列の順番に意味がある点に注意します。

```js:babel.config.js
module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react']
}
```

Webpackの設定ファイルを作成し、Babelを実行するタイミング(babel-loader)を明記します。
また、jsx拡張子はデフォルトで省略できないので、省略できるようにこちらも明記します。

```js:webpack.config.js
const path = require('path')

// 出力先は絶対パスで記載する
const outputPath = path.resolve(__dirname, 'dist')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: outputPath
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: ['babel-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  }
}
```

App.jsだけJSXファイルにしてみます。拡張子をjsxにしてから編集します。

```jsx:src/App.jsx
import React from 'react'

const App = () => <h1>React App JSX</h1>

export default App
```

`npm run build`を実行して、React App JSXと表示されればOKです。

JSXファイルで記述でき、かなりReactっぽくなりました。ただ`index.html`を直接ブラウザで参照してしまっているので、サーバー経由で参照するようにします。

# STEP 3 : 開発サーバーを試す
webpack-dev-serverをインストールして開発サーバーを導入します。

```
npm i -D webpack-dev-server
```

Webpackの設定ファイルに、開発サーバーの設定を追記します。

```js:webpack.config.js
const path = require('path')

// 出力先は絶対パスで記載する
const outputPath = path.resolve(__dirname, 'dist')

module.exports = {
  entry: './src/index.js',
  output: { /* 省略 */ },
  module: { /* 省略 */ },
  resolve: { /* 省略 */ },
  devServer: {
    contentBase: outputPath
  }
}
```

contentBaseにdistディレクトリを指定しているので、distディレクトリをルートにサーバーが起動します。

`package.json`にwebpack-dev-serverの実行コマンドを追記します。
（最新版だと`webpack-dev-server`でなく`webpack serve`で起動するようです）

```json:package.json
"scripts": {
  "build": "webpack --mode=production",
  "dev": "webpack serve --mode=development"
}
```

`npm run dev`でサーバーを起動し http://localhost:8080 にアクセスしてSTEP2と同様の結果になればOKです。
ちなみに、webpack-dev-serverはファイルの変更を検知してブラウザを自動リロードしてくれます。便利ですね。

ところで、dist配下に直接`index.html`を作成してしまったのですが、基本的にdist配下はバージョン管理しないはずです。
HTMLファイルの生成も、Webpack経由で処理するようにします。

# STEP 4 : HTML Webpack Pluginを試す

HTML用のプラグインをインストールします。

```
npm i -D html-webpack-plugin@next
```
src配下に`index.html`を作成します。
基本的にはSTEP1と同じですが、scriptタグはプラグインで自動挿入されるので削除しています。

```html:src/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React App</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>
```

Webpackの設定ファイルを修正します。
HTMLファイルのテンプレートを指定しつつ、devServerのcontentBaseもdistディレクトリに依存しなくなったので、コメントアウトしています。

```js:webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const outputPath = path.resolve(__dirname, 'dist')

module.exports = {
  entry: './src/index.js',
  output: { /* 省略 */ },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: { /* 省略 */ },
  resolve: { /* 省略 */ },
  devServer: {
    // contentBase: outputPath
  }
}
```

distディレクトリを削除し、開発サーバーを再起動します。distディレクトリは存在しませんが、STEP2と同様の内容が表示されるはずです。
`npm run build`を実行すると、distディレクトリが作成され、STEP２と同様のファイルが生成されます。

HTMLファイルが処理できたので、次はCSSファイルもWebpack経由で処理するようにします。

# STEP 5 : CSSローダーを試す

基本的には「JSでCSSをimportし、バンドル後のファイルに含める」という考え方です。
ただ、JSに含めたCSSをどのようにしてHTMLに反映させるかについては、選択肢があります。
そのため
　①JSでCSSをimportするためのローダー
　②JS内のCSSをHTMLに反映させる方法
それぞれ別のパッケージを使用する必要があります。
今回は、①`css-loader`②`style-loader`を使用します。
`style-loader`は取り込んだCSSをheadタグ内のstyleタグとして展開してくれます。

```
npm i -D css-loader style-loader
```
Webpackの設定ファイルを修正します。

```js:webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');

const outputPath = path.resolve(__dirname, 'dist')

module.exports = {
  entry: './src/index.js',
  output: { /* 省略 */ },
  plugins: [/* 省略 */],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  resolve: { /* 省略 */ },
  devServer: { /* 省略 */ }
}
```

useでは最後に定義したものから順番に処理されるため、必ず`css-loader`を後に記載する必要があります。
では、簡単なCSSファイルを作成し、`index.js`で読み込んでみます。

```css:styles.css
.tomato-title {
  color: tomato;
}
```

```jsx:src/App.jsx
import React from 'react'

const App = () => <h1 className="tomato-title">React App JSX</h1>

export default App

```

```js:src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

import './styles.css'

const rootEl = document.getElementById('root')
const rootComponent = React.createElement(App)

ReactDOM.render(rootComponent, rootEl)
```

タイトルがトマト色に変わっていたらOKです。

次は静的ファイルもWebpack経由で処理するようにします。

# STEP 6 : Asset Modulesを試す

静的ファイルの読み込みは`url-loader`や`file-loader`が使用できますが、Webpack5系から標準で静的ファイル用のtypeプロパティが追加されたので、そちらを使用してみます。

```js:webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const outputPath = path.resolve(__dirname, 'dist')

module.exports = {
  entry: './src/index.js',
  output: { /* 省略 */ },
  plugins: [/* 省略 */],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|svg|jpg)$/, // 他に必要な拡張子があれば追加
        type: 'asset'
      }
    ]
  },
  resolve: { /* 省略 */ },
  devServer: { /* 省略 */ }
}
```

標準だと、8kb以下のファイルはバンドルファイルに同梱(inline)、それ以上は別ファイルとしてdist配下に配置(resource)されるようです。

適当な画像をバンドルしてみます。

```jsx:src/App.jsx
import React from 'react'

import image from './image.png'

const App = () => (
  <>
    <h1 className="tomato-title">React App</h1>
    <img src={image} width="150" height="150" />
  </>
)

export default App
```

指定した画像が表示されればOKです。

以上で、Webpack経由でのバンドルは一通り設定できました。
最後に、このアプリをテストしてみたいと思います。

# STEP 7 : テストを試す

テストランナーは多くの種類がありますが、React公式が推奨しているJestを導入します。(Reactと同じFacebook製なので相性が良いです）

```
npm i -D jest
```

Jestはnode環境で動作する関係上、ESModules（import/export）が標準では使用できません。
しかし、Babelの設定ファイルを検出すると自動でBabelを通して実行してくれます。
STEP2でBabelの設定ファイルは作成しているので、簡単なテストを作って実行してみます。

```js:src/App.test.jsx
import React from 'react'
import { render } from 'react-dom'
import { act } from 'react-dom/test-utils'

import App from './App'

test('render content', () => {
  const container = document.createElement('div')
  document.body.appendChild(container)
  act(() => { render(<App />, container) })
  expect(container.textContent).toBe('React App')
})
```

App.jsx内で画像をimportしていますが、これはWebpackによって解決されるものです。Jestは画像importの処理方法を知らないので、このままだとエラーになります。
画像（ついでにCSSも）をimportするときはモックを使用するように、Jestの設定ファイルで指定します。

```js:jest.config.js
module.exports = {
  moduleNameMapper: {
    "\\.(png|svg|jpg)$": "<rootDir>/__mocks__/fileMock.js",
    "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
  }
}
```

モックを作成します。

```js:__mocks__/fileMock.js
module.exports = 'test-file-stub'
```

```js:__mocks__/styleMock.js
module.exports = {}
```

`package.json`にスクリプトを追加し、`npm test`で実行します。

```json:package.json
"scripts": {
  "build": "webpack --mode=production",
  "dev": "webpack serve --mode=development",
  "test": "jest"
}
```
テスト成功で終了すればOKです。

# おわりに

以上で、Reactの基本的な環境構築ができました。
各ステップでの細かい設定に関しては調査不足なところがあるので、今後の課題にしていきたいですが、全体の流れが俯瞰できて良い勉強になりました。