株式会社ヘンリー エンジニアブログ

株式会社ヘンリーのエンジニアが技術情報を発信します

株式会社ヘンリー Advent Calendar 2025 の感想

株式会社ヘンリーで SRE をしつつ、技術広報的なこともしている id:nabeop です。

2025年も会社でアドベントカレンダーを企画しました。2024年までは1トラックでの開催でしたが、2025年は2トラックでの開催をして多くのメンバーに参加してもらえるようにしました。

続きを読む

入社半年、はじめての医療ドメイン理解を振り返る

【これはHenryアドベントカレンダー 2025 シリーズ 1 における22日目の投稿です。昨日の記事は俺たちの成長は止まらない|Sugimura Masashiでした。】

はじめまして、6月にヘンリーに入社したエンジニアのichienです。初めてブログを書くので、半年遅れの入社エントリーになります。

私はこれまで地図や会計プロダクトでの経験を経て、今回、新しく医療ドメインにチャレンジしています。開発では主に医事(医療事務)会計領域を担当しています。

入社して半年、過去の学習経験を活かしながら医療ドメインのキャッチアップに取り組んでいますが、自分なりに理解するまでに時間がかかることが多く、試行錯誤しながら取り組んでいます。 前職ではマネジメント多めのEMロールだった所から、今のヘンリーではエンジニアにフルコミットしており、利用技術周りのキャッチアップにも取り組んでいます。 ライフステージの変化と共に利用時間に制約が増えていく中で、「いかに学習の質を高め、最速で学びを積み上げられるか」という戦略の大切さを感じている所です。

この半年でドメイン理解のために取り組んだことを振り返り、今後のアクションについて考えてみます。

なぜ医療ドメインにチャレンジしたのか?

子どもが成長過程で社会福祉機関による公共の支援や医療サポートを受ける機会がありました。 私にとって初めての経験で、地域で支え合う仕組みがあることにありがたいと思うと同時に、一人のエンジニアとしてこのような公共の仕組みに何か貢献できないだろうかと考え始めたことがきっかけでした。

そんな中、理想駆動で社会課題に真正面から立ち向かっているヘンリーの考え方に共感し、飛び込んでみました。

巨大で複雑なものは難しいけど面白い

当初は「どんなドメインでも、学んでいけばなんとかなるだろう」と楽観的に考えてました。しかし、国民皆保険制度に基づく医療費請求の仕組み、国が定める医療費の根幹を貫く診療報酬制度、外来診療/入院を含めた病院業務の複雑さを一歩一歩理解するにつれて、奥深すぎて一筋縄ではいかないなと考え直しました。

未知で”わからなかった”ことを学び、解像度を上げていくことは面白いです。 "わかる”状態が増えると、自身の成長を実感できて、楽しいのです。 更にそれが難しいことほど、わかった時の成長実感が大きく、より面白くなっていきます。

こんな気持ちで複雑な領域に私は向き合っています。ここから半年間の取り組みを振り返っていきます。

半年間の取り組みと振り返り

1. 医療事務の入門書を読む

最初はどこから手をつけるべきか迷子状態で会話内での単語やコードベースの命名やロジックを見ても、"わからないこと"が"わからない"状態でした。 まず書籍から全体像を理解しようと思いました。

一通り読んでちょっと分かった気持ちになった気がします。 しかし、「書籍で読んだあの内容のことか!」と理解がつながる瞬間があまりなく業務中で活かせてる実感がなかなか得られませんでした。 一度読んで終わりでなく、頭にインデックスを張っておき、思い当たった時に該当部分を読み直すぐらいの温度感でいると良さそうです。

「大量の"わからない"ことがあることが"わかった"状態になりました。」

2. AIで調べる

今はAIがあるので、フル活用したらドメイン理解に費やす時間をショートカットできるのではと考え、知らない用語が出てきたら即AIで調べるようにしていました。 これは学習のフィードバックループを高速に回せそうです。

「"わからなかったこと"を"わかった"気になる状態になりました。」

振り返ると、とても便利で調査負荷を圧倒的に圧縮できていたと思います。 ただ、AIとのやり取りは基本的にクローズドなので、正しい意味で"わかった"のか客観的に判断できず、自身で自信が持てない状態でもあったと思います。 自身でAIで調べる・周囲の人に聞くことは、それぞれ一長一短ありそうです。

3. 病院訪問

半年間で2件の病院を訪問させてもらい、医事課の方々が働く現場を見学させていただきました。 現場での実体験によって、初めて具体的に頭の中でイメージできるようになった感覚がありました。例えば、「外来受付には一台の共有PCが置かれており、画面全体に開かれたブラウザ内でHenryが常に表示されている」の様な現場状況を含めたイメージです。

N=1であっても自分の中に参考となる「絵」が持てると、その後の設計や議論の解像度が大きく変わってきます。これは前職でも経験あったこともあり、自身の中で再現性が高そうです。積極的に現場訪問の機会を得られる環境があるのはとても良いなぁと思っています。

4. エピックオーナーに挑戦

入社して2ヶ月ぐらいで、担当する医事領域の機能開発でエピックオーナーに挑戦しました。

ヘンリーでは、エンジニアリングチームが主体となってプロダクトの仕様を意思決定します。 ユーザーの課題を真に解決するために、社内のドメインエキスパートや医療従事者の経験者などと協働し、時にはエンジニアがリードしてディスカバリーからデリバリーまでを一貫して担います。

この役割を「エピックオーナー」と読んでいて、社内には挑戦を後押しする文化があります。 この役割の存在が、ヘンリーへの入社を決めた一つの理由でもあります。会社の説明資料に記載があるので、ピックアップしておきます。

挑戦する楽しさとドメイン理解が十分でない状況で担えるかどうか不安な気持ちと両方の気持ちが交錯していました。

ドメインエキスパートやサポート、Bizメンバー、デザイナー等多様なロールと協働し、フィードバックを得ながら意思決定を積み重ねることで知識不足を補っていきました。重ねるごとに、徐々に自分で提案できることが増えていきました。

リリース後には、実際に利用している医療機関様にインタビューをさせてもらい、現場の声を直接得ることで、自分達で価値の検証を行いました。

この経験から、ドメイン理解の学習においても学習効率を最大化するのは、「適切な課題設定」と「その解決のためなら何でもするというオーナーシップ」なのだなぁと思いました。 まずやってみること大事。

これからは「点」から「線」で理解していきたい

