React の開発環境を create-react-app を使わずマニュアルで構築する

プログラミング

React の開発環境は Create React Appコマンドや Next.js のようなフレームワークを使用することで簡単に構築できるようになりました。

しかし、いくつかの問題が解決されないまま create-react-app は React の公式ドキュメントから外されてしまいました。
公式ではフレームワークの使用を強く推奨していますが、既存のプロジェクトに React を導入したい場合や、フルスタックな機能を必要としないユーザーにとって、Next.js などのフレームワークは少々オーバースペック気味であり、学習コストを上げてまで……と感じる方も少なくないのではないでしょうか。

ということで、この記事では React の環境構築を一から手動で行う手順を改めて詳しく解説いたします。
TypeScript の導入や ES モジュールへの移行も考慮に入れています。
パッケージ構成や設定などの基本的な知識をしっかりと把握しておくことで、次のステップにスムーズに進むことができるようになるかと思います。

なお、本記事ではトランスパイラには Babel を、モジュールバンドラには webpack を使用します。

node.js のインストールについては、以下を参照してください。

参考: Node.js のパッケージ管理ツール npm とは


必要パッケージのインストール
まずはプロジェクト用のディレクトリを作成し、Node.js 環境を初期化します。

>mkdir Sample
>cd Sample
>npm init -y

次に各種パッケージをインストールします。
はじめに React と TypeScript ですね。
これがないと始まりません。

>npm install react react-dom
>npm install typescript @types/react @types/react-dom --save-dev

react
React のコアライブラリです。

react-dom
ウェブブラウザ環境で React コンポーネントを DOM にレンダリングするためのライブラリです。

typescript
TypeScript 本体です。
実行時には必要ないので開発用としてインストールします。

@types/react
React のための型定義です。
実行時には必要ないので開発用としてインストールします。

@types/react-dom
react-dom ライブラリのための型定義です。
実行時には必要ないので開発用としてインストールします。


次に Babel 関連です。
React は JSX という JavaScript の拡張構文を使用しますが、Babel は JSX 構文を通常の JavaScript 関数呼び出しに変換します。
ランタイムコア以外は開発用としてインストールします。

>npm install @babel/core @babel/cli @babel/preset-env @babel/preset-typescript @babel/preset-react --save-dev
>npm install @babel/plugin-transform-runtime --save-dev
>npm install @babel/runtime-corejs3

@babel/core
Babel コンパイラのコア部分です。

@babel/cli
コマンドラインから Babel を実行するためのツールです。

@babel/preset-env
ES6 以上のコードを古い JavaScript エンジンが理解できるように変換するためのスマートプリセットです。
Babel 7.12.0 以降では ES2022 の機能としてトップレベルのawaitをサポートします。

@babel/preset-typescript
TypeScript コードを純粋な JavaScript に変換するためのプリセットです。

@babel/preset-react
React の JSX 構文を React.createElement 呼び出しに変換するためのプリセットです。
JSX 構文が JavaScript にトランスパイルされます。

@babel/plugin-transform-runtime
Babel のヘルパー関数とポリフィルを共通化し、コードの重複を減らすためのプラグインです。
生成されるバンドルのサイズを削減でき、グローバルスコープの汚染を避けることが可能です。

@babel/runtime-corejs3
@babel/runtime の拡張版で ES6 以上のサポートが含まれ、より広範な JavaScript 機能のポリフィルが可能になります。

この後、webpack やテストサーバをインストールしますが、それぞれ該当の項にて解説いたします。


TypeScript の設定
Babel とは直接関係ありませんが、TypeScript のコンパイラであるtscコマンドが実行できるように設定ファイルを編集します。
Babel はトランスパイル時に型チェックを行わないため、tscコマンドで事前に型検証を行うことをお勧めします。
もし IDE の型チェックだけで十分とお考えの方は、本項を読み飛ばしていただいても問題ありません。

まずは TypeScript 環境を初期化します。

>npx tsc --init

tsconfig.json という設定ファイルが出力されるので、以下の項目を編集・追加します。
(他の項目はそのままで良いです)

