こんにちは。Inflabのフロントエンドエンジニア、GOHSUEです。
現在、Inflabのフロントエンドチームでは、20ほどのアプリケーションで使っているデザインシステムを最新バージョンにマイグレーションする作業に取り組んでいます。
正確に言うと、InflabのデザインシステムはオープンソースのMantine v6をラップしたものなので、既存のデザインシステムをv8に移行する作業でした。
作業の途中でv9がリリースされました。ただ、v8は安定性が確保されており、チームが今抱えている課題もv8で十分に解決できたため、目標はv8のまま変えませんでした。一方で、今後必要になったときにマイグレーションしやすいよう、拡張性を意識して作業しました。
バージョンの数字を1つ上げるだけに聞こえるかもしれませんが、実際にはスタイルの基盤そのものを変える作業でした。そのため、1回目の挑戦では失敗も味わいました。今回の2回目のマイグレーションではその原因を分析したうえで、土台となるコンポーネントライブラリを持続可能な方法で置き換えることに重点を置きました。
この作業は、1人で手を動かして片付けられる規模ではありません。しかも本業の合間を縫って進める必要があったので、なおさらAIエージェントを本格的に活用することにしました。
AIを活用したマイグレーションは、最近多くのチームが試みています。影響範囲の広い基盤ライブラリを移行した事例を記録しておけば、今まさに作業を続けている社内のメンバーや、同様のマイグレーションを検討しているほかの方々の役に立つのではないかと考え、本記事を書きました。
プロダクトの機能を開発しながら技術的負債を返していくのは、エンジニアにとっていつの時代も責務のようなものです。
AI時代になって一番よかったのは、やらなきゃ……やらなきゃ…… と先延ばしにしていたことに、機能開発と並行して取り組めるようになったことだと思います。
本記事は、その道のりを記録したシリーズの第1回です。
1本の記事に技術的な詳細をすべて詰め込むのではなく、まずは**「なぜ」「どのように」にあたる全体の流れ**を追っていきます。途中で行ったトラブルシューティングにも簡単に触れますが、今回は全体の流れに集中します。
つまりこのシリーズは「こうすればうまくいきます」という正解集ではなく、「こうした試行錯誤を経て、こういう選択をしました」という日誌に近いものです。シリーズ全体の地図を描く記事だと思って、気軽に読んでいただければ幸いです。😊
第1部:なぜ変える必要があったのか
既存のデザインシステムについて
冒頭でも触れたとおり、Inflabにはプラットフォームチームがなく、手持ちのリソースで、オープンソースのデザインシステムであるMantineをラップして運用してきました。
このデザインシステムがどんな背景で生まれ、どう運用されてきたのかは、同僚が詳しくまとめた記事があります。初めての方は、先にそちらをお読みいただくのがおすすめです。
まとめると、Inflabのデザインシステムは次のような構成です。
- Mantine v6をベースに、Inflearn・Rallitの共通コンポーネントをパッケージ単位で提供
- コンポーネントごとに
@inflearn/ds-*という形のパッケージを公開し、@inflearn/ds-reactというエントリーパッケージにまとめて使用 - スタイリングライブラリにはEmotionを使用
- マイクロフロントエンド構成で、全社の20ほどのアプリケーションがこのパッケージに直接依存
Inflearnのほとんどのサービスは、このデザインシステムの上で開発されています。
v8に移行しなければならなかった理由
問題なく使えているのに、わざわざバージョンを上げる必要があるんですか?
私たちのチームがv8への移行を決めた理由は次のとおりです。
-
一部コンポーネントが旧バージョンでは未提供
SpoilerやRadioCardのように、上位バージョン(v7以上)では標準で提供されているコンポーネントがv6には存在しないため、似たデザインで独自に実装して保守していました。 -
レスポンシブ対応のDX向上
v6では、レスポンシブ対応のためにEmotionのcss propsでmediaQueryを直接書いていました。v8では各コンポーネントでレスポンシブな値をオブジェクトで指定できるので、スタイルの凝集度を高められます。// Mantine v6 function DemoV6() { return <Box p={24} css={{ [mediaQuery.tablet]: { padding: 16, // ... }, [mediaQuery.mobile]: { padding: 12, // ... }, }} />; } // Mantine v8 function DemoV8() { return <Box p={{ base: 12, md: 16, lg: 24 }} />; } -
TypeScript 5へのバージョンアップ
これまでは、csspropの型が複雑になるとExpression produces a union type that is too complex to representエラーが発生していました。この問題はTypeScript v5以上で起きるもので、Mantineのバージョンを上げれば解消できる見込みがあります。関連Issue -
RSCなどのエコシステムの変化への備え
Mantineのバージョンは今後も上がり続け、いつかはスタイリング技術(Emotion)を置き換える可能性もあります。そのたびにゼロから手探りしないよう、マイグレーションの方法そのものをテンプレート化しておくことが目標でした。v9、v10が出ても同じ流れを再利用できるように構成しました。 -
AIのためのStorybookの構築
既存のStorybookは複数のエンジニアが手作業で作っていたため、書いた人によって書き方がバラバラでした。AIにデザインシステムを参照させる際に混乱を招きかねないため、書き方をルールとして定め、AIで自動化する方向を模索しています。
第2部:1回目の試み、そして失敗から学んだこと
1回目は何を試したのか
チームでは2025年にも、v7へのマイグレーションに一度挑戦しています。
このときは新規リポジトリにだけ事前実験として適用しており、レガシーリポジトリには適用できないという限界がありました。
1回目のやり方には、次のような特徴がありました。
- 既存デザインシステムと新デザインシステムの混在利用は不可(依存関係の衝突)
- yarn → pnpmへの移行
- Emotion(CSS-in-JS) → vanilla-extract(zero runtime CSS)へのスタイル全面移行
- 同じリポジトリ内でアルファ版(
xxx-next.x)として配布
なぜ失敗したのか
最大のボトルネックは、レガシーと新しいデザインシステムを混在させて使えないことでした。内部の依存関係のバージョンが衝突して2つのシステムを併用できず、結局一度にすべて移行しなければならない負担が生じたのです。
そこにyarn → pnpmへの移行、スタイルの全面移行、アルファ版の並行管理といった変更が重なり、マイグレーションの難易度が一段と上がりました。機能開発をしている同僚からすれば、これだけの変化を一度に受け入れながらマイグレーションするのは、どうしても大きな負担でした。
改めて立てた原則:一度に上げようとしない
そこで、まずはデザインシステムを併用できる環境を整え、パッケージを隔離することこそがマイグレーションの出発点だと判断しました。
この方法は、ネットワーキングの場で着想を得たものです。
AIをもっとうまく使おうという思いで、Tossのフロントエンドダイビングクラブ第7回に参加しました。そこでDongYun Kangさんをはじめ、ほかの会社の優秀なフロントエンドエンジニアの方々が、Inflabチームが悩んでいる課題について一緒に真剣に考えてくださいました。今回の方法は、そこから導き出した解決策です。この場を借りて、一緒に考えてくださった皆さんにお礼申し上げます。
// package.json - 1回目のマイグレーション方式(置き換え)
dependencies: {
// "@inflearn/ds-react": "^3.14.0",
// "@mantine/core": "~6.0.0",
// ...
"@inflearn/ds-react": "^4.0.0-next.12",
"@mantine/core": "~8.0.0", // 依存関係まで変更する必要がある
// ...
}
// package.json - 2回目のマイグレーション方式(混在)
dependencies: {
"@inflearn/ds-react": "^3.14.0",
"@mantine/core": "~6.0.0",
// ...
"@inflearn/ds-react-next": "^1.0.0",
// ...
}
1回目が既存のパッケージを置き換える方式だったとすれば、2回目は新しいパッケージを横に追加して混在させる方式です。
バンドルサイズが増えるというトレードオフはありますが、一度に丸ごと作り替えるよりも安定していて持続可能だということで、意見がまとまりました。
2つのバージョンを具体的にどのように隔離し、共存させたのかは、次の第3部でお話しします。
第3部:2回目の試み、共存を設計する
v6とv8を1つのアプリで共存させる
今回は、別の新しいリポジトリ(design-system-next)を作るところから始めました。既存のデザインシステムには直接手を加えず、v8ベースのデザインシステムを独立した場所で作っていく戦略です。
その核となるのが、v6とv8が1つのアプリケーションの中で共存できるようにすることです。全社のアプリを一度に止めて入れ替えることはできません。2つのバージョンが同じページで衝突せずに動かなければ、段階的なマイグレーションは成り立たないのです。
まず、コアとなるContextコンポーネントである ThemeProvider を新たに構成してnpmレジストリに公開し、レガシーと新しいデザインシステムを混在させて使えるかをPoCで検証しました。そのうえで、2つのシステムのスタイルがランタイムで混ざらないよう、2つの隔離の仕組みを設けました。
-
CSSクラスの隔離: v6は
.mantine-Button-root、v8は.mantine-next-Button-rootのように、新しいDSのMantineProviderにclassNamesPrefixを設定してクラス名を分けました。ランダムに生成されるクラス名が衝突することなく、2つのシステムを隔離したまま併用できる構造になりました。 -
Providerの分離: v6用の
ThemeProviderとv8用のThemeProviderを別々に用意し、画面の領域ごとにラップして使えるようにしました。
import { ThemeProvider as ThemeProviderV6 } from '@inflearn/ds-theme-provider';
import { ThemeProvider as ThemeProviderV8 } from '@inflearn/ds-theme-provider-next';
function App() {
return (
<ThemeProviderV8>
<ThemeProviderV6>
<Page />
</ThemeProviderV6>
</ThemeProviderV8>
);
}
依存関係の隔離(peer → dependency)
2つのバージョンが1つのアプリで共存するには、まずデザインシステムが依存している @mantine/core・@mantine/hooks などのバージョンを分ける必要があります。そこで新しいパッケージでは、これらを peerDependencies ではなく dependencies に含め、v8の依存関係をパッケージ内に隔離したまま公開しました。
// package.json - ds-react(既存)
peerDependencies: {
"@mantine/core": "~6.0.0",
"@mantine/hooks": "~6.0.0",
// ...
}
// package.json - consumer: 個別インストール ✅
dependencies: {
"@inflearn/ds-react": "^3.14.0",
"@mantine/core": "~6.0.0",
"@mantine/hooks": "~6.0.0",
// ...
}
// package.json - ds-react-next(新規)
dependencies: {
"@mantine/core": "~8.0.0",
"@mantine/hooks": "~8.0.0",
// ...
}
// package.json - consumer: 個別インストール ❌
dependencies: {
"@inflearn/ds-react-next": "^1.0.0",
// ...
}
既存の ds-react は @mantine/core@6 を peerDependencies にしているため、アプリケーション側で同じバージョンを直接インストールして合わせる必要があります。一方、新しい ds-react-next はv8の依存関係を自前で抱えているので、アプリケーション側でMantineを別途インストールする必要がありません(個別インストール ❌)。おかげで、1つのアプリの中でv6とv8の依存関係が互いに干渉しません。
バンドルサイズが相対的に大きくなるというトレードオフはありますが、この方式のメリットを考慮して、マイグレーション期間中は許容することにしました。パッケージ名も @inflearn/ds-{component}-next というルールで統一し、既存のパッケージと名前が衝突することなく並べてインストールできるようにしました。
なぜスタイルはEmotionのままにしたのか
Mantineはv7からEmotion(CSS-in-JS)への依存を取り除き、ネイティブCSS(CSS Modules・CSS変数)ベースに移行しました。ランタイムでスタイルを生成するコストをなくしてパフォーマンスを高め、バンドルサイズを減らし、React Server Components(RSC)のようにCSS-in-JSが制約される環境までサポートするためです。(Mantine v7 — Migration to native CSS)
実は、1回目のマイグレーションをうまく完了できなかったのは、この流れにそのまま沿ってEmotion(css prop)をvanilla-extractに全面的に置き換えようとしたことも一因でした。スタイルの技術を丸ごと変えるのは、それだけで1つの巨大なマイグレーションだからです。そこで2回目ではスタイリングの方法には手を付けず、新しいデザインシステムでも @mantine/emotion を通じてEmotionをそのまま維持することにしました。
「ネイティブCSSに移行したMantine」の上に「Emotionを載せた」構造になったわけですが、この選択が、後ほど扱うスタイル優先順位の問題の背景になります。
AIを選んだ理由と作業の分類
本題に入る前に、理解しやすいよう一度用語を整理しておきます。
[用語整理]
- 内部マイグレーション: 既存のデザインシステムのコードをv8に移行(ds-react-nextを作成)
- 外部マイグレーション: アプリケーション(Consumer)でデザインシステムのコンポーネントをマイグレーション(ds-react-nextに移行)
内部マイグレーションに向けて、まず既存のデザインシステムを調べました。
コンポーネントは全部で89個あり、そのうちエントリーパッケージに含まれているものは75個でした。これらをv8のAPIに合わせて一つひとつ移行し、動作が変わっていないかを検証し、Storybookまで作り直す。1人で手作業でこなすには、とても無理な量でした。
そこでAIと一緒に、レガシーのデザインシステムのコンポーネントを構成方法に応じて4種類に分類し、チェックリストを作りました。
| 分類 | 説明 | 難易度 |
|---|---|---|
| Type 1 | そのままラップしたもの(v6 → v8 純粋なラッパー / 再export) | ⭐ |
| Type 2 | 既存のソースにカスタマイズを加えたもの(v6 + カスタム → v8 + カスタム) | ⭐⭐ |
| Type 3 | 新たに作ったもの(カスタムだったが、v8では標準で提供) | ⭐⭐ |
| Type 4 | 複数のDSを組み合わせて作ったもの(完全カスタムを維持) | ⭐⭐⭐ |
こうして分類してみると、かなりの数のコンポーネントが 「公式ドキュメントの変更点を読み → 対応するAPIに置き換え → 既存の動作と比較検証する」 という、同じパターンの繰り返しで対応できるものでした。
このパターンなら、AIエージェントで十分に自動化できそうだと考えました。そこで「AIにコードを書いてもらう」と丸投げするよりも、マイグレーションのプロセスそのものを、エージェントがたどれる手順としてドキュメント化する方向を選びました。
マイグレーションの手順は、おおむね次のように定義しました。
分析フェーズ
- マイグレーションするコンポーネントを選定
- 既存のv6コンポーネントの内部ロジックを確認(単純なラップか、カスタムか)
- Mantine v6・v8の公式ドキュメントを比較し、何が変わったかを表にまとめる
- コンポーネントごとのパッケージをTypeで分類
実行フェーズ(Typeごとの内部マイグレーション)
- v8のAPIに合わせて変換
- v6とv8の動作が同じかを比較するテストを作成
- Storybookを追加(基本のStorybook、v6/v8比較のStorybook)
- プレイグラウンドで同じように動作するかを確認
- レジストリに公開
この手順を人ではなくエージェントが実行するようにすることが、2回目の取り組みの核心でした。こうして内部(デザインシステム)のマイグレーションが終わると、これらのコンポーネントを使っているアプリケーションを移行する外部マイグレーションへと続きます。
第4部:エージェントと一緒に移行する
第4部の取り組みでは、バックエンドエンジニアのWoodさんが、AIプロンプトの管理・レガシーDSのサブモジュール構成・公式ドキュメントの抽出など、マイグレーションのインフラを一緒に構築してくれました。
事前作業1. エージェントベースの作業
AIを使った作業では、毎回同じ結果が得られるとは限りません。範囲が広いほど、実行するたびに少しずつ違う結果が出やすくなります。しかしデザインシステムは20ほどのアプリケーションが依存する基盤なので、誰がいつ実行しても同じルール・同じ形に収束しなければなりません。
そこで本格的なマイグレーションに先立ち、エージェントが推測に頼らず、最も正確な一次情報(公式ドキュメント)とチームの規約を参照しながら一貫して作業できる土台を作りました。
-
Markdownのガイドドキュメントの整備
Mantine v6・v8の公式ドキュメントをMarkdownファイルとして抽出し、リポジトリに置きました(2つのバージョンを合わせて500ほど)。そのうえで、コンポーネントごとの変更点・マイグレーション手順・コーディング規約を、エージェントが参照できる形にまとめています。インデックスファイル(manifest.json)も用意し、コンポーネントごとに必要なドキュメントだけを探して読むようにしました。推測ではなく、ローカルに取得しておいた公式ドキュメントを一次情報にさせることで、存在しないAPIをでっち上げるケースを減らしています。 -
マイグレーション手順のコマンド化
先ほど整理した分析/実行の手順を、エージェントがそのままたどれる3つのコマンドファイル(変換・テスト・検証)として固定しました。(現在のClaude Codeではスキル(Skills)に統合されています。)どのコンポーネントも同じ順序・同じ基準で作業させます。何度もはまった落とし穴(例: スタイルの優先順位、アイコンサイズの注入)の解決策もコマンドファイルにルールとして書いておき、同じミスを繰り返さないようにしました。
事前作業2. 検証環境の構築
コンポーネントのマイグレーションと並行して、v6とv8が実際に共存できるかを確かめるため、内部マイグレーションが終わったコンポーネントを検証する環境を作りました。
-
Providerの整備
v6とv8が共存できるよう、v8用のThemeProviderとサーバースタイルの同期の仕組みを整えました。 -
プレビュー用Storybookの作成
v6とv8のコンポーネントを並べて比較するStorybookを作り、マイグレーション後の見た目が既存と同じかどうかを、すぐに確認できるようにしました。エンジニアだけでなく、デザイナーも2つのバージョンの違いをひと目で把握できる環境になりました。 -
v6 ↔ v8の自動比較テスト
目視に加えて、2つのバージョンを同じ条件でレンダリングしたうえでピクセル単位で自動比較するテストを用意しました。一定の割合(10%)以上の差があればテストを失敗させ、マイグレーション中に意図せず見た目が崩れるのを回帰テストのように検出します。この比較は後述する検証(verify)フェーズで自動的に実行されます。自動化がどれだけ速く結果を量産しても、「既存と同じ」と保証できなければ通さない、いわばゲートです。 -
サンプルNext.jsアプリでのテスト
実際のサービスに近いNext.js環境をworkspaceとして構成し、パッケージが実際のアプリで正常に動作するか、混在利用のシナリオで問題がないかをデプロイ前に検証するプレイグラウンドとして使いました。
事前作業3. マイグレーションスクリプトの構成
コンポーネントを1つ移行するだけでも、多くのコンテキストが必要です。既存のv6の実装とコード規約、v6/v8の公式ドキュメント、そして先に移行したコンポーネントで得た教訓まで、毎回改めて揃える必要があります。対象が数十個もある中で、人がそのたびにコンテキストを与えながら一度で終わらせるのは、事実上不可能でした。
そこで、先ほど定義した手順を1つのオーケストレーションスクリプトにまとめました。 コマンドファイルとして固定しておいた3つのフェーズ(変換(migrate) → テスト作成(test) → ビルド・検証(verify))を1つのコンポーネントに対して順番に実行し、通過したら次のコンポーネントに進む方式です。
実際のスクリプトはbashで書きましたが、全体の流れを図にすると次のようになります。オーケストレーターがコンポーネントを1つずつ前述の3フェーズのパイプラインに流し、通過したら次のコンポーネントへ、すべて終わったらレポート出力へ進みます。各フェーズはコンテキストの情報源(公式ドキュメント・サブモジュール)を読み込み、フェーズ間のコンテキストはファイルとして残る記憶(MIGRATION.md・WORK_LOG.md)に書き込んで次のフェーズに渡します。
オーケストレーターの肝は、人が使っていたコマンドファイルをそのままプロンプトに変換し、各フェーズを独立したヘッドレスセッションとして実行することです。1つのセッションで数十個を立て続けに処理するとコンテキストが肥大化して品質が落ちるので、フェーズごとにセッションを新たに立ち上げ、コンテキストを軽く保ちました。検証フェーズでは実際にビルドが通るかを確認し、失敗した場合は決められた回数までリトライします。最終的に失敗したコンポーネントは別途記録しておき、あとから続けて処理します。
セッションは毎回新しく立ち上がるので、各フェーズは必要なコンテキストをその都度読み込み、結果をファイルに残して次のフェーズに渡します。たとえば変換(migrate)フェーズでは、ドキュメントのインデックス(manifest)で対象コンポーネントを確認し → v6の元の実装は別のサブエージェントで分析し → v6・v8の公式ドキュメントのうち対象コンポーネントのものだけを読んだうえで → 変更点を MIGRATION.md(変更仕様書)に書いてv8のコードを生成し → 発生した問題と次のフェーズで確認すべき点を WORK_LOG.md に残してコミットします。
test・verify フェーズも同じ骨組みです。開始時に WORK_LOG.md を読んで前のコンテキストを復元し、MIGRATION.md を拠り所として作業したあと、最後に結果を書いて引き継ぎ、コミットします。特に verify はこれに加えて、ビルドとテスト(先ほど作ったv6↔v8のピクセル比較を含む)を実際に実行して合否をオーケストレーターに返し、オーケストレーターはその結果をもとにリトライするかどうかを決めます。
まとめると、複数のセッションとマイグレーション全体を通してコンテキストを維持するために、
(1) コンテキストはインデックスを経由して、毎回必要な分だけ読み込み(巨大な公式ドキュメントを丸ごと読まない)、
(2) エージェントの「記憶」は揮発するセッションメモリではなく、MIGRATION.md・WORK_LOG.md ファイルとして残して次のフェーズに引き継ぎ、
(3) フェーズごとにコミットして、途中で止まってもgitの履歴から状態を復元できるようにした、ということです。
こうして人は何をどの順番で移行するかを決め、結果をレビューすることに集中し、繰り返しの変換・テスト・検証はオーケストレーターに任せる構成にしました。実行も、次のようにコマンド1行で済みます。
# Type 1(純粋なラッパー)のうち、まだ移行していないものだけを自動処理
./scripts/migrate-components.sh --type 1 --auto
# 特定のコンポーネントだけ(変換 → テスト → 検証)
./scripts/migrate-components.sh Button Input Modal
内部(デザインシステム)のマイグレーション
事前作業が終わったあと、デザインシステムのリポジトリ内のコンポーネントを、エージェントを使ってマイグレーションし始めました。
最もシンプルな Type 1(純粋なラッパー) コンポーネントから始め、手順が安定して動くことを確認したうえで、徐々に難易度の高いコンポーネントへと範囲を広げていきました。
エージェントはコンポーネントごとに、前述の手順に沿って変換・テスト・Storybookの作成を行います。人はそのStorybookで結果をレビューし、判断に迷う部分はデザイナーと一緒に確認していきました。AIが繰り返し作業を処理し、人が意思決定を下すという分業が定着し始めた段階です。
外部(アプリケーション)のマイグレーション
デザインシステムの内部コンポーネントがある程度v8に移行できたら、次はそのパッケージを使っているアプリケーション(Consumer) を移行する番でした。
ここで中心となったツールがcodemodでした。単純なimportパスの変更からコンポーネントのpropの変換まで、アプリケーションのコードに繰り返し出てくる変更を、ASTを解析するスクリプトで自動化しました。AIを活用して自分たちで設計・実装し、まずサンプルリポジトリに適用してQAしながら磨き上げました。
// Before
import { Button } from '@inflearn/ds-react';
<Button leftIcon={<Icon />} compact>Click</Button>
// After(codemod実行後)
import { Button } from '@inflearn/ds-react-next';
<Button leftSection={<Icon />} size="compact-sm">Click</Button>
codemodは @inflearn/ds-codemod という別パッケージとして作り、各アプリケーションで次のように適用できるようにしました。
pnpm exec ds-codemod init # 設定ファイルを生成
pnpm exec ds-codemod # 変換を実行(--dry でプレビュー可能)
初期化スクリプトでpolyrepoやmonorepoといった環境に合ったYAMLを自動生成し、特定のコンポーネントだけ、あるいはすべてのコンポーネントをマイグレーションできるようにしました。
# ds-codemod.config.yaml
# @inflearn/ds-codemod の設定(Polyrepo)
# マイグレーション対象のパス
target: ./src
# マイグレーションするコンポーネント(すべて: components: all)
components:
- Button
- Text
- Input
- Select
- Checkbox
- Radio
- Switch
- ButtonGroup
- IconButton
css-to-styles:
components: all
# オプション
options:
dry: false
もちろん、codemodがすべてを解決したわけではありません。アプリケーションごとに環境が少しずつ異なっていました。その違いから生じた問題は、後述の「遭遇した主な問題」で簡単にまとめます。
進捗管理:ダッシュボードと優先順位
マイグレーション対象が20を超えるため、チームに共有するには、今どこまで進んでいるのかをひと目で把握できることが重要になりました。
そこで Claude(AnthropicのAIコーディングツール) を使って、各アプリケーションのマイグレーションの進捗状況(開発中 / 本番反映など)を一覧できるマイグレーションダッシュボードを作りました。作業を人の頭の中だけで管理せずに可視化しておいたことで、優先順位を決めたり漏れを防いだりするのに大いに役立ちました。
移行する順番にも戦略が必要でした。コミット数が少ない(=相対的に変更が少なくシンプルな)アプリケーションからマイグレーションしました。理由は以下のとおりです。
- リスクを下げるためです。シンプルなアプリから移行すれば、手順の穴を小さな範囲で先に見つけられます。
- 手順を磨き上げるためです。序盤に遭遇した問題とその解決策をcodemodやガイドドキュメントに反映しておけば、あとに続く複雑なアプリでは同じ問題に悩まされずに済みます。
小さなアプリで検証された手順が徐々に固まっていったことで、後半にはより大きなアプリも比較的スムーズに移行できました。
マイグレーションダッシュボードでは最新のコミット日を取得し、優先順位ごとにセクションを分けて、すぐに確認できるようにしました。
QAとcodemodの強化
自動化で移行した結果であっても、最終的に実際の画面が既存と同じかどうかは人の目で確認する必要があります。
特に、スタイルの優先順位やメディアクエリのように微妙にずれうる部分は自動テストだけでは捉えにくいため、社内のメンバーと一緒に主要な画面のQAを行いました。
QAをしていくうちに、特定のアプリケーションで繰り返し現れる問題のパターンが見えてきました。代表的なのが、Emotionの css propのスタイル優先順位の問題です。v8 + Emotionの環境では、アプリケーション側で css propで指定したスタイルがコンポーネントの styles APIよりも優先順位が低く、意図したスタイルが適用されないケースがありました。
こうした問題は1か所直せば終わりというものではないので、codemodに新しい変換ルールを追加し、css propを styles.root に自動で移し替えるようにしました。
// Before
<Text css={{ flexShrink: 0 }}>...</Text>
// After (css-to-styles codemod)
<Text styles={{ root: { flexShrink: 0 } }}>...</Text>
複数のリポジトリを点検しながら見つけたパターンをcodemodに取り込む作業を繰り返すうちに、一度直した問題には次のリポジトリで再び悩まされないという好循環が生まれました。
遭遇した主な問題
通常業務を回しながらv6とv8を共存させていたため、予想していなかった落とし穴も少なくありませんでした。一つひとつ深く掘り下げると記事がかなり長くなってしまうので、同じ道を歩む方々のために、特に印象に残った問題だけを簡単に残しておきます。
| 問題 | 症状 | 原因 | 解決 |
|---|---|---|---|
| TS5専用の構文 | v8のビルド時に TS1139 の型エラー |
Mantine v8の型がTS5の構文(const T extends string)を使っているが、私たちのパッケージはTS4 |
pnpm patch で node_modules の型宣言を一時的にパッチ(バージョンアップ後に削除予定) |
| app-shell(module-federation)のシングルトン衝突 | MANTINE_COLORS is not iterable |
app-shellが @mantine/core をシングルトンとして宣言していたため、すべてのimportが最も高いバージョン(v8)を参照 |
coreのシングルトン宣言を削除(すでにシングルトンである ds-react がv6を間接的に保証) |
| pnpmのpeer chain | v6/v8の共存時、peerDependencyのdepthが深いと誤った依存関係を参照 |
notifications・modals のpeerであるcoreが、peer chainをたどってroot(v6)まで上がってresolveされる |
peer chainの途中をdependencyとして宣言し、chainを断ち切る |
| ESM専用パッケージ + dynamic import | モーダルなどの遅延ロード時にランタイムエラー | Next.jsの dynamic import が内部的にCJS(require)を使っているのに、依存パッケージがESM専用 |
CJSをサポートする下位バージョンを使うか、ネイティブAPI(URLSearchParams など)で代替 |
| css propの優先順位 | アプリケーション側で指定した css のスタイルが適用されない |
v8環境では、コンポーネントの styles APIがEmotionの css propよりも優先順位が高い |
css → styles.root へ移し替えるcodemodを追加 |
| スタイル崩れ / FOUC | 初回レンダリング時のちらつき、一部のスタイル崩れ | Emotionのサーバースタイル同期の漏れ、必須CSSのimport漏れ・順序の問題 | サーバースタイル・Providerを点検し、必須CSSのimportと適用順序を確認 |
この表からもお察しいただけると思いますが、2つのバージョンを共存させるために生じた依存関係の解決(resolve)の問題と、ネイティブCSSに移行したMantineの上でEmotionを維持したことで生じたスタイル優先順位の問題が、最も厄介でした。
第5部:振り返り
第一段階の完了
こうして、@inflearn/ds-react を使っている主要なアプリケーションのv8へのマイグレーションに、ひと区切りをつけることができました。
現在の作業は「完了」というより「大きな山を1つ越えた」に近い状態です。まだ既存のデザインシステムを完全に取り除く作業、依存関係の整理、Storybookの体系化などの後続作業が残っています。それでも、以前のように個人の時間をたくさん費やすことなく、全社規模の基盤ライブラリをv8に移行する第一段階を終えられたのは、意味のある節目だと考えています。
AIは土台があってこそ同僚になる
最も強く感じたのは、AIエージェントは、よく整理された手順とドキュメントの上で、多少の融通が必要な繰り返し作業においてこそ真価を発揮するということでした。
AIに漠然と「マイグレーションして」と任せるのと、手順・検証・自動化ツールを整えたうえで任せるのとでは、結果の安定性がまったく違いました。
プラットフォームチームのない組織にはデザインシステムのメンテナーがいないので、今後もこうしたルールや土台が重要になると考えています。そんな中で、AIは一貫性のあるコードを生み出すことで、誰もがメンテナーになれる環境を提供してくれます。
完璧なマイグレーションはありません
通常業務を続けながら完璧なマイグレーションをやり遂げることは、事実上不可能です。私たちにできるのは、エラーが発生する確率を下げるプロセスを繰り返すことだけです。
サービスは継続的にデプロイされ、アプリケーションごとに環境が異なり、ライブラリのバージョンもまた上がっていきます。こうした環境では、「一度に完璧に」はコードの規模が大きくなるほど難しくなります。
そのため、リスクを最小限に抑えるプロセスに重点を置きました。リスクを細かく分割し(シンプルなアプリから)、見つけた問題を自動化に取り込み(codemod)、最終的に人の目で確認する(QA)というサイクルを繰り返しました。完璧を追い求めるよりも失敗の確率を着実に下げていくほうが、通常業務を続けながら大きな変化を生み出すための現実的な方法でした。
今後の作業
第一段階を終えたあとに残っている作業は、おおむね次のとおりです。
-
既存デザインシステムの削除:
@inflearn/ds-reactなどのv6パッケージを依存関係から完全に取り除く - 依存関係の整理: 隔離のためにdependencyにしていたMantineのパッケージを再びpeerに戻し、モノレポ全体でバージョンを一元管理する
- Storybookの体系化と公開
- TypeScript 5へのアップグレード
-
レガシースタイルへの対応: Emotionの
csspropなどを段階的に整理する
まだ越えるべき山は多いですが、着実に負債を解消していくこともエンジニアの役割だと考えています。
本記事は、AIを活用したデザインシステムマイグレーションシリーズの第1回でした。続編では、後続作業の進み具合に合わせてその話もお届けする予定です。
長文を読んでいただき、ありがとうございました。忙しくプロダクトを開発する中でも、チームの技術的負債を解消しようとしている素敵なエンジニアの皆さんのお役に立てれば幸いです。
※この記事は2026年6月6日に執筆されたものです。