半年振り返って、ドメイン理解にしても業務タスクにしても、目の前の課題の解決のために必要な知識の学習に多くの時間を使った感覚があります。 複数の取り組みから多くの情報を「点」で学習できたが、どうも自分の中で「点」と「点」をつなげて整理できてる感覚がまだまだ薄いです。 一方で「点」での理解を進めたことで、一定の成果を作ることができたとも思っています。

これからは長期的な視点を持って、学んできた「点」をつなげ、体系的に「線」で理解するためにアクションを増やしてゆく予定です。例えば、以下の様なことを考えています。

  • 医療事務の経験者やドメインエキスパートと積極的に話し、自分の理解に対して壁打ち等を通してフィードバックをもらう
  • 社内で得た情報をつなげるため、読むターゲットを社内ドキュメントに絞って幅広く読む
  • 厚生労働省が公開している資料などの一次情報の読み方を学ぶ

さいごに

ドメイン理解の進め方はまだまだ探索しながらなので、定期的に振り返っていきたいです。 私の半年振り返りを最後まで読んでいただきありがとうございました。

もしヘンリーに興味をもっていただけた方がいたら、ぜひお話しましょう!

jobs.henry.jp

みんなが考える難しさはどこにある? アーキテクチャ Conference 2025 のアンケート結果発表

ヘンリーで SRE をやっている id:nabeop です。

この記事は株式会社ヘンリー - Qiita Advent Calendar 2025 シリーズ2の10日目の記事です。

ヘンリーは2025年11月20日と21日に開催されたアーキテクチャ Conference 2025 で Gold スポンサーとしてスポンサーブースを出展しました。

ヘンリーブースの様子

会期中はたくさんの方にヘンリーブースに足を運んでいただき、ありがとうございました。ブースでは「アーキテクチャ」をキーワードに2つのアンケートを実施していました。

  • サービスは分ける?まとめる?
  • どの業界が一番設計しにくい?

今回はそれぞれのアンケート結果をみながら、会期中の様子などについてまとめます。

続きを読む

Server-Side Kotlin Night 2025を開催しました!

2025年11月25日、ヘンリーで「Server-Side Kotlin Night 2025」をKotlin Fest 2025の非公式アフターイベントとして開催しました!

henry.connpass.com

2024年に続き2回目の開催となります。
今回はそのイベントのセッション内容と、当日の様子をご紹介します。

セッション内容

AmperでKotlinのエコシステムを簡単キャッチアップ

登壇者:
一円 真治 (ichien)氏 株式会社ヘンリー

speakerdeck.com

KotlinConf 2025のOpening keynoteでも扱われ、聞いたことはある人が多くいるもののまだあまり情報の出回っていないAmperについてのセッション。
Gradleとの比較や実際に使ってみたデモもあり、とても貴重な内容です。

SNS上での反応

  • Amper、とっつきやすそうな yaml ファイルの形式だ
  • amper showでモジュール構成とか見れるのおもろい、良さそう
  • 開発環境の簡易化が開発ハードルが低くなるので、エコシステムが広まって欲しい

サーバーサイド Kotlin を社内で普及させてみた

登壇者:
川田 裕貴氏 株式会社マネーフォワード

www.slideshare.net

マネーフォワードさんでサーバーサイドKotlinを普及させた経験を元に、技術選定の難しさや実際に組織内から上がってきた意見などが紹介されています。
これからKotlinを導入したいと考えている方には必見の内容です。

SNS上での反応

  • Kotlinの技術選択めっちゃむずかしいのわかる...
  • coroutinesを活用するかどうか問題も、確かに
  • exposedも気になっているけどjOOQも良い
  • jOOQ悪くないけど、Pure Kotlinの夢はいつも見る

Why Kotlin? 電子カルテを Kotlin で開発する理由

登壇者:
縣 直道氏 株式会社ヘンリー

speakerdeck.com

ヘンリーでなぜKotlinを使用しているのか、その背景にある医療ドメインの難しさや、技術選定の理由について解説しています。
1つ前のマネーフォワードさんでの事例と併せて、Kotlinを導入するメリットを知れる内容になっています。

SNS上での反応

  • 病院ドメイン複雑そうだ…
  • Pure Kotlinの世界線に賭けた技術選定だ
  • ヘンリーさんがきっとGraphQL Kotlinの後継作ってくれるんだろうな

KotlinでミニマルなResult実装による関数型エラーハンドリング

登壇者:
lagénorhynque / カマイルカ氏 株式会社スマートラウンド

www.docswell.com

独自のResult型を実装し、エラーハンドリングを改善した事例を紹介しています。
Kotlinではエラーを例外で扱うことも多いですが、新たな選択肢としてとても興味深い内容になっています。

SNS上での反応

  • try/catchから離れて、ResultやEitherに慣れると後者の方が楽に感じてくるの分かる
  • 標準ライブラリの Result が他言語で Result / Either に慣れてる人から見るとめっちゃ独特でむずいんだよな
  • 「テストで便利」わかる。テストが簡潔になるのも大きなメリット

【パネルセッション】IntelliJ IDEA 以外での Kotlin 開発のリアル

登壇者:
nabeo (渡辺 道和)氏 株式会社ヘンリー
渡邊 泰紀 a.k.a yasunori氏 株式会社ログラス
髙野 氷河氏 Sansan株式会社
竹端 尚人氏(モデレーター)

KotlinをIntelliJ IDEA以外のエディタを使って書いている3名のエンジニアが集まり、パネルセッションを行いました。

  • VS Code、Neovim/Vim、Emacs 事情
  • VS Code、Neovim/Vim、Emacsを使っていてよかったと思ったことある?
  • ぶっちゃけ、IntelliJ IDEA が羨ましくなるときある?

という3つのテーマで語り合い、VS CodeとVimとEmacsの利用者が一同に介して同じテーマについて語り合うという、なかなか見れない貴重な機会となりました。
IntelliJという強力なIDEが存在する、Kotlinならではの切り口のセッションだったと思います。

SNS上での反応

  • VS CodeでKotlin書こうと思って挫折したので、すごい興味ある
  • VS Code での Kotlin 開発は本気で気になる。特に kotlin-lsp が出てきた後
  • シンタックスハイライト周りで文句はないけど、デバッグとか少し込み入ったことをし始めたときに挫折した
  • やっぱ「いけなくはない」という状況なのか…
  • Shift 2回のようなコマンドパレット系の機能は便利、わかる(IntellliJに限らず)

会場の様子

ウェルスナビ株式会社様に素敵な会場をご提供いただき、セッションも懇親会も大変盛り上がった会になりました。

