これは エムスリー Advent Calendar 2021 の10日目の記事です。
エンジニアリンググループの山本です。 業務ではエムスリーデジカルというクラウド電子カルテの開発をしています。 今回はデジカルのフロントエンド(AngularJSとReactで構成されています1)にデザインシステムを導入した話を書いてみました。
デジカルのフロントエンドには汎用的なUIコンポーネント集がありませんでした。 そのため、UIの実装に必要以上の工数がかかる上、デザインの一貫性も保ち辛い状態でした。しかし、開発メンバーの増加やデザイナーのジョインもあり、ついにデザインシステム導入をすることになりました。
完璧を目指さない
デザインシステム導入にあたり世の中の導入例を調べると
- コンポーネントは別packageとして作りたい
- コンポーネントのテストを書きたい
- Storybook等でカタログ管理をしたい
- ドキュメントもちゃんと作りたい
など、色々とやりたくなってしまいます。しかしチームの規模、開発リソース、タスクの優先度等を考えると、全てやるのは現実的ではありませんでした。頑張るのを諦めます。
課題感を整理したところ、下記のような開発体験の悪さが一番のツラみであることが分かりました。
どのコンポーネントを使っていいのか分からない
ButtonやTooltipといったコンポーネントが各所に複数定義されており、何を使ったらいいのか分からない状態でした。また、どのコンポーネントがあるのかが把握しにくい状況でした。
TypeScriptの型定義がない
プロジェクトにTypeScriptが導入されたばかりというのもありますが、型定義がなく新規tsxファイルの書き心地が悪い状況でした。
既存のコンポーネントが使いにくい
デジカルにはReact Bootstrapが入っていますが、バージョンが古い(v0.32.4, 執筆時点の最新版は v2.0.3)こともあり非常に扱いにくい状況でした。(まだまだ不具合が多い、ドキュメントが不十分、公開されてるInterfaceが古い等)
そして以下のように課題を解決しました。
digikar-uiを作ります。といってもpackageは分けないためsrc/digikar-uiディレクトリを作っただけです。
この直下にコンポーネントを集約していけば、どのコンポーネントを使えばいいか迷うことは無くなります(当たり前)。
Interfaceを考える
将来的に内部実装を変えられるように特定のライブラリに依存しないInterfaceをdigikar-uiとして定義しました。
Interfaceは使い勝手にも関わる大事な部分なので、世の中のUIライブラリ等の実装を参考にしつつメンバーと相談して決めました。
次に、既存の散らばったコンポーネントを、作成したInterfaceでラップしdigikar-uiとして扱えるようにしました。
// 非推奨です。 digikar-ui の Tooltip コンポーネントを使ってください。 export const LegacyTooltip = () => ()
// digikar-uiの新しいInterfaceで使えるようにする export const Tooltip: ({ message }: TooltipProps) => { return <LegacyTooltip content={message}> }
Exampleコンポーネントを作る
digikar-uiでは以下のようなExampleコンポーネントを作りました。 これをwebpack-dev-serverで提供し、一覧表示と動作確認をできるようにしました。
export const DropdownExample = () => { const [open, setOpen] = useState(false); return ( <Dropdown open={open} onToggle={setOpen}> <Dropdown.Toggle> <Button>Open</Button> </Dropdown.Toggle> <Dropdown.Menu> <Dropdown.MenuItem>aaa</Dropdown.MenuItem> <Dropdown.MenuItem>bbb</Dropdown.MenuItem> <Dropdown.MenuItem onClick={() => console.log('ccc clicked')}> ccc </Dropdown.MenuItem> </Dropdown.Menu> </Dropdown> ); };
一部抜粋
Storybook等の管理サービスは導入コストやメンテナンスに工数が割けないことを考え、現時点で導入は見送りました。(必要になったタイミングで導入を再検討すれば良さそうです) しかし上記Exampleでも十分に課題を解決しています。(デザイナーもローカルで確認できたり、実装例が分かったり等)。
最初から完璧を目指さなかったこと、開発体験向上にフォーカスを当てたことで、短期間でコンポーネントの整備が進みました。
実装方針
最後にもう少し具体的な話を書いておきます。
デジカルにはReact Bootstrapが導入されているため、可能な限りそちらをラップして使うようにしました。
先述したような扱いにくさやアクセシビリティの低さ等の問題もあり積極的に使いたいものではなかったですが、これが一番現実的と判断しました。
その他検討したこと
自分で作る
- 楽しそうではありますが真面目に作ると工数がかかりすぎます。OSSを使わない選択肢はありません。
Bootstrapのバージョンを上げる
- 影響範囲がでかいというのもありますが、AngularJSでもuib-bootstrapが導入されてる兼ね合いでバージョンアップはかなり厳しい状況です。
Chakra UIを使う
Headless UI を使う
- 依存ライブラリは被ってなかったので導入はできそうでしたが、工数面であまり恩恵を得られなさそうだったのと、まだまだスター数が少なかったです。
ざっくり紹介
色とかspacingとか
- すでに存在するSCSSの資産を活用し、 p-sm等のUtiltyクラスを生成しました。
<Box p="sm"/>
のような書き味をstyled-systemのようなもの使って定義するか迷いましたが、工数面やAngularJSからも使えるといったメリットを考慮し、こちらはclassで指定することにしました。
- すでに存在するSCSSの資産を活用し、 p-sm等のUtiltyクラスを生成しました。
Button, Checkbox, Radio等,
- アクセシビリティを配慮しながら CSS in JS (styled-components)を使ってスタイルを定義しました。
- Chakra UIやSpectrum等を参考にしました。
Modal Popover Dropdown
Bootstrapのラッパーです。 素直にラップできない場合はソースコードに直接手を加えました。この作業が一番辛かったです。
Icon
svgrを使って、SVGファイルから生成したコンポーネントをラップして作りました。 従来はアイコンフォントを使っていたのですが課題が複数あったため新しく作りました。
Toaster
react-hot-toastを使いました。useStateの戻り値をpackage内のglobal変数で管理するという実装のおかげで、Reactのツリーの外(AngularJS側)からも使えたりします。
まとめ
単にプロジェクトのUIコンポーネントを整備したという話でした。
現状も課題は残っていますがフロントの開発体験は大幅に向上し、開発工数も大幅に削減することができました。
最初から完璧を目指さなかったことで短期間3 で一定の成果を上げることができ、結果的に他のタスクに取りかかることがでて良かったです。
We are hiring!
エムスリーでは一緒に開発する仲間を募集しています。No1クラウド電子カルテの開発に興味がある方はカジュアル面談でお話しませんか?