🏢

脱 create-react-app! webpackでReact ×TypeScrip開発環境構築からHello World!まで

2022/03/13に公開
5

まえおき

この記事では、なんとな〜くゆる〜く webpack を理解する記事となっています。
2022 年 3 月からエンジニアとしてキャリアをスタートして、業務でマニュアルでの環境構築が必要になったのでこの記事を書こうと思いました。

脱 create-react-app

みなさんはcreate-react-appせずに React の開発環境を構築したことがありますか?
私は、ありませんでした。

実際の開発現場(業務)では、create-react-appでプロジェクトを始めることが無いそうです。(わたしの周りの場合では。)

create-react-appはコマンド一発で React の開発環境を構築できて便利ですが、webpack の設定が隠蔽されるため、の設定をいじくり回すにはejectする必要があります。
ejectはひと手間かかるうえ、公式では推奨していないようです。

それだったら、マニュアルで環境構築をしてしまおう、という話です。

そもそも webpack とは

ひとことで言うと、.jsファイルや.cssファイル、.jpg.pngなどの画像ファイルなどプロジェクトに必要なあらゆるファイルをひとつの.jsファイルにまとめる(バンドルする)ツールです。
いわいるモジュールバンドラーと呼ばれるヤツです。

マニュアルで環境構築するメリット

create-react-appは React の開発環境をワンコマンで用意できるので便利ですが、その反面デメリットも存在します。

一番のデメリットといえば、webpack(モジュールバンドラ) や Babel(トランスパイラ) といった、React アプリケーションの裏側で動いているライブラリの設定ができないことでしょうか。
誤解が無いように言うと、このデメリット自体がcreate-react-appの最大のメリットでもあります。
なぜなら、面倒な細かな設定を気にせずに、アプリケーションの開発に集中できるからです。
ただし、create-react-appでは対応しきれない裏側の設定を変更したい場合はデメリットになりうります。

その他にもいくつかデメリットは存在しますが、今回の記事の内容から若干それてしまいますので、ここまでにさせてください。
他のデメリットについてはコメント欄にて、k-satoさんとハトすけさんがご紹介されているので、そちらを参考にしていただければと思います。

不要なパッケージをインストールしないで済むとか、package.jsonがスッキリするとかでしょうか。

環境構築を始め!まずはパッケージのインストールをしよう

今回は以下のようなディレクトリ構成になっています。

root/
  ├ dist/
  │ ├ index.html
  │ └ main.js
  │
  ├ node_modules/
  │
  ├ src/
  │ ├ App.tsx
  │ └ index.tsx
  │
  ├ package.lock.json
  ├ pacckage.json
  ├ tsconfig.json
  └ webpack.config.js

npm initするよ

まずは初期化処理から。
適当なディレクトリを用意してターミナルで以下のコマンドを実行します。

npm init

実行後、ターミナルに色々質問されるので、とりあえず Enter を連打 👇👇👇
npm init時の設定は省略させていただきます。
(とりあえず連打で大丈夫なはず…)

npm initの設定について知りたい方はググるなどして参考にしてみてください。
私は、下記の Udemy の講座で勉強しました。

https://www.udemy.com/course/introductory-nodejs/

React をインストール

まずは以下のコマンドで React をインストールします。
パッケージはプロジェクトフォルダのnode_modulesディレクトリにインストールされ、package.jsondependenciesにパッケージ名が記載されます。

dependencies
npm install react react-dom @types/react @types/react-dom
dependencies にインストールするパッケージ一覧
  • react
  • react-dom
  • @types/react
  • @types/react-dom

TypeScript と webpack 周りのパッケージをインストール

TypeScript と webpack 周りのパッケージをインストールします。
npm installの後にオプションコマンド--save-devをつけてパッケージ名を記述します。
--save-devをつけることで、開発環境だけでパッケージを使うことができます。

devDependencies
npm install --save-dev webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env @babel/preset-react typescript ts-loader sass css-loader style-loader sass-loader
devDependencies にインストールするパッケージ一覧
  • webpack
  • webpack-cli
  • webpack-dev-server
  • babel-loader
  • @babel/core
  • @babel/preset-env
  • @babel/preset-react
  • typescript
  • ts-loader
  • sass
  • css-loader
  • style-loader
  • sass-loader

--save-devの詳細は、過去に書いた以下の記事を参考にどうぞ!

https://zenn.dev/hrkmtsmt/articles/5f4a0e5c79b77a

webpack の設定をしよう

パッケージのインストールが完了したら、webpack の設定をしていきます。
ルートディレクトリに webpack の設定ファイルwebpack.config.jsを作成します。

touch webpack.config.js

まずは、ファイルに以下のコードを書いて設定の下準備をします。

webpack.config.js
const path = require('path');

module.exports = {};