当日の様子はXでハッシュタグ #KotlinFest_SSK でも多く投稿されているので、こちらもぜひご覧ください。

今後もサーバーサイドKotlinに関する発信を続けていきます!

ヘンリーでは今後もサーバーサイドKotlinに関して、ブログやイベントを通して発信を続けていきます!
また様々な形で主催イベントも開いていこうと考えていますので、その際はぜひお越しいただければと思います。

わけのわからないバグに遭遇したときの生き抜き方

株式会社ヘンリーで医療会計部のエンジニアの一條(GitHub: @rerost , X: @hazumirr)です。

これはHenryアドベントカレンダー 2025 シリーズ 1 における7日目の投稿です。昨日の記事は スタートアップで経営を執行から引き剥がした10ヶ月間 でした。

僕は今まで、

  • 複雑な診療報酬をちょっとずつ理解しながらの開発
  • インシデント時のコマンダー
  • ロジックが複雑かつ機械学習を入れた検索システムの構築(前職)

といったことをしていました。 なので、わけのわからないバグに遭遇する機会は多い方だったと思います。 この経験の中で僕が実践していたことについて書きます。

目次

バグ自体の分類

割とここが肝だと思っていて、バグ自体が分類できたら対応の9割は終わったと言っても過言ではないと思います。 僕は、発生源・影響度の二軸で考えています。

緊急度

緊急度はバグ対応中にかなり早くに明確にしておくと対応がスムーズです。

  1. リリース済み機能でユーザーの行動がブロックされることがあり、即座に対応しなければいけない
  2. リリース済み機能でユーザーでの回避策などがあるが、早めに直さないとユーザーに手間を取らせてしまう
  3. 未リリースなどでユーザーに影響しない

1なら、まずは回避策を練るか一次対応するなどで根本対応より先にでること。対応へのリードタイムを減らすためにも、人を集めましょう。 2なら、落ち着いて原因を探りつつ、ユーザーへの案内などを速やかに行いましょう。 3なら、ゆっくり調べましょう。

といった具合です。

バグの発生源の種別

発生源は多くの場合、以下のどれかに分類されるかと思います。

  • 実装のみからくるもの。例: 外部ライブラリの使い方やアップグレードなど
  • 現状動いているものと、以下のどれかとのズレ
    • 過去の仕様
    • 関連するルールや法律など
      • 例: 診療報酬制度

2つ目の部分の説明をしていくと、まず具体例としては以下があります。

発生源
過去の仕様 昨日まで検索でヒットしていたものがヒットしなくなった。
先月まで自動で入力されていた部分が入力されなくなった。
関連するルールや法律など XXを算定すると、自動でYYも算定されるはずだが、算定されない。

このどちらかが、実装とズレていることでバグは発生します。

実装とどれかが異なるか

更にたどると、実装者がどれとズレているか、実装者の意図しない実装になっているのか、もしくは実装者自身が過去仕様・関連するルールの理解がズレているかがあります。

実装者と何がズレているか

これらの要因は、実装者のバグが入ったPRの文章やバグを報告してくれた方に話を聞いていくことで多くの場合は特定できるかと思います。

対応の流れ

  1. 再現する
    1. 手元環境で再現する
    2. 具体のバグ発生パターンをいくつか集める
  2. 原因を特定する
    • 状況を言葉にし、まとめる
    • 関連するものを図に起こす
    • 発生しないという主張・発生するかもしれないという主張、を交互に繰り返し出し続ける
  3. 修正する

再現する

手元で環境構築などをし、報告されたスクリーンショットや報告内容から同じ状況を再現し、試しましょう。

うまく再現できない場合、弊社の開発する電子カルテ・レセコンだと、医療機関の設定や患者の状態(過去の操作)が影響するので、以下を確認するのが良いです。

  • 医療機関設定
    • 病棟・病床の設定
    • 都道府県
    • 連携する検査機関の設定
    • 機能の有効無効
    • etc...
  • 患者の状況
    • 過去算定されたもの(※ 制度上、過去の会計データに依存するロジックなどがある)
    • 保険
    • etc...

また、報告されたケースと類似のバグが再現するケースを集めておくと、修正時のチェックで役に立つのでおすすめです。

原因を特定する

原因の特定ですが、まずはここまで得られた情報を元に、特定していきましょう。 無論、ここまで集まった情報で原因特定が済んでいるのであれば、スキップしましょう。

コードなどを読みながら、状況を言葉や図にしてまとめていくのが肝で、わけがわからない状況のとき、言葉や図にしておくことで頭で抱え続ける情報を減らすことができます。

また、自身で

  • このバグは発生しない。なぜなら〜、と主張をする
  • このバグは発生する。なぜなら〜、と主張をする

を繰り返していくこともおすすめです。

例えば、昨日まで検索でヒットしていたものがヒットしなくなった場合だと、

  • このバグは発生しない。なぜなら検索対象のデータは昨日と変化しておらず状況が変わらない
  • このバグは発生する。なぜなら検索対象のデータは先程の調査で確認した部分以外も対象だからだ
  • ....

みたいな形で進めて行くと、仮説を立てながら調査していくのがスムーズにできるし、見落としが減ります。

修正する

修正の際、考慮する必要があるのは、

  • 二次被害を産まないか?
  • 十分な対応か?

です。

例えば、ブログが昨日まで検索でヒットしていたものがヒットしなくなった場合には、直したが、下書き状態のブログまで検索の対象になってしまう、だと二次被害を産んでしまっています。

また、十分な対応か?で言えば、検索でヒットしないが問題なので、それ以外の要因でもヒットしなくなっていないか?は確認する必要があります。

特に、わけのわからないバグに遭遇した場合、こういったリスクがあるので注意する必要があります。

最後に

このあたりの話をもっと知りたい方や、ヘンリー全体のこと、などを知りたい方はぜひ一度カジュアル面談しましょう!

hrmos.co

スタートアップで経営を執行から引き剥がした10ヶ月間

株式会社ヘンリーでVPoEを務めている戸田id:eller)と申します。これはHenryアドベントカレンダー 2025 シリーズ 1における6日目の投稿です。昨日の記事は kobayangデザインシステムライブラリを実装するためのテクニック でした。

本日は弊社で経営と執行を分離するためにどう権限委譲を進めてきたかをご紹介したいと思います。スタートアップのVPoEって何をやってるんだろう、という疑問にお答えできれば幸いです。

目次

解きたい課題

