株式会社ヘンリーで SRE をしつつ、技術広報的なこともしている
id:nabeop です。
2025年も会社でアドベントカレンダーを企画しました。2024年までは1トラックでの開催でしたが、2025年は2トラックでの開催をして多くのメンバーに参加してもらえるようにしました。
続きを読む【これはHenryアドベントカレンダー 2025 シリーズ 1 における22日目の投稿です。昨日の記事は俺たちの成長は止まらない|Sugimura Masashiでした。】
はじめまして、6月にヘンリーに入社したエンジニアのichienです。初めてブログを書くので、半年遅れの入社エントリーになります。
私はこれまで地図や会計プロダクトでの経験を経て、今回、新しく医療ドメインにチャレンジしています。開発では主に医事(医療事務)会計領域を担当しています。
入社して半年、過去の学習経験を活かしながら医療ドメインのキャッチアップに取り組んでいますが、自分なりに理解するまでに時間がかかることが多く、試行錯誤しながら取り組んでいます。 前職ではマネジメント多めのEMロールだった所から、今のヘンリーではエンジニアにフルコミットしており、利用技術周りのキャッチアップにも取り組んでいます。 ライフステージの変化と共に利用時間に制約が増えていく中で、「いかに学習の質を高め、最速で学びを積み上げられるか」という戦略の大切さを感じている所です。
この半年でドメイン理解のために取り組んだことを振り返り、今後のアクションについて考えてみます。
子どもが成長過程で社会福祉機関による公共の支援や医療サポートを受ける機会がありました。 私にとって初めての経験で、地域で支え合う仕組みがあることにありがたいと思うと同時に、一人のエンジニアとしてこのような公共の仕組みに何か貢献できないだろうかと考え始めたことがきっかけでした。
そんな中、理想駆動で社会課題に真正面から立ち向かっているヘンリーの考え方に共感し、飛び込んでみました。
当初は「どんなドメインでも、学んでいけばなんとかなるだろう」と楽観的に考えてました。しかし、国民皆保険制度に基づく医療費請求の仕組み、国が定める医療費の根幹を貫く診療報酬制度、外来診療/入院を含めた病院業務の複雑さを一歩一歩理解するにつれて、奥深すぎて一筋縄ではいかないなと考え直しました。
未知で”わからなかった”ことを学び、解像度を上げていくことは面白いです。 "わかる”状態が増えると、自身の成長を実感できて、楽しいのです。 更にそれが難しいことほど、わかった時の成長実感が大きく、より面白くなっていきます。
こんな気持ちで複雑な領域に私は向き合っています。ここから半年間の取り組みを振り返っていきます。
最初はどこから手をつけるべきか迷子状態で会話内での単語やコードベースの命名やロジックを見ても、"わからないこと"が"わからない"状態でした。 まず書籍から全体像を理解しようと思いました。
一通り読んでちょっと分かった気持ちになった気がします。 しかし、「書籍で読んだあの内容のことか!」と理解がつながる瞬間があまりなく業務中で活かせてる実感がなかなか得られませんでした。 一度読んで終わりでなく、頭にインデックスを張っておき、思い当たった時に該当部分を読み直すぐらいの温度感でいると良さそうです。
「大量の"わからない"ことがあることが"わかった"状態になりました。」
今はAIがあるので、フル活用したらドメイン理解に費やす時間をショートカットできるのではと考え、知らない用語が出てきたら即AIで調べるようにしていました。 これは学習のフィードバックループを高速に回せそうです。
「"わからなかったこと"を"わかった"気になる状態になりました。」
振り返ると、とても便利で調査負荷を圧倒的に圧縮できていたと思います。 ただ、AIとのやり取りは基本的にクローズドなので、正しい意味で"わかった"のか客観的に判断できず、自身で自信が持てない状態でもあったと思います。 自身でAIで調べる・周囲の人に聞くことは、それぞれ一長一短ありそうです。
半年間で2件の病院を訪問させてもらい、医事課の方々が働く現場を見学させていただきました。 現場での実体験によって、初めて具体的に頭の中でイメージできるようになった感覚がありました。例えば、「外来受付には一台の共有PCが置かれており、画面全体に開かれたブラウザ内でHenryが常に表示されている」の様な現場状況を含めたイメージです。
N=1であっても自分の中に参考となる「絵」が持てると、その後の設計や議論の解像度が大きく変わってきます。これは前職でも経験あったこともあり、自身の中で再現性が高そうです。積極的に現場訪問の機会を得られる環境があるのはとても良いなぁと思っています。
入社して2ヶ月ぐらいで、担当する医事領域の機能開発でエピックオーナーに挑戦しました。
ヘンリーでは、エンジニアリングチームが主体となってプロダクトの仕様を意思決定します。 ユーザーの課題を真に解決するために、社内のドメインエキスパートや医療従事者の経験者などと協働し、時にはエンジニアがリードしてディスカバリーからデリバリーまでを一貫して担います。
この役割を「エピックオーナー」と読んでいて、社内には挑戦を後押しする文化があります。 この役割の存在が、ヘンリーへの入社を決めた一つの理由でもあります。会社の説明資料に記載があるので、ピックアップしておきます。
挑戦する楽しさとドメイン理解が十分でない状況で担えるかどうか不安な気持ちと両方の気持ちが交錯していました。
ドメインエキスパートやサポート、Bizメンバー、デザイナー等多様なロールと協働し、フィードバックを得ながら意思決定を積み重ねることで知識不足を補っていきました。重ねるごとに、徐々に自分で提案できることが増えていきました。
リリース後には、実際に利用している医療機関様にインタビューをさせてもらい、現場の声を直接得ることで、自分達で価値の検証を行いました。
この経験から、ドメイン理解の学習においても学習効率を最大化するのは、「適切な課題設定」と「その解決のためなら何でもするというオーナーシップ」なのだなぁと思いました。 まずやってみること大事。
半年振り返って、ドメイン理解にしても業務タスクにしても、目の前の課題の解決のために必要な知識の学習に多くの時間を使った感覚があります。 複数の取り組みから多くの情報を「点」で学習できたが、どうも自分の中で「点」と「点」をつなげて整理できてる感覚がまだまだ薄いです。 一方で「点」での理解を進めたことで、一定の成果を作ることができたとも思っています。
これからは長期的な視点を持って、学んできた「点」をつなげ、体系的に「線」で理解するためにアクションを増やしてゆく予定です。例えば、以下の様なことを考えています。
ドメイン理解の進め方はまだまだ探索しながらなので、定期的に振り返っていきたいです。 私の半年振り返りを最後まで読んでいただきありがとうございました。
もしヘンリーに興味をもっていただけた方がいたら、ぜひお話しましょう!
ヘンリーで SRE をやっている
id:nabeop です。
この記事は株式会社ヘンリー - Qiita Advent Calendar 2025 シリーズ2の10日目の記事です。
ヘンリーは2025年11月20日と21日に開催されたアーキテクチャ Conference 2025 で Gold スポンサーとしてスポンサーブースを出展しました。