プロパティの設定

module.exportsオブジェクトにプロパティを書いていきます。
今回は、以下のプロパティを設定します。

  • mode
  • entry
  • output
  • module
  • devServer
  • resolve
  • target
  • perfomance

module.exportsの中にプロパティを書いていくイメージです。

webpack.config.js
const path = require('path');

module.exports = {
  mode,
  entry,
  output,
  module,
  devServer,
  resolve,
  target,
};

mode

modeはバンドルするファイルに影響するプロパティです。
モードの値は 3 種類あります。

  • production (デフォルト値)
  • development
  • none

productionはバンドしたコード内の改行やインデント、余分な半角スペースなどを取り除いてバンドルします。
developmentは改行やインデントはそのままに、バンドルします。

今回は、デプロイする予定もないので値にdevelopmentを設定します。

webpack.config.js
const path = require('path');

module.exports = {
 mode: 'development',
  // ...
};

entry

entryはどのファイルを起点にバンドルするかを設定するプロパティです。
ひとつのファイルからインポートされたモジュールをたどってバンドルしてくれます。

今回はsrcディレクトリのindex.tsxを起点とします。

webpack.config.js
const path = require('path');

module.exports = {
  // ...
  entry: './src/index.tsx',
  // ...
};

output

outputはどのディレクトリにバンドルしたファイルを出力するかを設定するプロパティです。

今回は、distディレクトリmain.jsというファイル名でバンドルしたファイルに出力するように設定します。
出力先のパスはoutput.pathに、出力するファイル名はoutput.filenameに値を渡します。

webpack.config.js
const path = require('path');

module.exports = {
  // ...
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js',
  }
  // ...
};

module

modulesbabel-loadercss-loaderといった Loader の設定を行うプロパティです。
Loader の設定を細かく書かないといけないため、コードがネストしまくります。
一見「ウェッ」っとなりがちですが、ひとつづず見ていけば意外と「こんなもんか」と思えるようになるかもしれません。
個人的には、取り扱う Loader の多さと、コードの読みづらさかが際立っているプロパティだなぁ、と思っています。

module.rules

基本的にmodule.rulesの使い方・コードが読めるようになれば webpack 初心者は脱出できるのではないかと思います。

module.rulesは配列です。配列の中に、TypeScript や CSS の Loader を設定するオブジェクトを渡すことで、設定できます。

イメージ的には下記になります。

webpack.config.js
const path = require('path');

module.exports = {
  // ...
  module: {
    rules: [
      { /* TypeScriptのモジュール */ },
      { /* CSSのモジュール */ },
    ],
  },
  // ...
};

まずは、TypeScript の Loader から書いて行きましょう。
基本的に、オブジェクトの中にtestuseを設定すれば Loader が動くようになります。

testには、正規表現でターゲットとなる拡張子を書きます。どのファイルを処理の対象とするかといった設定です。

useには、使用する Loader を書きます。配列になっており、その中に Loader を文字列もしくはオブジェクトで渡します。
Loader は実行順にルールがあります。配列の後ろから処理が行われるので、順番に気をつけながら設定しましょう。

TypeScript の例で言うと、TypeScript から JavaScript にコンパイルして、JavaScript から ES5 にトランスパイルしたいので、配列の最初にbabel-loaderを設定し、次にts-loaderを設定することで、ts-loaderbabel-loaderの順で実行できるようになります。
この順番が逆だとコンパイルエラーが発生するので注意しましょう。

webpack.config.js
{
  test: /\.(ts|tsx)$/,
  use: [
    {
      loader: 'babel-loader',
      options: { presets: ['@babel/preset-env', '@babel/react'] },
    },
    {
      loader: 'ts-loader',
      options: {
        configFile: path.resolve(__dirname, 'tsconfig.json'),
      },
    },
  ]
}

続いて、サクッと SCSS の Loader も設定しましょう。
こちらも、useに設定するローダーの順番に気をつけましょう。

webpack.config.js
{
  test: /\.scss$/,
  use: [
    {
      loader: 'style-loader',
    },
    {
      loader: 'css-loader',
    },
    {
      loader: 'sass-loader',
    },
  ]
}

ちなみに上記の様に Loader にオプションを渡さない場合は以下の様に省略して記述することもできます。

webpack.config.js
{
  test: /\.scss$/,
  use: ['style-loader', 'css-loader', 'sass-loader']
}

devServer

devServerは、webpack-dev-serverの設定を行うプロパティです。
webpack-dev-serverは webpack で開発サーバーを立ち上げることができるライブラリです。コードを更新すると自動的にビルドしてブラウザのビューが更新されるので便利です。

devServer.static.directoryにはサーバーの起点となるディレクトリを書きます。
パスはoutput.pathと同様で構いません。

