本番環境でCSSが消えた!Webpack require.contextの落とし穴
はじめに
「開発環境では完璧に動いてたのに、本番にデプロイしたらCSSが効いてない...」
こんな経験、ありませんか?
原因を調べていくと、Webpackのrequire.context
という便利な機能に潜む落とし穴を発見。
今回はその問題と解決方法をシェアしたいと思います。
この記事で学べること
- Webpackの
require.context
とimport
の根本的な違い - CSS Modulesが本番環境で読み込まれない理由
- 実際のエラーの見つけ方と解決方法
- 今後同じ問題を防ぐためのベストプラクティス
前提知識
主要な技術の簡単な説明
CSS Modules:CSSのクラス名を自動的にユニークにして、スタイルの衝突を防ぐ仕組み
/* style.module.css */
.button { background: blue; }
/* → 実際は .button_abc123 みたいな名前になる */
Webpacker:RailsでWebpackを簡単に使えるようにするGem(ライブラリ)
react-rails:Railsの中でReactコンポーネントを簡単に使えるようにするGem
使用した技術スタック
- フレームワーク: Ruby on Rails 6.1+
- フロントエンド: React
- モジュールバンドラー: Webpack 5 (Webpacker)
- CSS: CSS Modules
- 自動マウント: react-rails
起きた問題
状況
新しく実装したReactコンポーネントで、こんなことが起きました:
- ✅ 開発環境:バッチリ動く
- ❌ 本番環境:JavaScriptは動くけど、見た目がぐちゃぐちゃ
実際に見えた症状
ブラウザの開発者ツールで確認すると:
<!-- 期待していたHTML -->
<div class="indexMainContainer_abc123">
<!-- コンテンツ -->
</div>
<!-- 実際のHTML(本番環境) -->
<div class="indexMainContainer"> <!-- ハッシュがついてない! -->
<!-- コンテンツ -->
</div>
ネットワークタブを見ても、CSSファイル自体が読み込まれていませんでした。
最初に疑ったこと
最初は「本番と開発で設定が違うのかな?」と思いました。でも、調べてみると問題はもっと根本的なところにあったんです。
原因を探る
なぜrequire.contextを使っていたか
もともと、こんな理由でrequire.context
を使っていました:
- 新しいコンポーネントを追加するたびに、packファイルを作りたくない
- components/配下のファイルを自動的に認識させたい
- DRY(Don't Repeat Yourself)の原則に従いたい
もともとのコード(問題あり)
// app/javascript/packs/components.js
const componentRequireContext = require.context("components", true, /(widgets)\/.+/);
const ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);
このコードはcomponents/widgets/
フォルダの中にあるコンポーネントを自動的に読み込む、便利な仕組みでした。
コンポーネントの構造
components/widgets/user-profile/
├── Main.jsx
└── (内部でIndex.tsxをimport)
└── Index.tsx
└── style.module.css (CSS Modulesをimport)
ここが問題だった!
require.contextがやっていること(詳細版)
ステップ1: Webpackがビルドを開始
webpack「よーし、components.jsをビルドするぞ!」
ステップ2: require.contextを発見
const componentRequireContext = require.context("components", true, /(widgets)\/.+/);
webpack「お、require.contextだ。widgets/フォルダの中を見てみよう」
webpack「Main.jsxを見つけた!これは後で使うかもしれないからメモしとこう」
webpack「でも中身は今は見ないよ。実行時に必要になったら見るから」
ステップ3: ビルド完了
webpack「ビルド完了!Main.jsxの存在は記録したよ」
webpack「え?Main.jsxの中にCSS importがある?知らないなぁ...」
結果: バンドルの中身
// user-profile-a1b2c3d4.js の中身(簡略化)
{
"./components/widgets/user-profile/Main.jsx": function(module, exports) {
// Main.jsxのコードはある
// でもCSS Modulesの処理はされてない!
}
}
/* user-profile-a1b2c3d4.css */
/* 空っぽ!CSSが生成されてない! */
つまり何が起きてたか:
-
require.context
は「ファイルの一覧」を作るだけ - ファイルの中身(import文)は見ない
- CSS Modulesは処理されない
- 本番環境でCSSファイルが存在しない!
どうやって解決したか
シンプルな解決策
新しくファイルを作って、普通にimportするだけでした:
// app/javascript/packs/user-profile.js
// これだけ!
import '../components/widgets/user-profile/Main';
なんでこれで直るの?
import文の場合(詳細版)
ステップ1: Webpackがビルドを開始
webpack「user-profile.jsをビルドするぞ!」
ステップ2: import文を発見
import '../components/widgets/user-profile/Main';
webpack「import文だ!Main.jsxの中身を見なきゃ」
ステップ3: Main.jsxの中身を解析
// Main.jsx
import Index from './Index';
webpack「Main.jsxの中にもimport文がある!Index.tsxも見よう」
ステップ4: Index.tsxの中身を解析
// Index.tsx
import styles from './style.module.css';
webpack「CSS Modulesだ!これは特別な処理が必要だな」
webpack「クラス名をユニークにして、CSSファイルも生成しよう」
ステップ5: ビルド完了
webpack「全部の依存関係を解析したよ!」
webpack「JavaScriptもCSSも、必要なものは全部バンドルに含めた!」
結果: バンドルの中身
// user-profile-a1b2c3d4.js の中身(簡略化)
{
"./components/widgets/user-profile/Main.jsx": function(module, exports) {
// Main.jsxのコード
},
"./components/widgets/user-profile/Index.tsx": function(module, exports) {
// Index.tsxのコード
},
"./components/widgets/user-profile/style.module.css": function(module, exports) {
// CSS Modulesのマッピング情報
module.exports = {
"container": "container_abc123",
"button": "button_def456"
}
}
}
/* user-profile-a1b2c3d4.css */
.container_abc123 { background: blue; }
.button_def456 { color: white; }
まとめると:
-
import
文 = Webpackに「このファイルの中身も全部見て!」という指示 - require.context = Webpackに「このフォルダのファイル一覧だけ作って」という指示
- CSS Modulesを使うなら、必ず
import
文で読み込む必要がある
この経験から学んだこと
1. 「ビルド時」と「実行時」は全然違う
ビルド時とは?
- コードを本番用に変換する時(開発者のPCやCIサーバーで実行)
-
npm run build
やwebpack
コマンドを実行した時 - この時にすべての依存関係を解決する必要がある
実行時とは?
- ユーザーのブラウザでJavaScriptが動く時
- ページを開いて、実際にコードが実行される時
- この時点では新しいファイルを読み込むことはできない(バンドルに含まれているものだけ)
// ❌ 実行時に動的に読み込む
require.context("components", true, /pattern/);
// → ビルド時:ファイル一覧を作るだけ
// → 実行時:一覧から必要なファイルを選ぶ
// → 問題:CSSの依存関係はビルド時に解決されない!
// ✅ ビルド時にしっかり解析
import './components/Component';
// → ビルド時:すべての依存関係を解析
// → 実行時:すでに処理済みのコードを実行するだけ
// → 結果:CSSも含めてすべて正しく動作!
2. モジュールとバンドルを理解する
そもそもモジュールって何?
モジュール = 機能ごとに分けられたコードの単位
// 昔のJavaScript(すべて1つのファイルに)
function calculatePrice() { /* ... */ }
function displayProduct() { /* ... */ }
function addToCart() { /* ... */ }
// 全部グローバル空間に...名前が衝突する危険!
// 現代のJavaScript(モジュールシステム)
// price.js
export function calculatePrice() { /* ... */ }
// product.js
export function displayProduct() { /* ... */ }
// cart.js
import { calculatePrice } from './price.js';
export function addToCart() { /* ... */ }
なぜモジュールが必要?
- コードの再利用がしやすい
- 名前空間の衝突を防げる
- 依存関係が明確になる
- チーム開発がしやすい
でも問題が...
- ブラウザは
import/export
を完全にはサポートしてない - ファイルが増えると読み込みが遅い
- 依存関係の順番を管理するのが大変
だからバンドルが必要!
そもそもビルドって何?
開発中のコード:
app/javascript/
├── components/
│ └── widgets/
│ └── user-profile/
│ ├── Main.jsx (100行)
│ ├── Index.tsx (200行)
│ └── style.module.css (50行)
├── utils/
│ └── helpers.js (30行)
└── packs/
└── user-profile.js (1行: import文だけ)
これを「ビルド」すると:
public/packs/
├── js/
│ └── user-profile-a1b2c3d4.js (381行: 全部まとめられた!)
└── css/
└── user-profile-a1b2c3d4.css (50行: CSSも処理された!)
ビルド = 開発用のファイルをブラウザが理解できる形に変換する作業
具体的には:
-
JSXをJavaScriptに変換
// 開発中(JSX) <div className={styles.container}>Hello</div> // ビルド後(JavaScript) React.createElement("div", {className: "container_abc123"}, "Hello")
-
TypeScriptをJavaScriptに変換
// 開発中(TypeScript) const name: string = "User"; // ビルド後(JavaScript) const name = "User";
-
CSS Modulesをユニークなクラス名に変換
/* 開発中(style.module.css) */ .container { background: blue; } /* ビルド後 */ .container_abc123 { background: blue; }
-
すべてを1つのファイルにまとめる(これがバンドル!)
バンドル = ビルドの結果できる「まとめファイル」
なぜまとめる必要があるの?
- ブラウザは
import
文を理解できない(最新のブラウザは一部対応) - ファイルが多いと読み込みが遅い(100個のファイルより1個の方が速い)
- 依存関係を正しい順番で読み込む必要がある
extract_css: trueの重要性
Webpackerの設定を確認してみよう
# config/webpacker.yml
production:
extract_css: true # 本番環境ではCSSを別ファイルに分離
extract_css: falseの場合
// バンドルされたJSファイルの中にCSSが含まれる
// user-profile-a1b2c3d4.js
const styles = ".container_abc123 { background: blue; }";
// JavaScriptの実行時に<style>タグを動的に追加
document.head.appendChild(styleTag);
extract_css: trueの場合
public/packs/
├── js/
│ └── user-profile-a1b2c3d4.js # JavaScriptのみ
└── css/
└── user-profile-a1b2c3d4.css # CSSは別ファイル
HTMLでの読み込み:
<link rel="stylesheet" href="/packs/css/user-profile-a1b2c3d4.css">
<script src="/packs/js/user-profile-a1b2c3d4.js"></script>
なぜextract_css: trueが重要?
- パフォーマンス: CSSとJSを並列で読み込める
- キャッシュ: CSSだけ更新した時、JSのキャッシュが効く
- FOUC防止: Flash of Unstyled Content(スタイルが後から適用される現象)を防げる
動作確認の方法
# 1. ビルドを実行
RAILS_ENV=production bundle exec rails assets:precompile
# 2. 生成されたファイルを確認
ls -la public/packs/css/
# user-profile-a1b2c3d4.css があるか確認
# 3. CSSファイルの中身を確認
cat public/packs/css/user-profile-*.css | grep container_
# .container_abc123 のようなクラスが含まれているか
# 4. manifest.jsonを確認
cat public/packs/manifest.json | jq '.'
# "user-profile.css": "/packs/css/user-profile-a1b2c3d4.css" があるか
もしCSSファイルが生成されていなかったら?
- CSS Modulesのimportが正しく解析されていない
- require.contextの問題である可能性が高い!
3. CSS Modulesは特別扱いが必要
CSS Modulesの仕組み(詳細)
通常のCSS:
/* style.css */
.container { background: blue; }
<!-- HTML -->
<div class="container">内容</div>
問題:他のコンポーネントも.container
を使ってたら衝突する!
CSS Modules:
/* style.module.css */
.container { background: blue; }
// JavaScript
import styles from './style.module.css';
// styles.container = "container_abc123" (ユニークな名前)
<div className={styles.container}>内容</div>
<!-- 実際のHTML -->
<div class="container_abc123">内容</div>
なぜ特別扱いが必要?
-
ビルド時の処理が必要
- クラス名をユニークに変換
- JavaScriptとCSSの対応表を作成
- CSSファイルを別途生成
-
通常のCSSインポートとは違う
// 通常のCSS(グローバルスタイル) import './global.css'; // ただ読み込むだけ // CSS Modules(ローカルスコープ) import styles from './style.module.css'; // オブジェクトとして扱う
-
Webpackの特別な処理が必要
- css-loaderの
modules
オプション - ファイル名に
.module.css
を使う規約 - ビルド時に依存関係として認識される必要がある
- css-loaderの
これからのベストプラクティス
1. どっちを使えばいい?
// 言語ファイルとか、動的に切り替えるものはrequire.contextでOK
const context = require.context('./locales', false, /\.json$/);
const locale = context(`./${language}.json`);
// CSSとか画像とか、依存関係があるものは普通にimport
import Component from './Component';
なぜ開発環境では動いていたのか?
実は開発環境では、webpack-dev-serverが賢く動いてくれていました:
- ファイルの変更を検知して、依存関係を動的に解決
- HMR(Hot Module Replacement)が働いて、CSSも更新される
- しかし本番ビルドでは、静的解析の結果だけが使われる
これが「開発では動くのに本番で動かない」の真相でした。
2. 新しいコンポーネントを追加するときのチェック項目
- CSS Modules使ってる?
- 使ってるなら、個別のpacksファイルを作る
- ちゃんとimport文で読み込む
- 開発環境と本番環境の両方で動作確認
3. 困ったときのデバッグ方法(詳細版)
ステップ1: 設定の確認
# extract_cssの設定を確認
grep -A5 -B5 "extract_css" config/webpacker.yml
ステップ2: ビルドとファイル確認
# 本番環境のビルドを実行
RAILS_ENV=production bundle exec rails assets:precompile
# 生成されたファイルを確認
ls -la public/packs/
# ├── css/ ← このディレクトリがあるか?
# ├── js/
# └── manifest.json
# CSSファイルの存在確認
ls -la public/packs/css/ | grep user-profile
# user-profile-a1b2c3d4.css があれば成功!
# manifest.jsonの中身を確認
cat public/packs/manifest.json | jq '.'
# {
# "user-profile.js": "/packs/js/user-profile-a1b2c3d4.js",
# "user-profile.css": "/packs/css/user-profile-a1b2c3d4.css" ← これがあるか?
# }
ステップ3: CSSの中身を確認
# CSS Modulesが正しく処理されているか
cat public/packs/css/user-profile-*.css | head -20
# .container_abc123 { ... } ← ハッシュ付きのクラス名になっているか?
# もし空っぽなら...
wc -l public/packs/css/user-profile-*.css
# 0 行なら、CSS Modulesが認識されていない!
ステップ4: Webpackのログを詳しく見る
# 詳細なログを出力してビルド
RAILS_ENV=production NODE_ENV=production bin/webpack --progress --profile
# CSS関連のエラーを探す
RAILS_ENV=production NODE_ENV=production bin/webpack 2>&1 | grep -i css
よくある問題と解決策
症状 | 原因 | 解決策 |
---|---|---|
CSSファイルが生成されない | require.contextで読み込んでいる | import文に変更 |
CSSは生成されるが空 | CSS Modulesのimportが解析されていない | 依存関係を確認 |
クラス名にハッシュがつかない | .module.cssの命名規則を守っていない | ファイル名を確認 |
開発では動くが本番で動かない | extract_cssの設定差 | 設定を統一する |
4. 他の解決方法も検討しよう
今回は個別のpackファイルを作る方法で解決しましたが、他にもこんな方法があります:
webpack.configをカスタマイズ
// config/webpack/environment.js
environment.config.merge({
module: {
rules: [{
test: /\.module\.css$/,
use: ['style-loader', 'css-loader']
}]
}
});
動的インポートを使う
// 必要な時だけ読み込む
const loadComponent = async () => {
const { default: Component } = await import('../components/widgets/user-profile/Main');
return Component;
};
ただし、これらの方法にもそれぞれトレードオフがあるので、チームの状況に応じて選択しましょう。
まとめ
今回の問題で一番大事だったのは、「Webpackがいつ、どこまでファイルを見てくれるか」を理解することでした。
覚えておきたいポイント:
-
require.context
は便利だけど、深くは見てくれない - CSS Modulesみたいな特殊なファイルは、普通の
import
で読み込もう - 「ビルドの時」と「実行の時」の違いを意識しよう
- 開発環境と本番環境の動作の違いに注意
もし同じような問題にぶつかったら:
- ブラウザの開発者ツールで症状を確認
- ビルドされたファイルとmanifest.jsonをチェック
- importの仕方を見直してみる
- それでもダメなら、webpack.configのカスタマイズを検討
この記事が、同じ問題で悩んでいる誰かの役に立てば嬉しいです!
Discussion