会期中はたくさんの方にヘンリーブースに足を運んでいただき、ありがとうございました。ブースでは「アーキテクチャ」をキーワードに2つのアンケートを実施していました。
今回はそれぞれのアンケート結果をみながら、会期中の様子などについてまとめます。
続きを読む2025年11月25日、ヘンリーで「Server-Side Kotlin Night 2025」をKotlin Fest 2025の非公式アフターイベントとして開催しました!
2024年に続き2回目の開催となります。
今回はそのイベントのセッション内容と、当日の様子をご紹介します。
登壇者:
一円 真治 (ichien)氏 株式会社ヘンリー
KotlinConf 2025のOpening keynoteでも扱われ、聞いたことはある人が多くいるもののまだあまり情報の出回っていないAmperについてのセッション。
Gradleとの比較や実際に使ってみたデモもあり、とても貴重な内容です。
登壇者:
川田 裕貴氏 株式会社マネーフォワード
マネーフォワードさんでサーバーサイドKotlinを普及させた経験を元に、技術選定の難しさや実際に組織内から上がってきた意見などが紹介されています。
これからKotlinを導入したいと考えている方には必見の内容です。
登壇者:
縣 直道氏 株式会社ヘンリー
ヘンリーでなぜKotlinを使用しているのか、その背景にある医療ドメインの難しさや、技術選定の理由について解説しています。
1つ前のマネーフォワードさんでの事例と併せて、Kotlinを導入するメリットを知れる内容になっています。
登壇者:
lagénorhynque / カマイルカ氏 株式会社スマートラウンド
独自のResult型を実装し、エラーハンドリングを改善した事例を紹介しています。
Kotlinではエラーを例外で扱うことも多いですが、新たな選択肢としてとても興味深い内容になっています。
登壇者:
nabeo (渡辺 道和)氏 株式会社ヘンリー
渡邊 泰紀 a.k.a yasunori氏 株式会社ログラス
髙野 氷河氏 Sansan株式会社
竹端 尚人氏(モデレーター)