portにはポート番号を指定できます。指定したポートでサーバーが立ち上がります。

webpack.config.js
const path = require('path');

module.exports = {
  // ...
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    port: 3000,
  },
  // ...
};

resolve

resolveは、インポート時にのパスの問題(絶対パスや相対パス)を解決するプロパティです。
resolve.extensionsに、拡張子を文字列として配列に渡すことで、インポートのパスに書く拡張子を省略できます。

webpack.config.js
const path = require('path');

module.exports = {
  // ...
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.json'],
  },
  // ...
};

target

targetは、サーバー側(Node.js)とブラウザ側(フロント)どちらにコンパイルするかを設定するプロパティです。
サーバー側であればnode、ブラウザ側であれば'web'をと書きます。

今回はフロントエンドの開発するのでwebを設定します。

webpack.config.js
const path = require('path');

module.exports = {
  // ...
  target: 'web',
};

webpack の設定完了

これで、一通り webpack の設定は完了です。
コードをまとめると、以下の用になります。

webpack.config.js
const path = require('path');

module.exports = {

  mode: 'development',
  entry: './src/index.tsx',
  entry: './src/index.tsx',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js',
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: [
          {
            loader: 'babel-loader',
            options: { presets: ['@babel/preset-env', '@babel/react'] },
          },
          {
            loader: 'ts-loader',
            options: {
              configFile: path.resolve(__dirname, 'tsconfig.json'),
            },
          },
        ],
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      },
    ],
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    port: 3000,
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.json'],
  },
  target: 'web',
};

Hello World!するよ

いよいよゴール目前です。
まずは、プロジェクトのビルドをするために必要なディレクトリとファイルを用意します。

distディレクトリを作成してその中にindex.htmlを作成します。
このディレクトリは、webpack.config.jsoutput.pathdevServerに記述したディレクトリです。

mkdir dist
touch dist/index.html

index.htmlには以下のコードを記述します。
コードの詳細は省きます。

index.html
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack × React × TypeScript</title>
  </head>
  <body>
    <div id="root"></div>
    <script defer src="main.js"></script>
  </body>
</html>

React を書いてく

プロジェクトのルートディレクトリにsrcディレクトリを作成して、その中にindex.tsxApp.tsxを作成します。

mkdir src
touch src/index.tsx
touch src/App.tsx

まずはApp.tsxのコードを書きます。

App.tsx
import React from 'react';

export const App: React.VFC = () => {
  return <div>Hello World!</div>
}

次にindex.tsxApp.tsxコンポーネントをインポートします。

index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from '../src/App';

ReactDOM.render(<App />, document.getElementById('root'));

TypeScript の設定をしよう

TypeScript の環境を構築します。
プロジェクトのルートディレクトリにて以下のコマンドを実行してを作成します。

touch tsconfig.json

tsconfig.jsonに以下のコードを記述すれば OK です。
細かいことを省いて設定を要約すると、「TypeScript で書かれたコードは ES2020 にコンパイル、JSX で書かれたコードは React のコードにコンパイルしますよ」的な内容です。

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "target": "ES2020",
    "module": "ES2020",
    "outDir": "dist",
    "jsx": "react",
    "moduleResolution": "Node",
    "lib": ["ES2020", "DOM"],
    "allowJs": true,
    "allowSyntheticDefaultImports": true
  },
  "exclude": ["./node_modules"]
}

tsconfig.jsonの各オプションは以下のリンクにまとめらているので、そちらを参照ください。

https://www.typescriptlang.org/ja/tsconfig

https://qiita.com/ryokkkke/items/390647a7c26933940470

package.json の scripts の設定を設定して Hello World!してみる

package.jsonscriptsに以下のコマンドを登録します。

buildは、npm run buildをすることでdistをディレクトリビルドしたファイルmain.jsが生成されます。
devは、npm run devをすることでwebpack-dev-serverが立ち上がります。http://localhost:3000にアクセスすることでビルドの結果を見ることができます。

package.json
{
  //  ...
  "scripts": {
    //  ...
    "build": "webpack",
    "dev": "webpack serve --open"
  },
  //  ...
}

さて、ひと通り Hello World!までの設定が完了したので、ターミナルでプロジェクトのルートディレクトリにいる事を確認して、以下のコマンドを実行しましょう。

npm run dev

するとブラウザに Hello World!が表示されると思います。

お疲れさまでした。
もっと開発を楽しんでいきましょう!
さいなら~ 👋

Thanks

https://webpack.js.org/configuration/

https://www.webdesignleaves.com/pr/jquery/webpack_basic_01.html

https://www.udemy.com/course/introductory-nodejs/

https://www.typescriptlang.org/ja/tsconfig

https://qiita.com/ryokkkke/items/390647a7c26933940470

GitHubで編集を提案

Discussion