この6月に「ヘンリーで初めて製品部室長合宿をしました」で触れたように、弊社では長らくCEOである逆瀬川がエンジニアリングチームのマネジメントを兼務していました。2024年末に組織再編を行い部長・室長・本部長などのポジションが明確になりましたが、その後も逆瀬川が部長を兼務していた状態でした。

図1 電子カルテや医事会計などの開発を逆瀬川が直接見ていた

この状態には多くの利点があります。逆瀬川はお客様のペインも業界の課題も頭に入っていますし、サービスを黎明期から見てきているので深く理解しています。またヘンリーを起業してまで解決しようとする社会問題へのハングリー精神を持っていますので、常に学んでおり顧客と十二分に会話できます。さらにデザイナーやPdMとしての経験もお持ちですから要点を抑えたマネジメントを行えますし、様々な複雑さを巻き取ってITエンジニアを製品実装に注力させることもできます。こうした多くの利点があるからこそ、逆瀬川が部長を兼務することに対して慣性が働いてきました。

しかし組織が大きくなってくると、利点以上に課題が目立つようになってきます。事業戦略上エンジニアリングチームを増やすことの必要性は以前から明らかでしたが、1人で2つ3つとチームをマネジメントすることには限界があります。また事業戦略を考える人と部長が同一人物であるために事業戦略を部長に説明するというプロセスが省略された結果、トップダウンとボトムアップをすり合わせる機会が失われました。このすり合わせこそが組織としてSECIを回し学ぶ機会を生み出すことが「知識想像企業」に書かれていますが、これが行われなかったわけです。結果としてチームで「なぜこれを優先するかがわからないが、責任を取る人が言うんだし問題ないのだろう」「この壮大な計画をどう実現するのかイメージ湧かないけど、きっとやらないとまずいんだろう」のような忖度が働くなどして、早い段階でリスクを洗い出す機会が失われたと考えています。

何よりもCEOという会社のトップが、最もすべき「意思決定」に時間を使えていないこと が課題です。製品の実装が事業における最重要課題であることを踏まえると確かにエンジニアリングチームのマネジメントも大切です。しかしそれらは信頼できる人に任せて、もっとCEOがやるべきことに注力できる環境が必要だと考えました。

何がブロッカーだったのか

弊社は「理想駆動」を基本原則としており、共感を呼ぶ理想を描いて人を巻き込む「燃える理想」や自ら手を挙げて成果を作りに行く「自分起点」を行動規範に掲げている会社です。なので実現するべき理想が明確であれば、周囲の理解や協力は得やすい環境にあります。それでもこの慣性に抗うことは難しかったわけですが、何がブロッカーだったのでしょうか。

ひとつは弊社が扱うドメインの複雑さにあります。当時は電子カルテと医事会計という2つのエンジニアリングチームがひとつの製品を実装する形を取っていましたが、「電子カルテ」ひとつを取っても多数の関係者、多数の業務、多数の連携、そして多数の状況が想定されます。さらにすべての機能に診療報酬制度という共通する概念が串刺しで関わるため、「電子カルテ」と「医事会計」で製品を割ることそのものもチャレンジだと言えます。この技術ブログでもコードベースの分割統治が難しいことに繰り返し触れてきましたが、製品やコードベースが割れない状態でチームを割ることをトップダウンで進めることは困難です。

dev.henry.jp

もうひとつはゴールが遠すぎてどうすればできるかがイメージできないことです。「マネジメントを他の人に任せてCEOを経営に注力させる」ことそのものは正しいように見えますが、今までそうではなかったものを変えていくために何をすればよいのかがイメージしにくい状態でした。

このため2024年末時点では、本質的な解決である権限委譲は難しいのではないか、仮に医事会計側はチャレンジできても電子カルテ側はまだ先になるのでは、という見方が強い状況でした。組織にあるこうした問題意識を踏まえて、経営を執行から引き剥がす活動を進めていきました。

課題を解く10ヶ月

1月: 方針の明確化と障害の分析を行い、戦略を立てる

まずこの時点で、全従業員が見えるNotion上に「逆さんの製品本部長としての負荷を下げる」と題したページを作成して方針を明記し、なぜ難しいと考えるのか、どのような障害がありそうなのかをひとつずつていねいに言語化しました。その結果として解けそうな課題に分解できたため、それを解決するアプローチとして2つの案を作り、どちらの方が適切かをCEOとして判断いただく材料としました。

図2 作成したNotionの目次。この下には四半期ごとの実績と起動修正の記録が並んでいる

こうして整理した結果、意思決定ではない業務から渡していくアプローチと、価値のデリバリーから渡していくアプローチであれば、段階を踏んで試しやすいということがわかりました。また不安に名前をつけて細分化していくことで、これだったら彼に任せられる、それだったら自分でカバーに入れば渡しやすい、あの問題はもう逃げようがないんだからとっとと意思決定しないといけない、といった意思決定がやりやすくなり、多くの社員の「自分起点」の引き金を引くことができました。その結果として、逆瀬川自身に自信を持ってやっていけそう!というモードになっていただけたと感じています。

図3 製品本部長として持っていた責務を整理した図。不安に名前をつけることが大切です。

3月: 医事チームを分割して権限委譲できる大きさにする

喫緊の課題としては医事業務を支えるチームが20名を超えており、朝会をはじめとしたコミュニケーションが機能していない問題がありました。このままではマネジメントの責任を委譲することもままならないため、ドメインをどこで割るべきかをエンジニアを巻き込んで話し合い、請求レセプトチームを立ち上げています。逆瀬川から2名のエンジニアを指名して両チームのマネジメントを委ね、3月から試行のうえで6月から本実施としており、1月に決めたスコープや委譲する順番を踏まえて着実に権限委譲を進めました。

ここでVPoEとして見ていたのは主に朝会で逆瀬川が発言せずに済んでいるかどうか、でした。朝会で引き続き逆瀬川が発言してしまうと権限委譲が有名無実化してしまいますので、逆瀬川と筆者とで目標を設定し、基本的にマネジメントに任せること、フォローは朝会以外の場で1対1で行うこと、第3四半期には逆瀬川が朝会参加をやめることを握りました。特に「逆瀬川は部長ではないがプロジェクトマネジメントは兼務していた」時期があり、このあたりの匙加減は相談しながら走っていました。

図4 朝会の反省をマネジメント陣で実施していた際の筆者によるフィードバック例

結果的にこれらの目標は期日通りに達成されることになり、コンティンジェンシープランを発動させることなく速やかに権限委譲が完了しました。

7月: 電子カルテチームを分割して権限委譲できる大きさにする