・・・
"compilerOptions": {
	"target": "es2022",
	"jsx": "react",
	"module": "esnext",
	"outDir": "./dist",
	"include": [
	  "src/**/*"
	]
・・・

target
出力される ECMAScript のバージョンです。
トップレベルawaitが実装されたes2022が良いでしょう。

jsx
JSX 構文をどのように扱うかを制御します。
この設定では、JSX をReact.createElement呼び出しに変換します。

module
出力されるモジュール形式です。
targetと同じくes2022で良いかと思います。

outDir
出力先ディレクトリを指定します。
この指定方法では、プロジェクトルートディレクトリのサブディレクトリ dist 配下に出力されます。

include
トランスパイルするソースファイルを指定します。
この指定方法では、プロジェクトルートディレクトリのサブディレクトリ src 配下のすべてのソースファイルがトランスパイルの対象となります。

tscコマンドによる型チェックはプロジェクト全体の網羅的な整合性を確認できることや、デプロイ環境の自動化にも組み込みやすいというメリットがあります。


Babel の設定
Babel のトランスパイルオプションは babel.config.json という設定ファイルで行います。
babel.config.js、.babelrc、.babelrc でも構いません。
これらの設定ファイルはトランスパイル時に自動的に読み込まれます。

babel.config.json や babel.config.js はプロジェクト全体に適用され、.babelrc や .babelrc は特定のサブディレクトリに適用する際に利用します。

{
	"presets": [
		["@babel/preset-env", {"modules":false}],
		["@babel/preset-typescript", {"isTSX":true, "allExtensions":true}],
		"@babel/preset-react"
	],
	"plugins": [
		["@babel/plugin-transform-runtime", {"corejs":3}]
	]
}

・presets
使用するプリセットを記述し、必要があれば各プリセットに対するオプションを指定します。

@babel/preset-env
"modules":falseとすることで、ESM 形式で出力されます。
デフォルトはcommonjsで、これはブラウザが認識できません。
実行時に以下のようなエラーが出力されたら、CommonJS になっている可能性があります。

Uncaught ReferenceError: require is not defined

@babel/preset-typescript
"isTSX":trueとすることで.tsx拡張子のファイル内で JSX 構文を使用できます。
これは主に React コンポーネントで使用される設定です。
"allExtensions":trueとすることでプリセットは.tsおよび.tsxファイルの両方で動作します。
これにより、TypeScript の全てのファイルが適切に処理されます。

・plugins
使用するプラグインを記述し、必要があれば各プラグインのオプションを指定します。

@babel/plugin-transform-runtime
"corejs":3とすることでcore-jsライブラリのバージョンを指定します。
デフォルトではundefinedになっており、core-jsを使用したい場合は明示的にバージョンを指定する必要があります。

ちなみに Babel の設定は package.json に記述することも可能です。

・・・
"babel": {
    "presets": [
        ["@babel/preset-env", {"modules":false}]
    ]
},
・・・


Webpack の設定
Webpack により複数の JavaScript ファイルやリソースを1つあるいは複数のファイルにバンドルします。
バンドル時にトランスパイルも行うので、babel-loaderもインストールします。
いずれも開発用パッケージです。

>npm install webpack webpack-cli babel-loader --save-dev

webpack
Webpack 本体です。

webpack-cli
コマンドラインから Webpack を実行するためのツールです。

babel-loader
Webpack のローダーの1つで、Webpack のビルドプロセス中に Babel を使用してトランスパイルを行います。
トランスパイルの実行には、babel-coreやプリセット(@babel/preset-envなど)も必要となります。
オプションの設定は前述の babel.config.json にて行います。

Webpack の設定はデフォルトでは webpack.config.* ファイルにて行いますが、--configオプションで設定ファイル名を指定できます。

以下は webpack.config.mjs の記述例です。

import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url)); // カレントディレクトリ
const srcDir = path.resolve(__dirname, "src"); // ソースファイルの場所
const publicDir = path.resolve(__dirname, "public"); // ドキュメントルートディレクトリ
const bundleDir = path.resolve(publicDir, "assets/js"); // バンドルモジュールの出力先

const main = {
	mode: "production", // development/production
	target: "web", // ブラウザ環境向けにバンドルを生成する
	entry: path.resolve(srcDir, "main.tsx"), // Webpackによるモジュールバンドルの入口点
	output: {
		path: bundleDir, // 出力先ディレクトリ
		filename: "main.js", // 出力ファイル名
		module: true, // バンドル全体がESM形式であることを明示
		chunkFormat: "module" // 各チャンクをESM形式で出力
	},
	resolve: { // 省略された拡張子の解決リスト
		extensions:[".tsx", ".jsx", ".ts", ".js", ".json"],
		byDependency: {
			esm: { fullySpecified: true } // ESMのインポート時に拡張子の省略不可を指定
		}
	},
	module: {
		rules: [{
			test: /\.(tsx|jsx|ts|js)$/, // ローダーが適用されるべきファイルの正規表現
			use: { loader: "babel-loader" }, // babel-loaderでトランスパイルする
			include: srcDir, // トランスパイルするソースコードの場所
			exclude: /node_modules/ // トランスパイルの対象から外す場所(正規表現)
		}]
	},
	experiments: { outputModule: true } // バンドル全体を ESM 形式で出力する(実験的機能)
};
export default [main];