KotlinをIntelliJ IDEA以外のエディタを使って書いている3名のエンジニアが集まり、パネルセッションを行いました。
という3つのテーマで語り合い、VS CodeとVimとEmacsの利用者が一同に介して同じテーマについて語り合うという、なかなか見れない貴重な機会となりました。
IntelliJという強力なIDEが存在する、Kotlinならではの切り口のセッションだったと思います。
ウェルスナビ株式会社様に素敵な会場をご提供いただき、セッションも懇親会も大変盛り上がった会になりました。

当日の様子はXでハッシュタグ #KotlinFest_SSK でも多く投稿されているので、こちらもぜひご覧ください。
ヘンリーでは今後もサーバーサイドKotlinに関して、ブログやイベントを通して発信を続けていきます!
また様々な形で主催イベントも開いていこうと考えていますので、その際はぜひお越しいただければと思います。
株式会社ヘンリーで医療会計部のエンジニアの一條(GitHub: @rerost , X: @hazumirr)です。
これはHenryアドベントカレンダー 2025 シリーズ 1 における7日目の投稿です。昨日の記事は スタートアップで経営を執行から引き剥がした10ヶ月間 でした。
僕は今まで、
といったことをしていました。 なので、わけのわからないバグに遭遇する機会は多い方だったと思います。 この経験の中で僕が実践していたことについて書きます。
割とここが肝だと思っていて、バグ自体が分類できたら対応の9割は終わったと言っても過言ではないと思います。 僕は、発生源・影響度の二軸で考えています。
緊急度はバグ対応中にかなり早くに明確にしておくと対応がスムーズです。
1なら、まずは回避策を練るか一次対応するなどで根本対応より先にでること。対応へのリードタイムを減らすためにも、人を集めましょう。 2なら、落ち着いて原因を探りつつ、ユーザーへの案内などを速やかに行いましょう。 3なら、ゆっくり調べましょう。
といった具合です。
発生源は多くの場合、以下のどれかに分類されるかと思います。
2つ目の部分の説明をしていくと、まず具体例としては以下があります。
| 発生源 | 例 |
|---|---|
| 過去の仕様 | 昨日まで検索でヒットしていたものがヒットしなくなった。 先月まで自動で入力されていた部分が入力されなくなった。 |
| 関連するルールや法律など | XXを算定すると、自動でYYも算定されるはずだが、算定されない。 |
このどちらかが、実装とズレていることでバグは発生します。

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

これらの要因は、実装者のバグが入ったPRの文章やバグを報告してくれた方に話を聞いていくことで多くの場合は特定できるかと思います。
手元で環境構築などをし、報告されたスクリーンショットや報告内容から同じ状況を再現し、試しましょう。
うまく再現できない場合、弊社の開発する電子カルテ・レセコンだと、医療機関の設定や患者の状態(過去の操作)が影響するので、以下を確認するのが良いです。
また、報告されたケースと類似のバグが再現するケースを集めておくと、修正時のチェックで役に立つのでおすすめです。
原因の特定ですが、まずはここまで得られた情報を元に、特定していきましょう。 無論、ここまで集まった情報で原因特定が済んでいるのであれば、スキップしましょう。
コードなどを読みながら、状況を言葉や図にしてまとめていくのが肝で、わけがわからない状況のとき、言葉や図にしておくことで頭で抱え続ける情報を減らすことができます。
また、自身で
を繰り返していくこともおすすめです。
例えば、昨日まで検索でヒットしていたものがヒットしなくなった場合だと、
みたいな形で進めて行くと、仮説を立てながら調査していくのがスムーズにできるし、見落としが減ります。
修正の際、考慮する必要があるのは、
です。
例えば、ブログが昨日まで検索でヒットしていたものがヒットしなくなった場合には、直したが、下書き状態のブログまで検索の対象になってしまう、だと二次被害を産んでしまっています。
また、十分な対応か?で言えば、検索でヒットしないが問題なので、それ以外の要因でもヒットしなくなっていないか?は確認する必要があります。
特に、わけのわからないバグに遭遇した場合、こういったリスクがあるので注意する必要があります。
このあたりの話をもっと知りたい方や、ヘンリー全体のこと、などを知りたい方はぜひ一度カジュアル面談しましょう!
株式会社ヘンリーでVPoEを務めている戸田(id:eller)と申します。これはHenryアドベントカレンダー 2025 シリーズ 1における6日目の投稿です。昨日の記事は kobayang の デザインシステムライブラリを実装するためのテクニック でした。
本日は弊社で経営と執行を分離するためにどう権限委譲を進めてきたかをご紹介したいと思います。スタートアップのVPoEって何をやってるんだろう、という疑問にお答えできれば幸いです。
この6月に「ヘンリーで初めて製品部室長合宿をしました」で触れたように、弊社では長らくCEOである逆瀬川がエンジニアリングチームのマネジメントを兼務していました。2024年末に組織再編を行い部長・室長・本部長などのポジションが明確になりましたが、その後も逆瀬川が部長を兼務していた状態でした。