医事チームの権限委譲が完了した7月には、残る電子カルテチームの権限委譲について議論しました。電子カルテチームが医事チームに比べて難しいのは、電子カルテという仕組みがかなり巨大であり、ドメインを2つに割ることが事実上不可能であろうと思われたことです。ここについてはVPoEがトップダウンで決めても禍根を残すだけだということが明らかだったため、部長候補やエンジニアリングマネージャに判断を委ね、筆者と逆瀬川はリマインドをするにとどめていました。

特にこの時期は全社で「Team Topologiesを参考にバリューストリームでドメインを割ることを考えてきたが、このアプローチは診療報酬という制度が背骨のように全体を貫いているHenryには合わない」という事実を認めて次の組織の在り方を模索しており、その最初の例を作ろうとしていた電子カルテチームのプレッシャーは大きなものだったと感じています。実際にSquad制度をはじめとしていくつかやり方が検討されていたようですが、ここでは詳細を割愛します。

また請求レセプトチーム発足の反省として、切り出したチームの目的は明瞭だったが残された方のチームはそうでなかったこと、チーム間の人員計画を流動的に行えなかったことの2点があったため、電子カルテチームの分割では二の舞を踏まないようマネジメントとエンジニアリングマネージャとで議論を重ねました。

最終的にはLeSSフレームワークを参考にして、組織としてはひとつだが実装や意思決定は独立して行える2つのチームを作る方向としました。もう少し思い切った判断をしたほうが良かったのかもしれないとは今も思いますが、少なくとも半年ほど動かしてみて破綻せず協調して動けているため、判断としては間違っていなかったのだと考えています。

9月: 逆瀬川の部長兼務が終了する

電子カルテチームの権限委譲が9月1日付けで完了し、逆瀬川が部長を兼務することがなくなりました。これによって逆瀬川は製品本部長のみを兼務することとなり、個別のチームを見る必要がなくなりました。筆者と逆瀬川は他の経営関係者と権限委譲を進めるための予算・稟議周りの整理や人事制度の刷新を進めることに注力できるようになったため、12月までを見込んでいた一大プロジェクトが早めに片付きました。

と、ここで満足しないのが我らがCCOの林太郎です。まだ4ヶ月あるやん!という激励をいただきまして、本部長と部門長の責務を具体化したうえで、逆瀬川に製品本部長をも権限委譲してもらうべく動きました。ここで指名されたのが1日めのアドベントカレンダーを書いてくれた縣(id:agtn)ですので、よろしければ彼の投稿も読んでもらえれば楽しんでいただけると思います。

dev.henry.jp

その後は順調に話が進み、縣が製品本部長に加えてVP of Productないし製品部門長を担うことになりました。これについては私が扱う内容を離れているので、いずれ役員が書いてくれるかなと思います。

図5 本記事執筆時点での組織。逆瀬川がCEOとしての責務に注力できるようになった。

11月: 縣が製品部門代表としてお客様にご挨拶をする

そして11月7日には弊社がお客様をはじめとした関係者を招いて地域医療の理想をともに語る場を設けたのですが、この場では逆瀬川ではなく縣が製品部門代表としてお客様にご挨拶をしています。これがかなりエポックメイキングな出来事であったことは、ここまで読んでいただけた方には伝わるでしょうか。

またこの頃には逆瀬川は CEOとしての活動、特に採用や意思決定に全力で取り組めるようになりました 。わかりやすいところでは第1四半期ではCEOの対社外の露出がnote記事2本に留まっていましたが、第4四半期ではまだ1ヶ月を残している12月頭の時点で記事3本、登壇3件とかなり増やせています。こうした地道な露出が将来の顧客や採用などの機会に繋がることを考えると、かなり良い改善状況ではないでしょうか。

振り返って、VPoEは何をしたのか、その存在意義はなにか

1年前は開発組織のほとんどのマネジメントを経営が兼務していた状況でしたが、我々は10ヶ月でこの問題を解消しマネジメントに権限委譲をするとともに、経営が採用や意思決定などの活動に注力できる状況が生まれました。また私や縣のような執行役員に執行が任せられる体制も整いました。これは拡大期のスタートアップにおける経営のあり方としてひとつの理想形だと考えており、結果が出せたことにひとまずホッとしています。

ただこの10ヶ月でVPoEがやったことを振り返ると、実はそんなに多くのことはやっていません。1月に方向性を示してやるぞとコミットしたこと、3月にスコープを決めて実際に権限委譲を進めたこと、その反省を踏まえて7月に最後の権限委譲を進めたことだけです。しかも3月と7月に悩みながら意思決定したのは筆者ではなく、対象チームの皆さんでありマネジメントであり、逆瀬川でした。筆者は「分割と権限委譲、するから。」と早期にスタンスを明示しておき、その成功を信じてビジョンを描き、あとは適切と思われるタイミングでリマインドを実施したのみです。

おそらく重要だったのは不安に名前をつけて整理して解決可能であることを示したことと、逆瀬川ではない他の人が部長を務めても充分に回るしむしろ理想に近いと言い続けたことです。これは連結6,000人のメガベンチャーでマネジメントを経験した筆者だからこそ、マネジメントが少ない段階でも確信を持って言えることだったのかもしれません。VPoEがあるべき姿を描いてその実現を信じて諦めていない、そのことを日々行動で示すことで一時期止まっていた検討が再開したり、理想的ではないかもしれないがそのときのベストを決めて次に行く動きが起こせたりといった効果があったと思っています。

株式会社ヘンリーはエンジニアリング組織の理想を追求していきます

権限委譲の浸透はひとまず行えましたが、SECIを回して学べる組織になるのはこれからです。また各チームが自律的かつ短時間に顧客と向き合って価値をデリバリーしていける状態を作るにも課題があると感じています。加えて縣とも話している課題として、チームの割り方がFeature Factory的になっていないか、バリューストリームで分けられないとしてももっと顧客価値に沿ったチームの形があるのではとも検討しています。デザイナーやQAといった専門家の知見を開発プロセスに横断的に組み込んでいく挑戦もまだ始まったばかりです。

株式会社ヘンリーではチーム作りのために泥をかぶり東奔西走しながら重要だけどとても地味なエンジニアリングを積み重ねられるVPoE室付きエンジニアを募集しています。エンジニアリング組織や社会課題解決、そして仲間が好きなエンジニアに来ていただけると嬉しいです。よろしくお願いいたします。

hrmos.co

デザインシステムライブラリを実装するためのテクニック

