はじめに
「Dockerを用いてReactの開発環境を構築してみたが、ホットリロードがやたら遅くて使い物にならない」
フロントエンドの開発をしようと環境構築を始めた私はそのような悩みに直面しました。画面の変更が多いフロントエンド開発において、ホットリロードが遅いことは致命的な問題です。そのような状態では、ソースコードの更新結果を確認しようとする度に、遅さのあまり開発意欲が削がれていってしまうのは、きっと私だけではないでしょう。
この記事では、そのような問題への対処法について記載していきます。
同じような問題に直面している方の一助になれば幸いです。
0. Dockerを用いてReact開発環境構築
まずは、Dockerを用いてReactの開発環境を構築していきましょう。
前提として、Dockerとはコンテナ型仮想環境を構築するためのプラットフォームです。Dockerコンテナを使用することには、どのようなマシンでも同じように動作するため、複数人で開発する際に環境の差異を考慮する必要がなくなったり、コンテナごとにバージョン管理が可能であるため、ローカル環境が汚染することを防げたりするなどの利点があります。
Dockerを用いて環境構築をする際に重要になるファイルが、Dockerfileとdocker-compose.ymlの2つです。
Dockerfileとは、Dockerイメージを構築するためのテキストドキュメントです。Dockerfileには、イメージ内に含めるソフトウェアやライブラリ、コード、環境変数、ファイルやディレクトリのコピー、ネットワーク設定など、Dockerイメージの作成に必要なすべての命令が書かれています。Dockerfileは docker build コマンドを使って読み込まれ、それに基づいて新しいDockerイメージが作成されます。
docker-compose.ymlファイルとは、複数のDockerコンテナの定義と関連設定を一元管理するためのYAMLファイルです。このファイルを使用すると、開発者は docker-compose up コマンド一つで複数の関連するDockerコンテナを一度に立ち上げることができます。
Dockerを用いてReactの開発環境を構築することを想定して、これら2つのファイルをシンプルに書くと、それぞれ次のような内容になるかと思います。
FROM node:18
RUN mkdir /app && chown node:node /app
WORKDIR /app
USER node
COPY --chown=node:node package.json yarn.lock ./
RUN yarn install
COPY --chown=node:node . .
EXPOSE 5173
CMD [ "yarn", "dev" ]
version: "3.8"
services:
react-app:
container_name: react-app
build:
context: .
tty: true
volumes:
- .:/app:cached
ports:
- 5173:5173
上記のようなファイルを用意して、次のコマンドを実行すると、
$ docker compose build --no-cache
$ docker compose up -d
1. node_modulesを名前付きボリュームにマウント
Reactプロジェクトの初期画面を表示することはできましたが、ここでひとつの問題が生じます。ホットリロードに数秒程度のラグが生じてしまうという問題です。画面の変更が多いフロントエンド開発において、それは致命的な問題でしょう。
結論から述べると、nodo_modulesがホストマシンとコンテナの間で同期的に共有されていることがこの問題の原因です。
Reactを含むNode.jsのプロジェクト内には、node_modulesというプロジェクトが依存している外部パッケージを格納しておくためのディレクトリが存在しています。npmやyarnなどのパッケージマネージャーを通じて外部パッケージがインストールされることによって、プロジェクト内でそのパッケージの関数、コンポーネント、モジュールなどを利用できるようになります。
前述のdocker-compose.ymlでは次のような記述をしました。
volumes:
- .:/app:cached
この記述は「現在のディレクトリ(Reactプロジェクト)をコンテナ内の/appディレクトリにマウントする」という意味です。したがって、Reactプロジェクトに含まれるnode_modulesも/appディレクトリにマウントされることになります。
そのようにnode_modulesをDockerのボリュームとしてマウントすると、その中にある何千もの小さいファイルをホストOSとコンテナOSが頻繁に読み書きする必要が生じます。これらのOS間のファイルI/Oは、特にMacOSやWindowsでは非常に遅いため、node_modulesをボリュームとしてマウントすると、アプリケーションの起動時間が長くなり、またホットリロードの速度も低下します。
そこで、node_modulesを名前付きボリュームにマウントすることで、この問題を解決しました。具体的には、次のようにdocker-compose.ymlを修正します。
version: "3.8"
services:
react-app:
container_name: react-app
build:
context: .
tty: true
volumes:
- .:/app:cached
- react_node_modules:/app/node_modules
ports:
- 5173:5173
volumes:
react_node_modules:
1つ目の修正点は次の部分です。
この修正により、react_node_modulesという名前付きボリュームをコンテナ内の/app/node_modulesディレクトリにマウントするようになります。
volumes:
- .:/app:cached
- react_node_modules:/app/node_modules
2つ目の修正点は次の部分です。
この修正により、react_node_modulesという名前のボリュームが定義されます。このボリュームは、Dockerホストの特定のディレクトリではなく、Dockerが管理する領域にデータを保持します。
volumes:
react_node_modules:
このような修正により、アプリケーションの node_modulesディレクトリがDockerの管理下にある別の名前付きボリュームに保存されます。これにより、ホストマシンとコンテナ間でのnode_modulesディレクトリの同期を避け、パフォーマンスを向上させることができます。また、node_modulesディレクトリは、コンテナを再ビルドしたり再起動したりしても維持されます。
再度、localhost:5173 にアクセスして、ホットリロードの速度を確認すると、数秒程度かかっていたのが大幅に改善しているのが見て取れるはずです。
2. Dev Containers を開発に使用
しかし、この対処は新たな問題を生じさせます。それはホスト側のReactプロジェクトからnode_modulesがなくなることです。
前述の通り、node_modulesには、関数、コンポーネント、モジュールなどを利用するための外部パッケージが格納されています。そのため、Reactプロジェクトでは、必要な関数などをnode_modulesから読み込んで使用します。
コンテナ側でサーバーは立ち上げているため、ホスト側のReactプロジェクトからnode_modulesがなくなったとしても、アプリが動かなくなることはありません。ただ、ソースコードの編集を行っているのはホスト側であるため、エディター(今回の場合はVSCode)から大量のエラーを吐かれて画面が真っ赤になってしまいます。
この問題を解決する方法として、Dev Containers を導入しました。
先ほどはReactプロジェクトの開発環境を構築するためにDockerを用いましたが、Dev Containers は同様の技術を用いて、プロジェクトごとにコンテナで分離された開発環境を構築することができます。また、コンテナにSSH接続することによって、コンテナ内で直接ソースコードを編集することができるようになります。
Dev Containersの構成は、プロジェクトのルートディレクトリに置かれた.devcontainerディレクトリ内のdevcontainer.jsonファイルで行われます。devcontainer.jsonはVSCodeの設定(使用するDockerfile、追加の拡張機能、開いたポートなど)を提供します。具体例は、次の通りです。
{
"name": "procject-dev",
"dockerComposeFile": [
"../docker-compose.yml"
],
"service": "react-app",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"dsznajder.es7-react-js-snippets",
"VisualStudioExptTeam.vscodeintellicode",
"christian-kohler.path-intellisense",
"mhutchie.git-graph",
"donjayamanne.githistory",
"oderwat.indent-rainbow",
"formulahendry.auto-rename-tag",
"vscode-icons-team.vscode-icons",
"MariusAlchimavicius.json-to-ts",
"wix.vscode-import-cost"
],
"settings": {
"files.encoding": "utf8",
"files.eol": "\n",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}
},
"forwardPorts": [5173],
"remoteUser": "node"
}
VSCodeで Dev Containers を使用するためには拡張機能をインストールする必要があります。Remote Development という拡張機能パックをインストールしてください。
インストールが完了したら、左下の><のようなボタンを押すと、下図のようなメニューが表示されます。そこから「コンテナーで再度開く」を選択してください。
そうすると、コンテナ内からソースコードを編集できるようになります。それにより、ホスト側でソースコードを編集する時のようなnode_modules不在によるエラー表示が生じなくなります。
3. コンテナ内からソースコードをGit管理
これですべての問題が解決されたように思われますが、ひとつだけ面倒ごとがあります。それはコンテナ内からGitコマンドを扱えないため、別のターミナルから操作する必要があることです。
そこで、ローカルとコンテナで同じ認証情報を使えるようにします。
そのために、SSHキーをssh-agentに追加します。ssh-agentとは、ユーザーがSSHを使用してリモートサーバーに接続する際に、秘密鍵のパスフレーズを毎回入力する必要を減らす役割を果たすプログラムです。
まずは、次のコマンドでssh-agentを起動します。
$ eval "$(ssh-agent -s)"
続いて、ssh-addコマンドを使用して鍵をエージェントに追加します。
Linuxの場合
$ ssh-add ~/.ssh/<秘密鍵の名前>
Macの場合
$ ssh-add --apple-use-keychain ~/.ssh/<秘密鍵の名前>
最後に、ssh-agentがログイン時に起動されるようにします。
~/.bash_profile や ~/.zprofile に以下を追記してください。
if [ -z "$SSH_AUTH_SOCK" ]; then
# Check for a currently running instance of the agent
RUNNING_AGENT="`ps -ax | grep 'ssh-agent -s' | grep -v grep | wc -l | tr -d '[:space:]'`"
if [ "$RUNNING_AGENT" = "0" ]; then
# Launch a new instance of the agent
ssh-agent -s &> $HOME/.ssh/ssh-agent
fi
eval `cat $HOME/.ssh/ssh-agent`
fi
このような対応によって、コンテナ内からもGit管理できるようになります。
終わりに
以上のような3つの対応によって、Dockerコンテナを用いた開発環境においても、ローカルと変わらない開発体験のまま、Reactのホットリロードを高速化することができます。対応済のDockerを用いたReact開発環境をGitHubで公開しているので、ぜひ参考に使っていただけると幸いです。
また、Reactの学習方法については、会社の同期がこちらの記事を書いているので、ぜひご覧ください。
参考資料
2. Dev Containers を開発に使用
3. コンテナ内からソースコードをGit管理