🔮

本番環境でCSSが消えた!Webpack require.contextの落とし穴

に公開

はじめに

「開発環境では完璧に動いてたのに、本番にデプロイしたらCSSが効いてない...」

こんな経験、ありませんか?
原因を調べていくと、Webpackのrequire.contextという便利な機能に潜む落とし穴を発見。
今回はその問題と解決方法をシェアしたいと思います。

この記事で学べること

  • Webpackのrequire.contextimportの根本的な違い
  • 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が生成されてない! */

つまり何が起きてたか:

  1. require.contextは「ファイルの一覧」を作るだけ
  2. ファイルの中身(import文)は見ない
  3. CSS Modulesは処理されない
  4. 本番環境で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 buildwebpack コマンドを実行した時
  • この時にすべての依存関係を解決する必要がある

実行時とは?

  • ユーザーのブラウザで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も処理された!)

ビルド = 開発用のファイルをブラウザが理解できる形に変換する作業

具体的には:

  1. JSXをJavaScriptに変換

    // 開発中(JSX)
    <div className={styles.container}>Hello</div>
    
    // ビルド後(JavaScript)
    React.createElement("div", {className: "container_abc123"}, "Hello")
    
  2. TypeScriptをJavaScriptに変換

    // 開発中(TypeScript)
    const name: string = "User";
    
    // ビルド後(JavaScript)
    const name = "User";
    
  3. CSS Modulesをユニークなクラス名に変換

    /* 開発中(style.module.css) */
    .container { background: blue; }
    
    /* ビルド後 */
    .container_abc123 { background: blue; }
    
  4. すべてを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が重要?

  1. パフォーマンス: CSSとJSを並列で読み込める
  2. キャッシュ: CSSだけ更新した時、JSのキャッシュが効く
  3. 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>

なぜ特別扱いが必要?

  1. ビルド時の処理が必要

    • クラス名をユニークに変換
    • JavaScriptとCSSの対応表を作成
    • CSSファイルを別途生成
  2. 通常のCSSインポートとは違う

    // 通常のCSS(グローバルスタイル)
    import './global.css';  // ただ読み込むだけ
    
    // CSS Modules(ローカルスコープ)
    import styles from './style.module.css';  // オブジェクトとして扱う
    
  3. Webpackの特別な処理が必要

    • css-loaderのmodulesオプション
    • ファイル名に.module.cssを使う規約
    • ビルド時に依存関係として認識される必要がある

これからのベストプラクティス

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で読み込もう
  • 「ビルドの時」と「実行の時」の違いを意識しよう
  • 開発環境と本番環境の動作の違いに注意

もし同じような問題にぶつかったら:

  1. ブラウザの開発者ツールで症状を確認
  2. ビルドされたファイルとmanifest.jsonをチェック
  3. importの仕方を見直してみる
  4. それでもダメなら、webpack.configのカスタマイズを検討

この記事が、同じ問題で悩んでいる誰かの役に立てば嬉しいです!

参考リンク

Discussion