この状態には多くの利点があります。逆瀬川はお客様のペインも業界の課題も頭に入っていますし、サービスを黎明期から見てきているので深く理解しています。またヘンリーを起業してまで解決しようとする社会問題へのハングリー精神を持っていますので、常に学んでおり顧客と十二分に会話できます。さらにデザイナーやPdMとしての経験もお持ちですから要点を抑えたマネジメントを行えますし、様々な複雑さを巻き取ってITエンジニアを製品実装に注力させることもできます。こうした多くの利点があるからこそ、逆瀬川が部長を兼務することに対して慣性が働いてきました。
しかし組織が大きくなってくると、利点以上に課題が目立つようになってきます。事業戦略上エンジニアリングチームを増やすことの必要性は以前から明らかでしたが、1人で2つ3つとチームをマネジメントすることには限界があります。また事業戦略を考える人と部長が同一人物であるために事業戦略を部長に説明するというプロセスが省略された結果、トップダウンとボトムアップをすり合わせる機会が失われました。このすり合わせこそが組織としてSECIを回し学ぶ機会を生み出すことが「知識想像企業」に書かれていますが、これが行われなかったわけです。結果としてチームで「なぜこれを優先するかがわからないが、責任を取る人が言うんだし問題ないのだろう」「この壮大な計画をどう実現するのかイメージ湧かないけど、きっとやらないとまずいんだろう」のような忖度が働くなどして、早い段階でリスクを洗い出す機会が失われたと考えています。
何よりもCEOという会社のトップが、最もすべき「意思決定」に時間を使えていないこと が課題です。製品の実装が事業における最重要課題であることを踏まえると確かにエンジニアリングチームのマネジメントも大切です。しかしそれらは信頼できる人に任せて、もっとCEOがやるべきことに注力できる環境が必要だと考えました。
弊社は「理想駆動」を基本原則としており、共感を呼ぶ理想を描いて人を巻き込む「燃える理想」や自ら手を挙げて成果を作りに行く「自分起点」を行動規範に掲げている会社です。なので実現するべき理想が明確であれば、周囲の理解や協力は得やすい環境にあります。それでもこの慣性に抗うことは難しかったわけですが、何がブロッカーだったのでしょうか。
ひとつは弊社が扱うドメインの複雑さにあります。当時は電子カルテと医事会計という2つのエンジニアリングチームがひとつの製品を実装する形を取っていましたが、「電子カルテ」ひとつを取っても多数の関係者、多数の業務、多数の連携、そして多数の状況が想定されます。さらにすべての機能に診療報酬制度という共通する概念が串刺しで関わるため、「電子カルテ」と「医事会計」で製品を割ることそのものもチャレンジだと言えます。この技術ブログでもコードベースの分割統治が難しいことに繰り返し触れてきましたが、製品やコードベースが割れない状態でチームを割ることをトップダウンで進めることは困難です。
もうひとつはゴールが遠すぎてどうすればできるかがイメージできないことです。「マネジメントを他の人に任せてCEOを経営に注力させる」ことそのものは正しいように見えますが、今までそうではなかったものを変えていくために何をすればよいのかがイメージしにくい状態でした。
このため2024年末時点では、本質的な解決である権限委譲は難しいのではないか、仮に医事会計側はチャレンジできても電子カルテ側はまだ先になるのでは、という見方が強い状況でした。組織にあるこうした問題意識を踏まえて、経営を執行から引き剥がす活動を進めていきました。
まずこの時点で、全従業員が見えるNotion上に「逆さんの製品本部長としての負荷を下げる」と題したページを作成して方針を明記し、なぜ難しいと考えるのか、どのような障害がありそうなのかをひとつずつていねいに言語化しました。その結果として解けそうな課題に分解できたため、それを解決するアプローチとして2つの案を作り、どちらの方が適切かをCEOとして判断いただく材料としました。

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

