レガシーなフロントエンドにReact + Web Componentsを導入し、社内デザインシステムを部分的に利用できるようにした話
はじめに
こんにちは。株式会社PLAN-BでCast Me!の開発チームにてフロントエンドエンジニアをしている イ・ヒドと申します。
本記事では、既存のレガシーシステムに React と r2wc ライブラリを活用し、社内デザインシステムを部分的に導入した事例についてご紹介します。
背景
Cast Me!は、今から約4〜5年前に開発が始まり、現在は Laravel + PHP ベースのレガシーシステムと、比較的に最近リニューアル・開発された Next.js + GraphQL ベースの新システムが共存している構成です。
今回の開発依頼は、Laravel で構成されたレガシー領域に「注意メッセージ」を表示してほしいという内容でした。
これまではこのような要望に対して、純粋な TypeScript や HTML/CSS を用いたハードコーディングで対応していました。しかし、レガシーシステムには !important を多用したCSS, 他ページに影響を与えるCSSが多く、予期せぬレイアウト崩れが発生し、Next.js + デザインシステムの環境であれば30分程度で終わる作業が、レガシーでは1〜2時間以上かかることも少なくありませんでした。
このような課題を踏まえ、社内デザインシステムをレガシー環境でも使えるようにしたい、少なくともReactを部分的に導入できれば開発効率が向上し、UIの一貫性も担保できると判断し、Reactとr2wcの導入に至りました。
導入ステップ
レガシープロジェクトにはすでに TypeScript が導入されており、パッケージマネージャーとして yarn v1、ビルドツールに webpack、静的解析ツールに eslint が導入されている状態でした。
Reactの最新版と社内デザインシステムをインストールしました。なお、社内デザインシステムでは Panda CSS の Token 機能が積極的に活用されているため、Panda CSS も併せて導入しました。
また、eslintには React 関連のプラグインを追加し、設定も調整しました。
Reactコンポーネントは既存のTypeScriptファイルとは分離して管理したいと思ったため、resources/src/react ディレクトリを新設し、webpackでReact専用のビルドファイル(react.js)を別に出力するよう設定しました。
...
entry: {
main: './resources/src/index.ts', // 既存のTypeScriptファイル
react: './resources/src/react/index.tsx' // 新しく追加したReactのエントリーポイント
}
...
この react.js を LaravelのレイアウトBladeに <script> タグで読み込むことで、Reactのレンダリングが可能になります。
Reactの部分導入と課題
以下のように、特定のクラス名を持つDOM要素に対してReactをレンダリングするユーティリティ関数を作成し、Reactをページに埋め込む仕組みを試しました。
import { Alert } from '@/react/components/alert'
import { createRoot } from 'react-dom/client';
export const reactRender = () => {
const el = document.querySelectorAll('.react-alert');
if (!!el.length) {
el.forEach((elem) => createRoot(elem).render(<Alert />));
}
}
この方法でも一応実現可能でしたが、以下の課題があると思いました。
- Reactコードが全ページでバンドルされてしまうため、対象のDOMが存在しないページでも不要な読み込みが発生し、バンドルサイズが肥大化される可能性がある。
- どこにReactが埋め込まれているのか初見で判別しづらい
- Propsなどを普通に渡すことが難しい(datasetやjsonを使うことになる)
r2wc の導入
上記課題の解決を検討する中で、ReactコンポーネントをWeb Componentsに変換できる r2wc ライブラリを発見しました。
r2wc検討したところ、Web Component化により、以下のメリットが大きいと判断し、
導入を決定しました。
- Propsを通常のHTMLのattributeとして渡すことができる
- Blade側で <custom-tag /> を使うだけで簡単に埋め込める
- どこにReactのコンポーネントが埋め込まれているのか、判断しやすい("<react-"で全文検索すれば特定可能)
実装コード
type Components = {
[name: string]: (name: string) => Promise<any>;
};
export const defineReactComponents = (components: Components) => {
Object.entries(components).forEach(([name, resolve]) => {
document.querySelector(name) && resolve(name);
});
};
また、バンドルサイズの問題に関しては以下のようにモジュールを非同期で読み込むことで対処しました
import { defineReactComponents } from '@/react/utils/definer';
import r2wc from '@r2wc/react-to-web-component';
// ページに指定タグが存在する場合のみ、対象コンポーネントを非同期で
// importしWebComponent登録
defineReactComponents({
'react-notice': async (name: string) => {
const { Notice } = await import('@/react/components/Notice');
customElements.define(name, r2wc(Notice));
},
});
<div>
<h1>Example Page</h1>
<react-notice></react-notice>
</div>
これにより、Laravel Blade側では <react-notice> タグを記述するだけで、Reactコンポーネントを必要なときにだけWeb Componentとして登録・描画できるようになりました。
発生した問題とその解決
1. CSSのreset.cssの影響
一部のデザインシステムのスタイルが正しく反映されない問題がありました。
最初は Panda CSSの何かしらの設定ミスや、Web Component化によるバグではないかと思いましたが、
原因を調査した結果、レガシー側で使用されていたreset.cssが影響し、スタイルが打ち消されていたことが判明しました。
解決策としては CSSの @layer 機能を活用し、reset.cssを含めている既存style.scssに legacyという命名でlayerを追加し、優先度を下げることで問題を解消しました。
2. CodeBuildにおけるNode.jsバージョン問題
本番環境へのリリースはECS + CodePipeline + CodeBuildのパイプラインを使用していますが、ここでビルドエラーが発生しました。
- CodeBuildのAmazon Linux 2ベースイメージはNode.js 16が最新
- Panda CSSはNode.js 18以上が必要
- Node.js 18の自動インストールはサポートされずエラー
一時は「Node.js 18の要件が満たせないなら、デザインシステムは諦めるしかないかも…」と思いました。
しかし、AWS公式フォーラムの事例を参考にし、Node.js 18を手動でインストールすることで解決しました。
commands:
- wget -nv https://d3rnber7ry90et.cloudfront.net/linux-x86_64/node-v18.17.1.tar.gz
- mkdir -p /usr/local/lib/node
- tar -xf node-v18.17.1.tar.gz
- mv node-v18.17.1 /usr/local/lib/node/nodejs
- echo "export NODEJS_HOME=/usr/local/lib/node/nodejs" >> ~/.bashrc
- echo 'export PATH=$NODEJS_HOME/bin:$PATH' >> ~/.bashrc
- export NODEJS_HOME=/usr/local/lib/node/nodejs
- export PATH=$NODEJS_HOME/bin:$PATH
この方法でPanda CSSを含むビルドが成功し、本番環境にも無事にデプロイができました。
感想・まとめ
今回の取り組みを通じて、レガシーシステムでもReactやデザインシステムを活用できるベースを整えることができました。
特にUIの改修作業については、従来のようにスタイル崩れに悩まされながら1〜2時間かけていたものが、今では30分以内での対応が可能になりました。
また、AWS CodeBuildの制限やNode.jsのバージョンの扱いなど、普段はあまり意識しないAWS側の課題にも触れることで、デプロイパイプラインへの理解が深まった機会になりました。
今後もレガシー環境を含めた開発体験の改善を継続し、安全かつ高速に開発できる環境づくりを進めていきたいと思います。
Discussion