この記事は Claude on SonicGarden の記事です。ソニックガーデンのプログラマが、Claude Codeの活用について書いています。#claude_on_sonicgarden
Bootstrap 4 → 5 移行をClaude Codeで進めたら、人力の数倍速で終わった話
TL;DR
- 既存Railsアプリ(Hamlビュー数百ファイル)をBootstrap 4.6 → 5.3 へ移行
- 戦略: BS4とBS5を並行ロードする「互換シム(compat shim)」を最初に挟み、段階的に置換
- ツール: Claude Code に CLAUDE.md でプロジェクト規約を伝え、置換 → 表示確認 → 微修正のループを回した
- 規模: 505ファイル / 約2,900行追加・1,950行削除 / 55コミット / 約1週間(うち実作業はもっと短い)
- 効果: 機械的な置換は丸投げできるので、レビューと表示崩れの確認に集中できた
1. はじめに
Bootstrapの4→5は破壊的変更が多く、ml-2 → ms-2、badge-primary → text-bg-primary、form-group の廃止、close ボタンの仕様変更、ガター(gx-* / gy-*)の概念追加、JS APIの変更…と置換対象が膨大です。Hamlを数百ファイル抱えているプロジェクトでこれを一括手作業でやるのは、誰も得をしません。
正直に告白すると、このBS5移行は 「やらなきゃいけないのは分かっているが、重くて手が出せない」 タスクとして長らく塩漬けにしていました。500ファイル超のhamlを目視で書き換える作業を想像するだけで腰が引けて、後回しにしては「またいつかやろう」と先送りにしていた、典型的な技術的負債です。Claude Code がなかったら、おそらく今もまだ4系のままだったと思います。
この記事では、Rails 8 + Vite + Hamlで運用している中規模Webサービス(500画面前後)の Bootstrap 4.6 → 5.3 移行を、Claude Codeに半分以上委譲した実例を紹介します。
対象読者
- BS4からBS5への移行を検討中のRails / Hamlプロジェクトの担当者
- 「AI支援で大規模リファクタリングを進めたいけど、どう進めればいいか分からない」という方
- Claude Codeを単発の質問回答ではなく、腰を据えて使い倒す手応えを掴みたい方
2. 前提環境
| 項目 | バージョン |
|---|---|
| Ruby | 3.4.8 |
| Rails | 8.0 |
| ビューエンジン | Haml |
| アセットビルド | Vite |
| 既存BS | 4.6.2 |
| 移行先 | bootstrap@^5.3 |
| フォーム | simple_form |
Hamlのビューは概算で500ファイル超、コンポーネント(ViewComponent)も多用しています。
3. 移行の戦略
3-1. ノーガード一括置換は避ける
最初に重要なのは、yarn upgrade bootstrap 一発で済ませないことです。動かない画面が500ある状態をデバッグするのは現実的ではありません。
そこで取った戦略はオーソドックスな段階的移行です。
[1] BS4とBS5を並行ロードできる状態を作る(package alias)
↓
[2] BS4→BS5の互換クラスマッピング(SCSS shim)を入れる
↓
[3] 影響範囲が小さい画面(システム管理者画面)でパイロット
↓
[4] 全画面のクラス名をBS5ネイティブへ機械的に置換
↓
[5] simple_formのカスタム設定をBS5仕様へ(form-group廃止等)
↓
[6] 互換シム削除
↓
[7] 表示崩れの個別修正
3-2. 並行ロード: npm aliasで bootstrap5 を別名インストール
package.json でこう書きます。
{
"dependencies": {
"bootstrap": "^4.6.2",
"bootstrap5": "npm:bootstrap@^5.3"
}
}
bootstrap5 というキーで Bootstrap 5 を別名インストール。SCSSではこちらを参照するようにすれば、BS4のJS資産(jQuery依存のカルーセル等)を残したまま、CSSだけ先に切り替えるといった芸当ができます。
3-3. 互換シム: bs4_compat.sass
これが移行の鍵です。BS4のクラスを @extend でBS5のクラスへマッピングするSCSSを1ファイル用意します。
// app/javascript/stylesheets/overrides/bs4_compat.sass
//
// Bootstrap 4 → 5 互換シム(段階的に削除予定)
// ビューやコンポーネントを BS5 ネイティブのクラス名に書き換えたら、
// 対応するシムを削除していく。
// simple_form BS4 wrapper 互換
.form-group
margin-bottom: 1rem
.form-control-label
@extend .form-label
.custom-control
@extend .form-check
.custom-control-input
@extend .form-check-input
.custom-select
@extend .form-select
// badge カラークラス
.badge-primary
@extend .text-bg-primary
.badge-secondary
@extend .text-bg-secondary
// ...
// margin/padding の left/right → start/end
@for $i from 0 through 5
.ml-#{$i}
@extend .ms-#{$i}
.mr-#{$i}
@extend .me-#{$i}
.pl-#{$i}
@extend .ps-#{$i}
.pr-#{$i}
@extend .pe-#{$i}
.ml-auto
@extend .ms-auto
// text alignment
.text-left
@extend .text-start
.text-right
@extend .text-end
// btn-block(BS5で廃止)
.btn-block
display: block
width: 100%
これで「BS4のクラスがビューに残っていてもBS5の見た目で動く」状態を作れます。一気に書き換えられないチームでは特に有効です。
このシムを入れて、まず全画面で見た目が壊れない状態を作ることがゴール。ここまでは人力でやって、ここから先をClaude Codeに任せました。
4. Claude Codeでの実際の進め方
4-1. CLAUDE.md でプロジェクト規約を伝える
リポジトリ直下に CLAUDE.md を置き、以下を記述しました。
# CLAUDE.md
## プロジェクト概要
Rails 8.0.4 / Ruby 3.4.8 / Vite + Stimulus / Haml + ViewComponent。
## 開発コマンド
bin/dev # サーバー
bundle exec rspec # テスト
bundle exec rubocop # lint
## コーディング規約
- rubocop設定は sgcop gem に準拠
- コミットメッセージ・コメント・PR説明は日本語
- 変更したファイルは必ず lint
- .rb → bundle exec rubocop <ファイル>
- .haml → bundle exec haml-lint <ファイル>
## テストガイドライン
- テストは spec/models と spec/system のみ
- describe/context/it は日本語
- AAA パターン
- バグ修正は TDD(失敗するテストを先に書く)
このCLAUDE.mdは会話ごとに自動でロードされるので、毎回「lintしてね」「日本語でコミットメッセージ書いてね」と指示する必要がなくなります。地味ですが毎回の指示の繰り返しが消える効果は絶大でした。
4-2. パイロット:影響範囲が小さい画面で先行
いきなり500ファイルに手を入れず、まず管理者画面(影響範囲が狭く、見るユーザーが社内に閉じている)でやらせました。コミット規模としては50ファイル程度・250行ほどの差分。これでフローが回せることを確認してから本番投入。
差分が大量に出ても、置換の規則性が高いので目視レビューが効きます。これが手書きだとそうはいきません。
4-3. 本番置換:全画面を一気に変換
依頼の仕方は、おおむねこんな粒度です。
このリポジトリで使われているBS4のクラス名を、対応するBS5のクラス名に置き換えてください。
対象ファイルはapp/views/**/*.hamlapp/components/**/*.haml。
マッピングはapp/javascript/stylesheets/overrides/bs4_compat.sassを参照してください(このファイルが正解)。
置換が終わったら、変更ファイルに対してbundle exec haml-lint <ファイル>を実行してください。
ここで重要なのは、人間がマッピングルールの正解(compat shim)を先に作っておくこと。これがないとAIは「だいたい合ってる置換」をしますが、プロジェクト固有の選好(例:badge-info をどう扱うか)は判断できません。
このフェーズの一括置換PRは、Hamlだけで390ファイル・1,100行超の差分になりました。単純作業の集合体なので生成自体は短時間で済みます。
4-4. simple_form:BS5用ラッパーへ
simple_form は BS5 公式に対応しているのですが、BS4時代の wrapper 設定が残っているとそのまま動きません。config/initializers/simple_form_bootstrap.rb の入れ替えと、form-group などの旧クラスを使ったカスタム入力UI周りの調整が必要でした。
ここはやや特殊なドメインなので、Claude Code に simple_form 公式ドキュメントを参照させながら、生成 → 動作確認 → 微修正のループを回しました。
4-5. シム削除:終わったら必ず燃やす
最後に大事なのが、互換シムを全部消すことです。残しておくと「動くから書き換えなくていい」状態が継続して、結局移行が完了しません。
シム削除は段階的に進めました。
- 利用箇所が無くなったマッピング(margin/padding系、text-align系など)から削除
- simple_form 互換部分は initializer の置き換え後にまとめて削除
- 最後に
bs4_compat.sass自体をリポジトリから削除
シムを消すと当然どこかで表示崩れが出ます。残った崩れを別PRで個別修正していくのが、移行最終フェーズの作業です。シム削除後の表示崩れ修正は人間(と Claude Code との対話)の出番で、ブラウザでの目視確認が中心になります。
5. 効率化のポイント
5-1. 「機械的な部分」と「判断が必要な部分」を分離する
Claude Code は機械的な置換が得意です。一方、こういうのは人間(か、人間が判断材料を渡したAI)の領域です。
| 領域 | 担当 |
|---|---|
| クラス名の一括置換 | Claude Code |
bs4_compat.sass(マッピング規則) |
人間 |
| パイロット範囲の選定 | 人間 |
| 表示崩れの修正方針 | 人間(実装は Claude Code に委譲可) |
| simple_form のカスタムwrapper設計 | 人間と対話 |
btn-info の文字色のような視覚的判断 |
人間(ブラウザで確認) |
5-2. 一度の依頼で膨大に走らせない
「全部やって」ではなく「app/components/ 配下のhamlだけ」のように 依頼単位を切る。理由は3つ。
- レビューしやすい
- 途中で方針修正が効く
- CIが通る粒度でコミットできる
5-3. lint・テストはClaude Code側で走らせる
CLAUDE.md に「変更ファイルは必ず lint」と書いておくと、置換と同時に bundle exec haml-lint を回してくれます。rubocop / haml-lint / rspec を自走させることで、人間がレビューに到達するときには既にCIグリーン手前まで来ている状態になります。
5-4. PR分割と「流し見」レビュー
一括置換のPRは、目を皿のようにしてレビューしようとすると消耗します。代わりに:
- lintが通っている ことで構文的な正しさを担保
- テスト(特にsystem spec)が通っている ことで動作の正しさを担保
- PRは複数に分割(パイロット → 全画面 → simple_form → シム削除 → 表示崩れ修正)
- 目視レビューは「置換ルールから外れた怪しい差分がないか」のスポットチェックに専念
5-5. 画面確認は人間がやる
CSSの世界は、テストでは検出できない崩れがあります(崩れている=エラーではない、ただ醜い)。人間がブラウザで主要画面を一通り回って確認するフェーズは省略できません。が、Claude Code が主要画面のリストを routes.rb から抽出してチェックリスト化することはできるので、ここも委譲できます。
6. 落とし穴
6-1. btn-info の文字色問題
BS5は色のコントラスト計算を厳密化したので、btn-info などが自動で黒文字になることがあります。手作業でレビューしないと気づきにくい。
-# BS4: 白文字
= link_to '詳細', path, class: 'btn btn-info'
-# BS5: 黒文字になりがち(背景色明るめ判定)
-# 修正:明示的に text-white を付与
= link_to '詳細', path, class: 'btn btn-info text-white'
6-2. ガター(g-* / gx-* / gy-*)
BS5でグリッドのガター指定が変わりました。BS4の mb-3 などで間隔を取っていた実装を機械的に置換すると、なぜか間延びすることがあります。
-# BS4風(残存)
.row.mb-3
.col-6
.col-6
-# BS5的に正しい
.row.gy-3
.col-6
.col-6
このパターンは機械置換だと拾えないので、シム削除後の表示崩れ修正フェーズで個別対応しました。
6-3. Turbo Frame と stretched-link
これはBS5の問題というより、たまたま同時に進んでいたTurbo化と相性が悪かったものです。.stretched-link がページ全体に広がってしまう事象が出て、position: relative を親要素に付けて解決。
.card
position: relative // stretched-link を内部に閉じ込める
6-4. JSの依存
data-toggle="modal" → data-bs-toggle="modal" のような -bs- プレフィックス追加。これはJavaScriptのイベントハンドラやStimulus controllerでデータ属性を使っている箇所も波及するので、grep -r 'data-toggle' のような棚卸しが必要です。
6-5. simple_form の error_notification クラス
simple_form のカスタムwrapper を BS4向けに書いていると、生成HTMLに alert alert-danger が出るが、BS5での適切な表示には alert-dismissible 等の追加が要るケースがあります。実プロジェクトではここで2-3回 review-fix のループが回りました。
7. 数字でみる効果
実プロジェクトの数字です(雰囲気値)。
| 指標 | 値 |
|---|---|
| 期間 | 約1週間(パイロット〜表示崩れ修正完了) |
| 非マージコミット数 | 55前後 |
| 変更ファイル数 | 500前後 |
| 行数 | +2,900 / -1,950 |
| 自分の体感工数 | 「人力換算で1.5〜2ヶ月相当の作業を、1週間で」 |
最大の効率化ポイントは「書く時間ではなく、書いた直後の確認・修正サイクルが短くなった」点です。手作業での一括置換は、置換ミス1個発見するごとに git grep → 修正 → 確認のループに入るのですが、Claude Codeに任せていると 「あ、text-left が3箇所残ってます。直して、テストも通しました」 で終わる。心理的な疲労が違います。
8. ベストプラクティス(自分用まとめ)
- CLAUDE.md を最初に整備する(lint・テスト・コミットメッセージの規約を書く)
- 互換シム を人力で先に作る(AIに「マッピング規則の正解」を渡す)
- パイロット を小さい画面で1本通してから本番投入
- PRは分割:パイロット → 全画面 → ライブラリ調整 → シム削除 → 崩れ修正
- lint・テストはAIに自走させる(差分レビュー前にグリーン手前まで持ってくる)
- 画面の目視確認は省略しない(CSSはテストで担保しきれない)
- シムは必ず最後に消す(残すと永遠に移行が終わらない)
9. おわりに
「AIに丸投げで終わった」とは言えません。むしろ、
- 戦略を立てる
- 互換シムを書く
- パイロット範囲を決める
- 表示崩れの修正方針を決める
といった判断の比率は逆に増えました。一方で、1500行のhaml置換のような単純作業は完全に消えました。プログラマの仕事の中身が「書く」から「決める」へシフトする感覚は、想像していたより気持ちよかったです。
何より大きかったのは、「重くて着手できなかった作業に、着手する判断が下せるようになった」 ことです。今までは「これは1〜2ヶ月かかるから、次のスプリントには入れられない」と棚上げしていた類のタスクが、「1週間ならやれる」に変わる。技術的負債の塩漬け期間が短くなる効果は、コード品質だけでなく、開発者の精神衛生にも効きます。
Bootstrap 5への移行を先送りしているプロジェクトの背中を、この記事が少しでも押せたら嬉しいです。