喫緊の課題としては医事業務を支えるチームが20名を超えており、朝会をはじめとしたコミュニケーションが機能していない問題がありました。このままではマネジメントの責任を委譲することもままならないため、ドメインをどこで割るべきかをエンジニアを巻き込んで話し合い、請求レセプトチームを立ち上げています。逆瀬川から2名のエンジニアを指名して両チームのマネジメントを委ね、3月から試行のうえで6月から本実施としており、1月に決めたスコープや委譲する順番を踏まえて着実に権限委譲を進めました。
ここでVPoEとして見ていたのは主に朝会で逆瀬川が発言せずに済んでいるかどうか、でした。朝会で引き続き逆瀬川が発言してしまうと権限委譲が有名無実化してしまいますので、逆瀬川と筆者とで目標を設定し、基本的にマネジメントに任せること、フォローは朝会以外の場で1対1で行うこと、第3四半期には逆瀬川が朝会参加をやめることを握りました。特に「逆瀬川は部長ではないがプロジェクトマネジメントは兼務していた」時期があり、このあたりの匙加減は相談しながら走っていました。

結果的にこれらの目標は期日通りに達成されることになり、コンティンジェンシープランを発動させることなく速やかに権限委譲が完了しました。
医事チームの権限委譲が完了した7月には、残る電子カルテチームの権限委譲について議論しました。電子カルテチームが医事チームに比べて難しいのは、電子カルテという仕組みがかなり巨大であり、ドメインを2つに割ることが事実上不可能であろうと思われたことです。ここについてはVPoEがトップダウンで決めても禍根を残すだけだということが明らかだったため、部長候補やエンジニアリングマネージャに判断を委ね、筆者と逆瀬川はリマインドをするにとどめていました。
特にこの時期は全社で「Team Topologiesを参考にバリューストリームでドメインを割ることを考えてきたが、このアプローチは診療報酬という制度が背骨のように全体を貫いているHenryには合わない」という事実を認めて次の組織の在り方を模索しており、その最初の例を作ろうとしていた電子カルテチームのプレッシャーは大きなものだったと感じています。実際にSquad制度をはじめとしていくつかやり方が検討されていたようですが、ここでは詳細を割愛します。
また請求レセプトチーム発足の反省として、切り出したチームの目的は明瞭だったが残された方のチームはそうでなかったこと、チーム間の人員計画を流動的に行えなかったことの2点があったため、電子カルテチームの分割では二の舞を踏まないようマネジメントとエンジニアリングマネージャとで議論を重ねました。
最終的にはLeSSフレームワークを参考にして、組織としてはひとつだが実装や意思決定は独立して行える2つのチームを作る方向としました。もう少し思い切った判断をしたほうが良かったのかもしれないとは今も思いますが、少なくとも半年ほど動かしてみて破綻せず協調して動けているため、判断としては間違っていなかったのだと考えています。
電子カルテチームの権限委譲が9月1日付けで完了し、逆瀬川が部長を兼務することがなくなりました。これによって逆瀬川は製品本部長のみを兼務することとなり、個別のチームを見る必要がなくなりました。筆者と逆瀬川は他の経営関係者と権限委譲を進めるための予算・稟議周りの整理や人事制度の刷新を進めることに注力できるようになったため、12月までを見込んでいた一大プロジェクトが早めに片付きました。
と、ここで満足しないのが我らがCCOの林太郎です。まだ4ヶ月あるやん!という激励をいただきまして、本部長と部門長の責務を具体化したうえで、逆瀬川に製品本部長をも権限委譲してもらうべく動きました。ここで指名されたのが1日めのアドベントカレンダーを書いてくれた縣(id:agtn)ですので、よろしければ彼の投稿も読んでもらえれば楽しんでいただけると思います。
その後は順調に話が進み、縣が製品本部長に加えてVP of Productないし製品部門長を担うことになりました。これについては私が扱う内容を離れているので、いずれ役員が書いてくれるかなと思います。