この時点で Webpack によるバンドルが可能となります。
package.json に以下のスクリプトを組み込んで実行します。

・・・
  "scripts": {
・・・
    "bundle": "webpack --stats-error-details"
  },
・・・
# 本番環境用のバンドル
>npm run bundle


テストサーバの設定
React でのローカル環境でのテストを行う際は、開発用ローカルサーバの立ち上げが必要です。
ローカルサーバを介さずにブラウザから React で実装されたページにアクセスしようとすると、以下のようなエラーが生じる可能性があります。

# ローカル環境にあるファイルを相対パスでincludeしようとした
Uncaught TypeError: Failed to resolve module specifier "react". Relative references must start with either "/", "./", or "../".

# file://でローカル環境にあるhtmlをブラウザで表示しようとした
Access to script at 'file:///C:/React/test1/dist/App.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, isolated-app, ipns, https, chrome-untrusted, ipfs, data, chrome-extension, chrome.

Webpack には Webpack Dev Server という機能があり、これを利用することで容易にローカルサーバを立ち上げることができます。
Webpack Dev Server は React で実装されたページの閲覧を可能にし、コードの変更がリアルタイムでブラウザに反映されるため、開発プロセスが効率的になります。

React のテスト環境を設定するために、開発専用の Webpack 設定ファイル(webpack.config.dev.mjs)を作成します。
先に作成した webpack.config.mjs に以下の項目を追加します。

・・・
	devServer: {
		static: { // 静的ファイルの設定
			directory: publicDir, // ドキュメントルート
			publicPath: "/", // 静的ファイルのURLのパス
			watch: true // ファイルの変更を監視し自動で再トランスパイル
		},
		hot: false, // ★
		liveReload: true, // ★
		compress: true, // 通信データの圧縮
		port: 8080 // ポート番号
	},
	stats: { errorDetails: true }, // エラー詳細を出力
	devtool: "source-map" // ソースマップの出力
・・・

hot
HMR【Hot Module Replacement】 は特定のソースコードの変更をページ全体をリロードすることなく反映させる機能です。
HMR は ES2022 に対応していないので無効にしています。

liveReload
HMR が無効の場合でもファイル変更時にページがリロード(ライブリロード)されます。

statsdevtoolはテストサーバとは関係ありませんが、開発用として指定した方が良いオプションです。


Webpack Dev Server を起動するにははwebpack serveコマンドを使用します。
このコマンドはトランスパイル、バンドル、テストサーバの起動を一括で行います。
なお、バンドルされたファイルは実際には出力されず、メモリ上で管理されます。

package.json に以下のスクリプトを組み込んで実行します(★部分)。

・・・
  "scripts": {
    "tsc": "tsc --noEmit",
    "babel": "babel ./src --out-dir ./dist --extensions \".ts,.tsx\"",
    "bundle": "webpack --stats-error-details",
    "start": "webpack serve --mode development --config webpack.config.dev.mjs" ★
  },
・・・
>npm run start

これでテストサーバが立ち上がりました。

以上で開発環境の構築は完了です。


テストの実行
環境の構築ができたところで、実際に React の実装されたページを表示してみます。
カレントディレクトリ(プロジェクトのルートディレクトリ)配下の src ディレクトリに React のソースファイルを置きます。

以下は React の実装例です。

import React from "react";
import { createRoot } from "react-dom/client";

const App = () => {
	return (
		<div>
			<div>Hello from React!</div>
			<div>Welcome to my React application.</div>
		</div>
	);
};

const container = document.getElementById("message");
if(container != null){
	const root = createRoot(container);
	root.render(<App />);
}

ドキュメントルートのディレクトリに表示する HTML ファイルを置きます。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reactのテスト</title>
</head>
<body>

<h1>Hello World!</h1>
<div id="app"></div>
<div id="message"></div>
<script type="module" src="/assets/js/main.js"></script>
</body>
</html>

型チェックのため、React のソースファイルをtscコマンドでトランスパイルしてみます。

>npm run tsc

問題ないようでしたら、バンドルからテストサーバの立ち上げまで行います。

>npm run start

ブラウザでページを表示してみます。
http://localhost:8080/

Reactの画面

うまく表示されました。


テストサーバ単体
例えばバンドルせずに検証したい場合など、単にテストサーバを起動するだけであれば、http-serverの使用が適しています。

>npm install http-server --save-dev

package.json に以下のスクリプトを組み込んで実行します。

・・・
  "scripts": {
・・・
    "start2": "http-server ./public -p 8081"
  },
・・・
>npm run start2

ブラウザでの表示は以下のアドレスとなります。
http://localhost:8081/

コメント