株式会社ヘンリーでソフトウェアエンジニアをしている小林(kobayang)です。 最近、社内のデザインシステムライブラリの更新を行った際に、汎用的なコンポーネントの実装について整理したので、その内容について記述します。

おことわり

この記事は Henry アドベントカレンダー 5 日目の記事です。 この記事は 電子カルテの開発を支える技術3 ~ モダンな技術で再発明する ~ の、「デザインシステムライブラリを実装する」から「汎用的なコンポーネントを実装するテクニック」の節を切り出した内容になります。 なお、この記事の内容は React 前提になります。

Props の定義

汎用的なコンポーネントを作る上で考慮すべき Props 定義について記述します。

HTML Attributes を公開する

HTML Attributes を UI コンポーネントから提供することで、Native の HTML と同様に UI コンポーネントを利用者が使用でき、汎用性が上がります。 たとえば、シンプルなボタンの UI コンポーネントを作ることを考えた時に、Props を次のように定義します。

type ButtonProps = {
  // 特定のButtonのプロパティを定義
  size: ButtonSize;
  // HTML Attributesを定義
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

onClickonMouseEnter などのイベントハンドラ、または aria などのアクセシビリティに関するプロパティを一度に定義することができます。

forwardRef で ref を公開する

汎用的なコンポーネントを作る際は、フォーカス管理やその他さまざまな理由で ref を使いたいケースがあるため、受け渡しができるようにしておくと便利です。

React 19 以前のバージョンをサポートする際には forwardRef による ref の受け渡しが必要になります。

ref の受け渡しは次のように記述できます。

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    return <StyledButton ref={ref} {...props} />;
  }
);

なお、React 19 から ref は forwardRef を使用しなくてもよくなったので、最新の React では直接 Props に ref を定義して良くなりました。

HTML 要素を変更可能にする

アプリ開発側がセマンティックを柔軟に指定できるようにするために、HTML 要素の設定を、UI コンポーネントから変更可能にしたいことがあります。 例としてよくあるのが、ボタンの UI コンポーネントが button 要素として使われるのか、または a 要素として使われるかを変更したい場合です。

一方で、JSX は HTML と紐づいているため、このような HTML 要素を変更可能にするためにはひと工夫が必要となります。 このような HTML 要素を変更できる UI コンポーネントのことを Polymorphic Component と呼びます。

ライブラリにおける Polymorphic Component の例

Headless UI(UI を持たない汎用的なコンポーネントライブラリ)や、スタイリングライブラリでは HTML 要素を変更するためのプロパティが定義されています。Chakra UI1、Radix UI2 などの Headless UI や styled-components3 のコンポーネントは、as プロパティによってコンポーネントの HTML 要素を切り替えることができます。Material UI4 では、component プロパティによってコンポーネントの HTML 要素を切り替えることができます。

const Button = styled.button``;

// Anchor Button として使うことができる
<Button as="a" href="/example_page" />;

Polymorphic Component を実装する

後述する styled-components と併用する場合のように、自前で Polymorphic Component を実装することが必要になるケースがあります。少々複雑ですが、PolymorphicComponent を次のように定義することで、自前で HTML 要素を外側から変更できるコンポーネントを実装することができます。ここでは tag というプロパティでコンポーネントの HTML 要素を変更することにします。

import * as React from "react";

type TagProps<C extends React.ElementType> = { tag?: C };
type PropsToOmit<C extends React.ElementType, P> = keyof (TagProps<C> & P);

export type PolymorphicComponentProps<
  C extends React.ElementType,
  Own = object
> = Own &
  TagProps<C> &
  Omit<React.ComponentPropsWithRef<C>, PropsToOmit<C, Own>>;

type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

export type PolymorphicComponent<
  DefaultC extends React.ElementType,
  Own = object
> = {
  <C extends React.ElementType = DefaultC>(
    props: PolymorphicComponentProps<C, Own>
  ): React.ReactElement | null;
};

UI コンポーネントの実装は次のように行います。

export const Button: PolymorphicComponent<"button", ButtonProps> = ({
  tag,
  ref,
  ...rest
}) => {
  const Component = tag ?? "button"; // ここでHTML要素を変更
  return <Component ref={ref as any} {...rest} />;
};

// Anchor Button として使うことができる
<Button tag="a" href="/example_page" />;

React 19 以前のバージョンをサポート対象とする場合は、ref を直接定義できないため、forwardRef の Helper も用意しておくと便利です。

// forwardRef の helper
function forwardRefWithTag<DefaultC extends ElementType, OwnProps>(
  render: <C extends ElementType = DefaultC>(
    props: PolymorphicComponentProps<C, OwnProps>,
    ref: PolymorphicRef<C>
  ) => React.ReactElement | null
): PolymorphicComponent<DefaultC, OwnProps> {
  return React.forwardRef(
    render as unknown as any
  ) as unknown as PolymorphicComponent<DefaultC, OwnProps>;
}

export const Button = forwardRefWithTag<"button", ButtonProps>(
  ({ tag, ...rest }, ref) => {
    const Component = tag ?? "button";
    return <Component ref={ref as any} {...rest} />;
  }
);

styled-components を使用する場合の注意点

styled-components で styled を使うと、コンポーネントのスタイルを追加することができますが、as を複数回使用すると、上書きする元のスタイルが失われてしまうため注意が必要です。UI ライブラリ自身で styled-components を利用しつつ、アプリケーション側でも styled-components によってスタイルの上書きを可能にしたい場合は、as の適用を集約するようにしましょう。

Slot Pattern

Polymorphic Component を使わない別の方法として、children で指定したコンポーネントを代わりに使用するパターンもあります。Radix UI はこのパターンを Slot コンポーネントという名前で提供しており、Slot Pattern と私は呼んでいます。

Polymorphic Component で紹介した Button と同等のサンプルを記述します。

import { Slot } from "@radix-ui/react-slot";

type Props = {
  // true の場合、子要素のコンポーネントを使用する
  asChild?: boolean;
  // button の プロパティを定義
};

const Button = React.forwardRef<HTMLButtonElement, Props>(
  ({ asChild, ...rest }, ref) => {
    const Comp = asChild ? Slot : "button";
    return <Comp ref={ref} {...rest} />;
  }
);

// Anchor button として使うことができる
<Button asChild>
  <a href="/example" />
</Button>;

Polymorphic Component と比較すると、Button の Props 定義がシンプルになるメリットがあります。また、コンポーネントごと分けることができるので、こちらの方が汎用的ではあります。一方で、子の要素でカスタマイズできることが暗黙的になるため、利用側のインターフェースは複雑になるデメリットがあることや、Slot は内部で cloneElement を使っており、React Server Component の互換性の問題がある5ため、使用は限定的にすると良いかもしれません。

