この記事でわかること
この記事では、ミニアプリのMVPリリース後にClaudeCodeを学習目的で導入し、AIとの壁打ち・検証によって得られたTailwind CSSの「それ知らんかったわ」を共有いたします。
ミニアプリにflashメッセージの追加実装を行っていた際に発生した
「ローカル環境ではflash通知の背景色が正常に表示されるのに、本番環境では表示されないよ!」
という不具合から私が得たTailwind CSSを扱う際の注意点を本記事ではご紹介いたします。
私は現在エンジニアを目指して学習中の超初学者でございます。
内容に誤りや分かりにくい表現が含まれている可能性があります。
初歩的な内容も含まれますが、私と同じような疑問を持つ方や、IT初学者の方の参考になればと思い、この記事を書いております。ご了承くださいませ。
今回不具合の起きたコードの実装や技術調査・検証は全てClaude Codeを通して行っております。
開発環境
- Ruby / Rails: (3.2.2 / 7.1.6)
- tailwindcss-rails: 4.6.0
- tailwindcss-ruby: 4.3.1
- デプロイ先: Render(Dockerによるビルド)
実装内容と発生した不具合
アプリ内でのログイン・ログアウトやCRUD機能の成功・失敗時に、flashメッセージ(トースト通知)を実装し、notice/alertで背景色を出し分ける実装を行いました。
```erb
<div class="... <%= type == "alert" ? "bg-[#fdf0f0] text-[#9c3b3b]" : "bg-[#eaf6ec] text-[#2f6b2a]" %>">
<%= message %>
</div>
処理が成功すれば「緑色の通知」、失敗すれば「赤色の通知」というシンプルな三項演算子による実装です。
こんな単純な実装でデプロイエラーなんかないだろうと油断していたら、
エラーは起きずとも不具合が発生しました。それが以下です。
調査の過程
1.デプロイ時の設定ミス・凡ミスを疑う
まずキャッシュ・デプロイのタイミングの齟齬を疑い、ブラウザのシークレットウィンドウやNetworkタブでCSSファイルのハッシュ値を確認しました。
先日Renderに自動デプロイの設定を施したばかりだったので、デプロイ設定まわりも調査しました。
→ 最新のデプロイであることを確認
2.本番環境のcssファイルの中身を見る
次に本番で読み込まれているCSSファイルの中身を検証ツールにて直接検索しました。
cssファイルの中身って検証ツールで見られるんですね...(Claude Codeに教えてもらった。)
→ 追加したはずの色コード(eaf6ecなど)がcssファイルに含まれていないことを確認
3.開発環境で不具合を再現
ローカルのDockerで本番と同じDockerfileを使ってビルドし、不具合を再現できるか検証しました。
ここからの確認作業や試行作業はClaude Codeくんにお任せしました。
→ こうして同じ現象を手元で再現
4.Claude Codeくんがコードレビュー
「他の色クラス(既存のもの)はCSSに含まれているのに、flashメッセージの色クラスだけ含まれていない」という差分に着目しました。(Claude Codeくんが。)
→ 僕は「何!ほんまかいな!」って言う役です。
5.原因の特定
三項演算子をif/elseに分解して、静的なクラス文字列に変えてビルドし直したところ、
正しくcss(背景色)が反映されることを確認
→ 三項演算子の書き方に原因があると特定
原因の解説
どうやらTailwindCSSは、Rubyのコードを実行して結果を見ているわけではなく、ファイルをただのテキストとして読み込み、正規表現的にクラス名らしき文字列を抽出している。とのこと。
私は以下のように、HTML属性のダブルクォートの中で、Rubyの三項演算子もダブルクォートを使って文字列を組み立てていました。
class="... <%= type == "alert" ? "bg-[#fdf0f0] text-[#9c3b3b]" : "bg-[#eaf6ec] text-[#2f6b2a]" %>"
この際、ClaudeCodeくんが言うには、「TailwindCSSのスキャナーは class=" の後に最初に出現する "("alert" の開きクォート)を属性の終わりと誤認してしまい、その後ろにある実際の色クラスを読み取れなくなっていた。」とのことでした。
特に注目してほしい箇所がクォーテーション(")の数です。
- class=" で始まった、この属性を囲む "
- その中の三項演算子の "alert" という文字列のための "
- さらにその中の "bg-[#fdf0f0] ..." という文字列のための "
つまり " が何重にも入れ子になっています。人間やRubyから見れば「どの " がどこと対応しているか」は文脈から読み取れるかもですが、Tailwindの読み取りエンジンは、単純に「次に出てきた " が閉じクォートだ」と判断してしまう可能性が高い状態になっていました。
修正内容の解説
そして「TailwindCSSが読み取りやすく」を意識して修正したコードが以下です。
<% if type == "alert" %>
<div ... class="... bg-[#fdf0f0] text-[#9c3b3b]">
<% else %>
<div ... class="... bg-[#eaf6ec] text-[#2f6b2a]">
<% end %>
条件分岐(<% if %> / <% else %>)はclass 属性の外側に配置し、HTMLタグそのものを丸ごと出し分ける形になっているので、Tailwindからは「ただの2つの独立したdiv」にしか見えず、それぞれのクラス名を迷いなく正確に読み取れる、というわけ。らしいです。
これであれば、class="..." の中には ERBの入れ子クォートが一切登場せず、
純粋な静的テキストのみになります。
検証してみる
実際に三項演算子からif文で背景色の条件分岐を行うようにしてから、正常にcssが当たるようになりました。
といっても、「TailwindCSSが"(ダブルクォート)の読み取りがちょっと苦手」というのは初めて聞きました。
後で調べるとそれを回避するための慣習はあるみたいで、無知でございました。。。
で、そんな時ことClaudeCodeの出番ですね。
こんなふうに投げてみました。
tailwindが"の入れ子に弱いといった弱点があるとは知らなかった...
何か入れ子構造の弱点を試せる簡単な実験的コードをくれないかな?
ClaudeCodeから「4パターンのテストコードを用意して、実際にビルドして違いを確認してみましょう。」と提案があったので実験してみました。
テストパターンを用意する
<!-- パターンA: 静的(三項演算子なし)※対照群 -->
<div class="bg-[#111111]">A</div>
<!-- パターンB: 三項演算子+ダブルクォート(今回のバグと同じ構造) -->
<div class="<%= true ? "bg-[#222222]" : "bg-[#333333]" %>">B</div>
<!-- パターンC: 三項演算子+シングルクォート(入れ子クォート説の検証) -->
<div class="<%= true ? 'bg-[#444444]' : 'bg-[#555555]' %>">C</div>
<!-- パターンD: 三項演算子の結果を一度変数に入れてから埋め込む -->
<% test_d = true ? "bg-[#666666]" : "bg-[#777777]" %>
<div class="<%= test_d %>">D</div>
これを本番環境を模したローカル環境で動かして、それぞれのcssが当たるか試します。
実験結果
Claude Codeが実験してくれた結果がこちらです。
| パターン | 内容 | 結果 |
|---|---|---|
| A | 静的(三項演算子なし) | ✅ FOUND |
| B | 三項演算子+ダブルクォート(今回の不具合と同じ構造) | ❌ MISSING |
| C | 三項演算子+シングルクォート | ✅ FOUND |
| D | 三項演算子の結果を変数に入れてから埋め込む | ✅ FOUND |
前回の「クォートの入れ子が原因」という仮説が、きれいに裏付けられました。
分かったこと
上の実験結果をまとめると、
- パターンB:(ダブルクォートの中にダブルクォートが入れ子)だけが失敗
- パターンC:同じ三項演算子でも、内側をシングルクォート('...')に変えただけで解決。class="..." の外側のダブルクォートと、内側のクォートの種類が一致しなければ、Tailwindは正しく最後まで読み取れることが分かりました。
- パターンD:三項演算子自体は変えず、単にclass属性の外(<% %>の中)で先に計算しておくだけでも解決。class="..."の中に直接クォートが入れ子にならなければ問題ないということみたいです。
今回の不具合の本質的な原因
今回の不具合になったコードがこれですが、
class="... <%= type == "alert" ? "bg-[#fdf0f0] text-[#9c3b3b]" : "bg-[#eaf6ec] text-[#2f6b2a]" %>"
今回のバグの本質的な原因は「三項演算子を使ったこと」自体ではなく、「class="..."というダブルクォートの中に、同じダブルクォートを使った文字列がそのまま入れ子になっていたこと」でした。
実は、先ほどのif/else文での修正より、内側のクォートをシングルクォートに変えるだけのほうが、もっと小さい変更で同じ効果が得られたことになります。
最小限の修正を行なった場合
実験のパターンCと同じ考え方で、三項演算子の内側(背景色指定)だけシングルクォートに変え、divを1つに戻した形が以下です。
<div class="... <%= type == "alert" ? 'bg-[#fdf0f0] text-[#9c3b3b]' : 'bg-[#eaf6ec] text-[#2f6b2a]' %>">
<%= message %>
</div>
"(ダブルクォート)の入れ子構造に対して、if文かシングルクォートのどちらで配慮するかはお好みですが、if文実装の場合は一見冗長に見えてしまう気もするので、僕はシングルクォートの方がお好みでした。
なぜローカル環境でのテストで気づけなかったか
開発環境ではbin/devにcssファイルを監視してもらっていますが、
こちらは一度立ち上げたらずっと動き続けて、少しずつ差分を追記していく仕組みらしいです。
今回は実装過程で色を何度も試行錯誤したので、
そのどこかの段階で一時的に静的な書き方をしていた瞬間があり、その時に一度クラスを検出・記憶してしまったのかと思われます。
そうすると、その後はずっと「知っているクラス」として保持され続けていた可能性が高いみたいです。
一方、本番のビルドは毎回まっさらな状態から1回だけスキャンするので、そういった「過去の記憶」に頼れず、今回のクォート入れ子問題がそのまま結果に出てしまった、という出力の違いが出ました。
まとめ
- TailwindCSS(JITエンジン)はHTMLやRubyの構文を理解しているわけではなく、テキストとしてクラス名を抽出しているだけ
- 動的にクラス名を組み立てる際は、クォートの入れ子に注意する
- 「ローカルでは動くのに本番だけ動かない」系の問題は、ローカルの長時間起動しているプロセス(監視・キャッシュ)が原因を覆い隠していないか疑うといいかも
もっと要約すると、HTML/CSSの入れ子構造は、
外側は(")、内側は(')にしておくと安心ということでした!
あと、HTML/CSSの入れ子構造におけるダブルクォーテーション/シングルクォーテーションの使い分けは思ってたより参考文献が少なくて裏付けに苦労しました。
一応Geiminiに聞くとすぐにこんなことを答えてくれました。
CSSで「ダブルクォーテーション(")」を入れ子にする場合、
外側と内側で「ダブル」と「シングル(')」を使い分けます。
やっぱりAIの裏付けができるのもAIなのかもしれませんね...笑
参考