そして11月7日には弊社がお客様をはじめとした関係者を招いて地域医療の理想をともに語る場を設けたのですが、この場では逆瀬川ではなく縣が製品部門代表としてお客様にご挨拶をしています。これがかなりエポックメイキングな出来事であったことは、ここまで読んでいただけた方には伝わるでしょうか。
またこの頃には逆瀬川は CEOとしての活動、特に採用や意思決定に全力で取り組めるようになりました 。わかりやすいところでは第1四半期ではCEOの対社外の露出がnote記事2本に留まっていましたが、第4四半期ではまだ1ヶ月を残している12月頭の時点で記事3本、登壇3件とかなり増やせています。こうした地道な露出が将来の顧客や採用などの機会に繋がることを考えると、かなり良い改善状況ではないでしょうか。
1年前は開発組織のほとんどのマネジメントを経営が兼務していた状況でしたが、我々は10ヶ月でこの問題を解消しマネジメントに権限委譲をするとともに、経営が採用や意思決定などの活動に注力できる状況が生まれました。また私や縣のような執行役員に執行が任せられる体制も整いました。これは拡大期のスタートアップにおける経営のあり方としてひとつの理想形だと考えており、結果が出せたことにひとまずホッとしています。
ただこの10ヶ月でVPoEがやったことを振り返ると、実はそんなに多くのことはやっていません。1月に方向性を示してやるぞとコミットしたこと、3月にスコープを決めて実際に権限委譲を進めたこと、その反省を踏まえて7月に最後の権限委譲を進めたことだけです。しかも3月と7月に悩みながら意思決定したのは筆者ではなく、対象チームの皆さんでありマネジメントであり、逆瀬川でした。筆者は「分割と権限委譲、するから。」と早期にスタンスを明示しておき、その成功を信じてビジョンを描き、あとは適切と思われるタイミングでリマインドを実施したのみです。
おそらく重要だったのは不安に名前をつけて整理して解決可能であることを示したことと、逆瀬川ではない他の人が部長を務めても充分に回るしむしろ理想に近いと言い続けたことです。これは連結6,000人のメガベンチャーでマネジメントを経験した筆者だからこそ、マネジメントが少ない段階でも確信を持って言えることだったのかもしれません。VPoEがあるべき姿を描いてその実現を信じて諦めていない、そのことを日々行動で示すことで一時期止まっていた検討が再開したり、理想的ではないかもしれないがそのときのベストを決めて次に行く動きが起こせたりといった効果があったと思っています。
権限委譲の浸透はひとまず行えましたが、SECIを回して学べる組織になるのはこれからです。また各チームが自律的かつ短時間に顧客と向き合って価値をデリバリーしていける状態を作るにも課題があると感じています。加えて縣とも話している課題として、チームの割り方がFeature Factory的になっていないか、バリューストリームで分けられないとしてももっと顧客価値に沿ったチームの形があるのではとも検討しています。デザイナーやQAといった専門家の知見を開発プロセスに横断的に組み込んでいく挑戦もまだ始まったばかりです。
株式会社ヘンリーではチーム作りのために泥をかぶり東奔西走しながら重要だけどとても地味なエンジニアリングを積み重ねられるVPoE室付きエンジニアを募集しています。エンジニアリング組織や社会課題解決、そして仲間が好きなエンジニアに来ていただけると嬉しいです。よろしくお願いいたします。
株式会社ヘンリーでソフトウェアエンジニアをしている小林(kobayang)です。 最近、社内のデザインシステムライブラリの更新を行った際に、汎用的なコンポーネントの実装について整理したので、その内容について記述します。
この記事は Henry アドベントカレンダー 5 日目の記事です。 この記事は 電子カルテの開発を支える技術3 ~ モダンな技術で再発明する ~ の、「デザインシステムライブラリを実装する」から「汎用的なコンポーネントを実装するテクニック」の節を切り出した内容になります。 なお、この記事の内容は React 前提になります。
汎用的なコンポーネントを作る上で考慮すべき Props 定義について記述します。
HTML Attributes を UI コンポーネントから提供することで、Native の HTML と同様に UI コンポーネントを利用者が使用でき、汎用性が上がります。 たとえば、シンプルなボタンの UI コンポーネントを作ることを考えた時に、Props を次のように定義します。
type ButtonProps = { // 特定のButtonのプロパティを定義 size: ButtonSize; // HTML Attributesを定義 } & React.ButtonHTMLAttributes<HTMLButtonElement>;
onClick や onMouseEnter などのイベントハンドラ、または aria などのアクセシビリティに関するプロパティを一度に定義することができます。
汎用的なコンポーネントを作る際は、フォーカス管理やその他さまざまな理由で 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 要素の設定を、UI コンポーネントから変更可能にしたいことがあります。
例としてよくあるのが、ボタンの UI コンポーネントが button 要素として使われるのか、または a 要素として使われるかを変更したい場合です。
一方で、JSX は HTML と紐づいているため、このような HTML 要素を変更可能にするためにはひと工夫が必要となります。 このような HTML 要素を変更できる UI コンポーネントのことを 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" />;
後述する 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 を使うと、コンポーネントのスタイルを追加することができますが、as を複数回使用すると、上書きする元のスタイルが失われてしまうため注意が必要です。UI ライブラリ自身で styled-components を利用しつつ、アプリケーション側でも styled-components によってスタイルの上書きを可能にしたい場合は、as の適用を集約するようにしましょう。
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 の要素の自動化について考えてみます。
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>; };
上記のように Section と Heading コンポーネントを定義することで、Heading のレベル制御の使い分けが可能になります。次のように定義することで、タイトルを h1 として、サブタイトルを h2 として描画できます。
<Section> <Heading>タイトル</Heading> <Section> <Heading>サブタイトル</Heading> </Section> </Section>
UI コンポーネントから何かしらの機能を提供する場合に、いくつかのデザインパターンを用いることがあります。代表的なパターンとして、 Render Props Pattern、Getter Props Pattern、Compound 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 自体を提供することが多い印象があります。
ライブラリのインターフェースで、UI コンポーネントに aria や event handler の機能を隠蔽しつつ提供する方法として Getter Props Pattern がよく使用されます。
たとえば、SelectBox のアクセシビリティを提供するライブラリである Downshift7 の useSelect という hooks を例に出します。この hooks は Native の Select 要素ではなく、自前で SelectBox を実装する際に必要なメニュー表示であったり、開閉管理やキーボード操作などのさまざまな機能を提供します。
実装の全貌は省略しますが、useSelect の返り値には getXXXProps というプロパティがあり、これらを UI コンポーネントに差し込むことで、機能が提供されます。
const { getToggleButtonProps, ... } = useSelect(...); return <button {...getToggleButtonProps()} />;
getToggleButtonProps は、button をクリックした際の選択メニューの開閉機能であったり、aria-expanded などの aria 属性などを自動的に提供します。このような get props をコンポーネントに差し込んで機能を提供するパターンを Getter Props 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 という名前で、スタイリングを行うベースコンポーネントを提供しています。
Chakra UI を例にとると、次のように Box を使うことができます。
<Box bg="tomato" w="100%" p="4" color="white" _hover={{ bg: "green" }}> This is the Box </Box>
Chakra UI は _hover でホバーリアクションなど、擬似クラスも定義できることが特徴です。一方で、擬似クラスが提供されている場合は、スタイリング用のライブラリにも依存しているため、既存で使用しているスタイリング用のライブラリとの衝突が起きないか注意が必要です。
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)を実装することを考えると、次のような機能を満たす必要があります。
また、さらに role や aria-expanded などのアクセシビリティ属性、アニメーションなど、プロダクトの性質やユーザー属性によって追加で対応が必要になります。
自前で上記の機能に対応するのは割に合わないことが多く、特にこだわりがなければ既存のライブラリを使うことをオススメします。ここまでに紹介した Radix UI などの Headless UI ライブラリがその一例で、一般的なユーザビリティやアクセシビリティを担保することができます。
ここでは、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 をトリガーとなるボタンに、floatingRef と getFloatingProps を Popup したいコンテンツに差し込むことで、Popover 機能を実現することができます。詳しくは Floating UI のドキュメントを参照していただけると嬉しいです。
汎用的な UI コンポーネントを作る上でのテクニックについて記述しました。
複雑に思える既存のライブラリの実装も、分解すると上記のテクニックやパターンの組み合わせであったりします。たとえば、Radix UI では、Compound Pattern と Polymorphic/Slot Pattern を組み合わせることで、高い汎用性と機能の凝集性を持ったコンポーネントの提供を実現しています。
この内容が、デザインシステムライブラリのUI実装などの参考になれば幸いです!