セマンティックを自動化する

先ほど、HTML 要素を変更可能にすることでセマンティックをアプリ開発側で柔軟に設定できる方法について記述しました。一方で、ライブラリの方針次第ではセマンティックをコンポーネントの利用側で考えさせないようにする方向も考えることができます。

具体的な例として、h1-h6 の要素の自動化について考えてみます。

h1-h6 の自動化

Heading のセマンティックについて考えると、h1-h6 のレベルを使い分けることは重要です。なお、この項は 「React で h1-h6 を正しく使い分ける」6の記事を参考に記述しています。

Heading のレベルが何かは、実装するコンポーネントだけでなく、外側の HTML の構造に依存します。Context API を使うことで、h1-h6 の使い分けを行うことができます。

const HeadingLevelContext = React.createContext({ level: 1 });

export const useLevel = () => {
  const context = useContext(HeadingLevelContext);
  return context.level;
};

const Section = () => {
  const level = useLevel();
  const nextLevel = Math.min(level + 1, 6);

  return (
    <HeadingLevelContext.Provider value={{ level: nextLevel }}>
      {children}
    </HeadingLevelContext.Provider>
  );
};

const Heading = ({ children }) => {
  const level = useLevel();
  const H = `h${level}`;
  return <H>{children}</H>;
};

上記のように SectionHeading コンポーネントを定義することで、Heading のレベル制御の使い分けが可能になります。次のように定義することで、タイトルを h1 として、サブタイトルを h2 として描画できます。

<Section>
  <Heading>タイトル</Heading>
  <Section>
    <Heading>サブタイトル</Heading>
  </Section>
</Section>

機能を提供するデザインパターン

UI コンポーネントから何かしらの機能を提供する場合に、いくつかのデザインパターンを用いることがあります。代表的なパターンとして、 Render Props Pattern、Getter Props Pattern、Compound Pattern を紹介します。

Render Props Pattern

子要素の中身に依存せずに、子要素に機能を与えることができます。例として、ホバー状態を管理するコンポーネント Hover を考えてみましょう。

Render Props Pattern を用いることで、次のような使い方でホバー管理ができます。

<Hover>
  {({ hovered, getProps }) => (
    <div {...getProps()}>{hovered ? "Hovered!" : "Hover me"}</div>
  )}
</Hover>

実装としては次のようになります。

type Props = {
  hovered: boolean;
  getProps: () => React.HTMLAttributes<HTMLElement>;
};

const Hover: React.FC<{ children: (props: Props) => ReactNode }> = ({
  children,
}) => {
  const [hovered, setHovered] = useState(false);
  const getProps = () => ({
    onMouseEnter: () => setHovered(true),
    onMouseLeave: () => setHovered(false),
  });
  return <>{children({ hovered, getProps })}</>;
};

型定義が、children: ReactNode ではなく、children: (props: Props) => ReactNode となっていることがポイントです。Render Props Pattern は hooks で記述する機能を隠蔽することができるメリットがある一方、render の実装部分が複雑になるデメリットがあります。

私の体感では、最近のライブラリは hooks 自体を提供することが多い印象があります。

Getter Props Pattern

ライブラリのインターフェースで、UI コンポーネントに aria や event handler の機能を隠蔽しつつ提供する方法として Getter Props Pattern がよく使用されます。

たとえば、SelectBox のアクセシビリティを提供するライブラリである Downshift7useSelect という hooks を例に出します。この hooks は Native の Select 要素ではなく、自前で SelectBox を実装する際に必要なメニュー表示であったり、開閉管理やキーボード操作などのさまざまな機能を提供します。

実装の全貌は省略しますが、useSelect の返り値には getXXXProps というプロパティがあり、これらを UI コンポーネントに差し込むことで、機能が提供されます。

const { getToggleButtonProps, ... } = useSelect(...);

return <button {...getToggleButtonProps()} />;

getToggleButtonProps は、button をクリックした際の選択メニューの開閉機能であったり、aria-expanded などの aria 属性などを自動的に提供します。このような get props をコンポーネントに差し込んで機能を提供するパターンを Getter Props Pattern と呼びます。

Compound Pattern

Render Props や、Getter Props のデザインパターンは、プロパティが render 実装に露出する構造になっています。一方で React Context を使うことで、さらにそれらを隠蔽して見かけ上シンプルに機能を提供することができます。

次の例のように、<UI.Feature /> のような形で提供するデザインパターンで、Context をコンポーネントに隠蔽しつつ、機能を提供します。例として、トグルの状態を管理するコンポーネント Toggle を考えてみましょう。トグルの状態で、表示する内容の出し分けを行い、またボタンでトグルを行います。

<Toggle>
  <Toggle.On>The button is on</Toggle.On>
  <Toggle.Off>The button is off</Toggle.Off>
  <Toggle.Button>Toggle</Toggle.Button>
</Toggle>

実装としては次のように記述できます。

const ToggleContext = createContext();

function Toggle({ children }) {
  const [on, setOn] = useState(false);
  const toggle = () => setOn(!on);

  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      {children}
    </ToggleContext.Provider>
  );
}