k-satok-sato

正直な事をいうと、私はcreate-react-appとマニュアルで開発環境を構築する真のメリット・デメリットをあまり理解していません。

create-react-appでプロダクションで回す様なアプリを開発する際のデメリットとして考えられるのは主に下記の2点でしょうか🤔🤔?(絞り出せば他にもありそうですかね)

  • (1) 重要な設定部分がブラックボックス化してしまう: 本来しっかりと理解して運用すべきであるバンドルの設定の箇所が見えなくなり、カスタマイズ等がやりづらい
  • (2) 不必要なdependenciesが生まれる(バンドルサイズが膨らむ): CRAは内部で色々なdependenciesを持ち、本来自分たちのアプリには必要のないものがインストールされてたりしているかと思います。上の通りカスタマイズが大変なので、これを抜くのも一苦労となりそうです。(create-react-appのwebpackの設定)(多分これだと思います。間違っていたらごめんなさい🙇‍♂️🙇‍♂️)

ただ頭の良い方々が考えて作ってくれているので、Webpackの設定をよく理解せず、"プロダクションで使うアプリだから絶対独自のWebpack設定が必要だ!!"、よりもCRAを使わせてもらった方が楽で安心だったりする様な気もします☺️☺️☺️

参考

Hiroki MatsumotoHiroki Matsumoto

詳しい内容を教えて頂きありがとうございます🙇‍♂️

なるほど、CRAのデメリットは「重要な部分がブラックボック化」と「不必要なdependenciesが生まれる」の2点が主な理由なんですね❗️

ご共有頂いた記事も拝見しましたが、Don't use create-react-app: How you can set up your own reactjs boilerplate."Disadvantages of CRA" にもまとめられていますね‼️

ただ頭の良い方々が考えて作ってくれているので、Webpackの設定をよく理解せず、"プロダクションで使うアプリだから絶対独自のWebpack設定が必要だ!!"、よりもCRAを使わせてもらった方が楽で安心だったりする様な気もします☺️☺️☺️

確かに。場合によってはCRAにすべてを委ねて安心を得るのも良いかもしれませんね☺️

create-react-appのwebpackの設定のソースコードもザーッと見ましたが、webpackの設定がとても複雑で、カスタマイズが大変そうだと感じました🤔
正直、細かい部分までは理解できませんでした😂

ハトすけハトすけ

こんにちは、webpackのとても詳しい記事ありがとうございます。

業務でcreate-react-app使ってるので、その立場からの意見です。参考程度に。

実際に使ってて不便に思うことは、jestがcreate-react-appの本体であるreact-scriptsに依存することですね。最近v5がでてきましたが、v4時代はjestはv26.6しか使えませんでした。

jestはv27系だとeach文で、以下のように$名前付きでコメント内でアクセスできる便利な機能があります。

describe.each([
  {a: 1, b: 1, expected: 2},
  {a: 1, b: 2, expected: 3},
  {a: 2, b: 1, expected: 3},
])('.add($a, $b)', ({a, b, expected}) => {
  test(`returns ${expected}`, () => {
    expect(a + b).toBe(expected);
  });
});

v26系だとこの機能が提供されていないのでわざわざdescribeをもう一回ネストしてました。

describe.each([
  {a: 1, b: 1, expected: 2},
  {a: 1, b: 2, expected: 3},
  {a: 2, b: 1, expected: 3},
])('test each condition', ({a, b, expected}) => {
  describe(`.add(${a}, ${b})`, () => {
    test(`returns ${expected}`, () => {
      expect(a + b).toBe(expected);
    });
  })
});

微妙な例ですがreact-scriptsでできないなら、ejectしたくないかつ、react-app-rewiredのような拡張もしたくないので、僕は諦めています。

create-react-app使ってる人はwebpack扱いたくないかつ、react-app-rewiredのようなハック的な機能も保守したくない人が多いイメージですね。長文すみません。

Hiroki MatsumotoHiroki Matsumoto

別角度からの情報ありがとうございます🙇‍♂️

jestはreact-scriptsに依存しているんですね❗
新しい機能が使えないのは若干不便ですね🤔

create-react-app使ってる人はwebpack扱いたくないかつ、react-app-rewiredのようなハック的な機能も保守したくない人が多いイメージですね。長文すみません。

やはり、面倒な設定を省けるのは大きそうですね😁
webpack周りのパッケージの保守も大変ですよね😅
どちらも一長一短といった感じですね🤔

ejectの他にもreact-app-rewiredでwebpackの設定を上書きできるんですね、知りませんでした❗

フシハラフシハラ

とても参考になりました。ありがとうございます。
よくある、reactをejectして~だと設定項目が膨大でどこから読めば良いのかわからなくて途方に暮れていましたが
必要最低限の情報があって理解がすすみました。