Toggle.On = function ToggleOn({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? children : null;
};

Toggle.Off = function ToggleOff({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? null : children;
};

Toggle.Button = function ToggleButton(props) {
  const { on, toggle } = useContext(ToggleContext);
  return <button onClick={toggle} {...props} />;
};

このような Context を使って複数のコンポーネント群を提供する実装パターンを Compound Pattern と呼びます。Context を使っている分、UI コンポーネントの実装は複雑になっており、一方で、利用側は内部実装のことをほとんど考えなくてよいというメリットがあります。

どのデザインパターンを採用するか?

3 つの代表的なデザインパターンを紹介しました。

体感として、Headless UI では Compound Pattern が内部実装を隠蔽できることから、多く採用されていることが多い印象があります。一方で、Render Props Pattern や、そもそも hooks を提供する方がライブラリの実装の見通しがよいため、そちらを採用するという視点もあるでしょう。

どのパターンにも一長一短があり、ライブラリの課題感や提供する規模感などに応じて、適切にパターンを採用することが重要です。

コンポーネントにスタイルプロパティを提供する

Headless UI などのライブラリでは、スタイリング機能を提供するプロパティをコンポーネント自体に追加していることがあります。Chakra UI、Radix UI、Material UI では Box という名前で、スタイリングを行うベースコンポーネントを提供しています。

Box

Chakra UI を例にとると、次のように Box を使うことができます。

<Box bg="tomato" w="100%" p="4" color="white" _hover={{ bg: "green" }}>
  This is the Box
</Box>

Chakra UI は _hover でホバーリアクションなど、擬似クラスも定義できることが特徴です。一方で、擬似クラスが提供されている場合は、スタイリング用のライブラリにも依存しているため、既存で使用しているスタイリング用のライブラリとの衝突が起きないか注意が必要です。

styled-system による Box の定義

styled-components を使用している場合、styled-system8 というライブラリを用いて、Box を自前で定義できます。

簡単に提供するなら、次のように記述できます。

import styled from "styled-components";
import { space, layout, typography, color } from "styled-system";

const Box = styled.div`
  ${space}
  ${layout}
  ${typography}
  ${color}
`;

自分で定義をカスタマイズして実装することもできます。自前で CSS プロパティを定義する Box の実装例を次に挙げます。

const StyledBox = styled("div").withConfig({
  shouldForwardProp: (prop) => {
    return !stylePropNames.has(prop);
  },
})<StyleProps>`
  box-sizing: border-box;
  min-width: 0;
  ${styledSystemConfig};
`;

export const Box = forwardRefWithTag<"div", StyleProps>(
  ({ tag = "div" as ElementType, ...rest }, ref) => {
    return <StyledBox as={tag} ref={ref} {...rest} />;
  }
);

スタイル提供用の設定は次のように記述できます。

import { CSSProperties } from "react";
import { compose, Config, system } from "styled-system";

const paddingConfig = {
  p: { property: "padding" },
  pt: { property: "paddingTop" },
  pb: { property: "paddingBottom" },
  pl: { property: "paddingLeft" },
  pr: { property: "paddingRight" },
  px: { properties: ["paddingLeft", "paddingRight"] },
  py: { properties: ["paddingTop", "paddingBottom"] },
} as const satisfies Config;

const padding = system(paddingConfig);

const stylePropConfig = {
  ...paddingConfig,
  // 他に必要なスタイル定義を登録する
} as const;

export const styledSystemConfig = compose(
  padding
  // 他に必要なスタイル定義を登録する
);

export const stylePropNames = new Set(styledSystemConfig.propNames ?? []);

type StyleConfig = typeof stylePropConfig;

type PrimaryCssPropName<Config> = Config extends { property: infer Property }
  ? Property & keyof CSSProperties
  : Config extends { properties: readonly [infer First, ...unknown[]] }
  ? First & keyof CSSProperties
  : never;

type CssPropertyValue<Config> = PrimaryCssPropName<Config> extends [never]
  ? CSSProperties[keyof CSSProperties]
  : CSSProperties[PrimaryCssPropName<Config>];

export type StyleProps = {
  [Prop in keyof StyleConfig]?: CssPropertyValue<StyleConfig[Prop]>;
};

このように記述することで、Box へのスタイルを型付きで組み込むことができるようになります。

既存ライブラリに実装を移譲する

ユーザビリティやアクセシビリティを考慮したコンポーネントを自前で実装するのはとても難しいです。

たとえば、ボタンなどを押下したときに表示される Popup パネル(Popover)を実装することを考えると、次のような機能を満たす必要があります。

  • フォーカス管理
    • 開いたら最初のフォーカス可能要素をフォーカス
    • 閉じたら元のトリガーにフォーカスを戻す
  • キーボード操作
    • Esc で閉じる
    • パネル内の Tab キーによる移動
    • 矢印キーによる選択項目の移動
  • 開閉管理
    • 外側クリックで閉じる
  • 位置決め・衝突管理
    • 決められた位置に表示する
    • ビューポートの大きさによって上下や左右の位置を入れ替える

また、さらに rolearia-expanded などのアクセシビリティ属性、アニメーションなど、プロダクトの性質やユーザー属性によって追加で対応が必要になります。

自前で上記の機能に対応するのは割に合わないことが多く、特にこだわりがなければ既存のライブラリを使うことをオススメします。ここまでに紹介した Radix UI などの Headless UI ライブラリがその一例で、一般的なユーザビリティやアクセシビリティを担保することができます。

Floating UI を用いた Popover 実装

ここでは、Floating UI9 というライブラリを用いたコンポーネントの実装を紹介します。

ライブラリ名のように、ボタン押下で浮かび上がる Popup や Tooltip などのコンポーネントのユーティリティであることはもちろんですが、それに加えて、Menu や SelectBox、ダイアログなど、何かしらのトリガーアイテムによって表示される要素全般に対して機能を提供しています。

Floating UI を使った Popover の実装を考えます。次のような hooks を提供し、Popover のコンポーネントに Props を当てることができます。

function usePopover<ReferenceElm extends HTMLElement>({
  placement,
  offsetPx,
}: PopoverArgs): PopoverProps<ReferenceElm> {
  const [open, setOpen] = useState(false);

  // For Floating UI Positioning
  const { refs, floatingStyles, context } = useFloating({
    open,
    onOpenChange: setOpen,
    placement: placement,
    whileElementsMounted: autoUpdate,
    middleware: [
      offset({ mainAxis: offsetPx }),
      flip({ padding: FlipPadding }),
      shift({ padding: ShiftPadding }),
    ],
  });

  // For Interactions
  const { getReferenceProps, getFloatingProps } = useInteractions([
    // Dismiss
    useDismiss(context, {
      escapeKey: true,
      outsidePress: true,
      bubbles: true,
    }),
    // Role
    useRole(context, {
      role: "dialog",
    }),
  ]);

  return {
    open,
    setOpen,
    referenceRef: refs.setReference,
    getReferenceProps,
    floatingRef: refs.setFloating,
    getFloatingProps,
    floatingStyles,
    context,
  };
}

内容は割愛しますが、referenceRef と Getter Props である getReferenceProps をトリガーとなるボタンに、floatingRefgetFloatingProps を Popup したいコンテンツに差し込むことで、Popover 機能を実現することができます。詳しくは Floating UI のドキュメントを参照していただけると嬉しいです。

まとめ

汎用的な UI コンポーネントを作る上でのテクニックについて記述しました。

複雑に思える既存のライブラリの実装も、分解すると上記のテクニックやパターンの組み合わせであったりします。たとえば、Radix UI では、Compound Pattern と Polymorphic/Slot Pattern を組み合わせることで、高い汎用性と機能の凝集性を持ったコンポーネントの提供を実現しています。

この内容が、デザインシステムライブラリのUI実装などの参考になれば幸いです!