DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

TSKaigi Kansai 2024に参加して

はじめに

DIGGLE エンジニアのitoです。

DIGGLEではたびたびメンバーがカンファレンスに参加することがあります。今回、私は同じ開発チームの同僚のliuさんとともにTSKaigi Kansai 2024に参加しました。

kansai.tskaigi.org

参加した際には感想の記事を作成・公開することが慣習になっているためliuさんとともに参加した感想を書いてみたいと思います。

過去のカンファレンス参加記事はこちらから

diggle.engineer

diggle.engineer

TSKaigi Kansai 2024について

TSKaigi Kansai 2024は「2024年5月に東京で開催されたTSKaigi 2024から派生した初の地域型イベントです」とある通り、TSKaigi 2024から始まったカンファレンスです。

以前RubyKaigiに参加させてもらった経験や、フロントエンドでTypeScriptを使っていることから、TypeScriptやフロントエンドにまつわるカンファレンスにオフラインで参加してみたいという気持ちがありました。DIGGLEの開発環境を鑑みてもTSKaigi 2024はマッチしているなと思ったのですが、開催を知ったタイミングが開催当日になってしまったためやむなくオンラインでの参加になりました。

一方でTSKaigi Kansai 2024は幸いカンファレンスの開催を早めに認知することができ、中部在住の私(ito)にとっても行きやすい場所であったため関西在住のメンバーとともに参加することを決めました。

DIGGLEではカンファレンス参加にあたって補助が出る場合があるため、CTOに確認をお願いしました。確認の結果、無事会社の援助を受けて参加できることになりました!

実際のSlackのスレッド

セッション/LTを見た感想

オフラインで参加した私とliuさんで多くのセッション/LTを見ました。

見た発表すべてが面白く、さまざまな知見を与えてくれました。以下では特に気に入った発表をピックアップして感想を書いていきます。

オープニング

主催のかたから「未知の世界に飛び込むのもよし、既知の知識を深めるのもよし。みなさんそれぞれの興味関心に合わせてTSKaigiを楽しんでください」というお話をいただきました。 参加者が充実した一日を過ごせるように導いてくれたオープンニングでした! 色々勉強になる一日になりそうだと思ってワクワクしました!

(liu)

kanban
入り口看板
会場
会場全体。テーブル席とチェア席どちらも用意されています。
TSKaigiItems
TSKaigi Kansaiトートバックとガイドブック

TypeScriptの型システムは万能機械の夢を見るか?

kansai.tskaigi.org

一番最初に参加したセッションは、エイリアンに講演者が連れ去られる映像からスタートし、エイリアンにさらわれる人のコスプレをした講演者が登場(笑)。チューリングマシンとオートマトンの紹介から入り、TypeScriptの型システムは設計の理論上(*1)、チューリングマシンと同等の計算能力を持つチューリング完全であることと、その強さを紹介する内容でした。このセッションで紹介された型システムだけで作り上げられた色々なプログラムの実例を見て、型安全性を担保すること以外に、TypeScriptで実現できることが実は結構あることを再認識できました。

DIGGLEでは、フロントエンドのTypeScript化に以前から注力しています。まだTypeScriptを使う範囲が限られているのと、計算の部分はRuby on Railsで実装されているバックエンドに任せているため、現時点ではほとんどフロントエンドで扱うデータの型安全性の担保としてのみ使われています。そのため、まだTypeScriptで何かの計算を行う処理はあまりないのですが、講演者の話を聞いて使える武器の選択肢がまたひとつ増えました!

(liu)

*1. 登壇者により、TypeScriptの実際の運用ではいろんな制限があるため、本当にチューリング完全と言えるかどうかもっと検証すべきという論点がのべられていました。

テストコード品質を高めるためにMutation Testingライブラリ・Strykerを実戦導入してみた

kansai.tskaigi.org

テストコードはみなさんも書いていると思いますが、そのテストコードの品質はカバレッジだけで本当に品質担保ができるのか?という視点を講演者が述べました。

Mutation Testは、コードに何らかのバグを仕込みます。テストコードが、バグの仕込まれたあるコードから返された不正な結果を検知できるかテストします。言い換えると、Mutation Testはテストコードをテストすることができます。テストコードの品質が担保されることで、コードの品質担保にもつながると思いますので、有用でよい内容でした。

便利なMutation Testツールと使い方も紹介されました。「Stryker」というツールはコードの変異とレポートを自動的に作ります!興味ある方は検索してみてください!

DIGGLEみたいな大規模のプロジェクトだと導入するのは大変そうですが、部分的にちょっとずつ導入すればできそうです!

(liu)

型チェック 速度改善 奮闘記

kansai.tskaigi.org

Nxを使って3つのアプリケーションとそれに紐づく100のモジュールをモノリポ化で管理している環境でのTypeScriptの型チェック速度の劣化とその改善のお話でした。

DIGGLE ではpnpm workspaceではあるものの、同様にフロントエンド環境をモノリポで管理するように移り変わっており、今後同様の事象が発生する可能性があり興味を持って発表を聞きました。

特にDIGGLEで利用を始めているzodにおいて「不要なスキーマ定義があると速度低下の原因になる」ことは実装時に意識から外れてしまう部分だと感じたため、レビューの際に気をつけるポイントとして意識する必要がありそうです。

発表の中で型チェックで時間がかかっている場所の調査方法を実演いただき、DIGGLEで実務に落とし込む際にはどのようにすれば実施できるのか?をイメージすることができました(基本的にはアプリケーションのパフォーマンス改善と似たようなプロセスを経るのだなという印象でした)。 また、スライドで表示されていたきれいなグラフはどのように表示しているのか?がとても気になっていたため、speedscopeという Web アプリを利用していることがわかりスッキリしました。

(ito)

コンパウンド戦略を支えるフロントエンド基盤

kansai.tskaigi.org

ビジネスサイドの展望を踏まえて、フロントエンドの基盤を変化させているお話でした。

ビジネスサイドのどういった要望からフロントエンドを垂直分割することになったのかという話は現在のDIGGLEに通ずるところがあり、DIGGLEの今後の展望に対する重要な示唆を得ました。

垂直分割する際に一旦各画面をnpm package化によって分割し、package化作業が完了したのちインフラも分割していくという流れが紹介されており、自身が考えていた進め方に合致していました。 そのため垂直分割するとなった際には自信をもって手法やスコープの話をできそうです。

(ito)

よくできたテンプレート言語として TypeScript + JSX を利用する試み

kansai.tskaigi.org

テンプレート言語が抱える問題点をTypeScript + JSXの組み合わせを活用することによって解消するというお話でした。

DIGGLEではフロントエンドにてReactを使っているため、JSXには馴染みがあり、テンプレート言語として使うとはどういったことなのだろう?という興味で発表を聞いたのですが、JSXにこういった使い方もできるのか!という驚きを受けました。 私的なアプリ開発でjsx-slackを使ったこともあったのですが、内部的にどういったことをしているのかを本発表で知ることができ、とても面白かったです。

メール基盤での活用や複雑なJSON生成で活用できることがわかり、手法の紹介もいただけたためうまく活用できる部分を業務の中で探していきます。

(ito)

フロントエンドの型安全性を高める!Jotaiを用いたフォーム設計の実践

kansai.tskaigi.org

フォームに関してバリデーションを効かせつつ、複雑なフォームを実装するための1つのアイデアとしてJotaiを活用するというお話でした。

発表ではカーテンの注文フォームを具体例として様々な解決策が紹介されていました。DIGGLEでは紹介された例ほど複雑なフォームは現状ない認識でしたが、通貨単位を今後扱っていく方針になった際には、発表の中で紹介されていた「フォームに表示する要素向けの外部atomと、計算などアプリ内で扱うための内部atomの2つのatomを用意して、それぞれを同期させることで実装をきれいにする」という考え方を活用できそうです。

発表を聞くことで、Parse, don't validateの考え方は強力だなということを改めて痛感しました。

(ito)

全体を通して

当日は京都駅で迷子になりながらもなんとか会場の京都市勧業館「みやこめっせ」に到着し、オープニングから参加することができました。

様々なセッション/LTを通して、DIGGLEで現状悩んでいることと似たようなことをみなさんも抱えており、どのようにその悩み事を解消していったのか?という実例を見させていただいたように思います。

知らない世界に飛び込み、さまざまな知見を得ることができたため、得た知見を活かしてDIGGLEのフロントエンド環境向上を心新たに頑張っていきます。

We're hiring!

DIGGLEではともにプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

リリースフローの改善 - 素早いProductリリースのために取り組んでいること

DIGGLEでは、Productの改善を素早く反映するために2種類のリリースフローを使い分けてリリースを行っています。
現在、より素早いリリース→改善のサイクルの実現のためにリリースフローの改善に取り組んでいるのでそのお話をします。

これまでのリリースフロー

これまで、四半期ごとリリースと週次リリースの2種類のフローで開発を進めていました。リリース時にシステムを止める必要があったり、変更内容が大きいなどProduct利用者へ事前のアナウンスが必要な場合は四半期ごとリリースに含め、そうではない修正は週次リリースでリリースを行っています。修正内容に合わせて2種類のフローを使い分けることで、できたものを素早く顧客に届けることにつながっています。
またDIGGLEでは、デプロイはすでに自動化されており、チェックリスト形式のリリース手順書も整備されているため、入ってすぐの方でもリリース作業が行えるような体制が整っています。

週次リリースのフロー

週次リリースのブランチ運用

週次リリースしてよい機能の開発は、fast-pathブランチから作業ブランチを作成しコード変更とレビューが終わればfast-pathにマージします。 毎週水曜日の段階でfast-pathブランチに未リリース分があれば、masterブランチ → productionブランチの順でマージを行いリリースします。

また通常時は使用しませんが、重大な不具合やセキュリティアップデートが必要な場合などでは、masterブランチから作業ブランチを作成するhotfixでのリリースを行っていました。

週次リリースの課題

Productの成長に合わせてエンジニアチームのメンバーも増えたため、1回あたりの週次リリース対象量が多くなりました。

その結果、

  • リリース前レビューに必要な時間が増える
  • トラブル時の原因特定に時間がかかる

といった問題があり、1回のリリース作業が長引いてしまうことが度々起こっていました。

シンプルなフローの検討

週次リリース作業に時間がかかってしまう大きな原因は、1回あたりのリリースが大きいからでした。 それなら、できたものを都度リリースするようにしてしまえば、リリース作業が長引く問題は解決するのではと考えました。

これまで週次リリースでは、

  • 作業ブランチをfast-pathブランチにマージする際のレビュー
  • fast-pathブランチをmasterにマージし、本番環境にリリースする際のレビュー

の2回レビューが発生していました。

これを都度リリースではmasterから直接作業ブランチを作成し、レビューした後リリースすることで、

  • 週次リリースの準備に使用していたブランチが不要になり、Hotfixと同じようなフローになった
  • 2回発生していたレビューが1回のみになった

といったようにシンプルなフローとなりました。

都度リリースのブランチ運用

またこれによって

  • レビュー完了後、即時リリースした方がリリース待ちの期間が短くなる
  • 即時リリースでは、レビュー・検証が済んでからリリース準備を始めるので、リリース作業時間が短い

といったメリットが享受できることを期待しています。

段階的な実施

即時リリースに切り替えるにあたって考えられるリスクをエンジニアチーム全体で話し合った結果、すぐに改善しにくいような懸念点もあったものの、 仮にうまくいかなかったとしても前の手順に戻すことは容易だったので、時間をかけて懸念点をすべて解決し切ることよりも実際に運用してみて特に問題になることをフォーカスして解決していくことにし、段階的に移行することを計画しました。

具体的には、最初の期間は一部のチームで運用を回してみてから次の期間で全員に適用するようにしました。

まとめ

リリースフローを週次から即時に変更することで、1回のリリース作業時間を短くしつつ、デリバリーの速度を早めることができました。 今後の課題として、検証に使用する環境をより使いやすくすることや、デプロイ自体の時間を短縮することがあります。

リリースフローに限らず一度複雑化してしまったルールをやめてシンプルにすることは、ときに痛みをともなう行為だと思いますが、反応を見ながら少しずつ変えて試してみるのも一つの方法だと思うので、参考になれば幸いです。

pg_repack運用の勘所

はじめに

DIGGLEエンジニアのaki0344です。
DIGGLEではPostgreSQLを利用しています。
PostgreSQLは自動バキュームによって適時統計情報が更新されますが、パラメータやユーザの使い方などの要因により、こちらの意図したタイミングでバキュームが実行されない可能性があります。
そこで、自動バキュームが実行されない場合でも一定の性能を保つために、毎日ユーザの利用が少ない夜間にpg_repackを実行することで統計情報の更新を行っています。

reorg.github.io

今回はpg_repackを使用する中で、DIGGLEを利用していただいている企業が増えてきたことで発生してきた問題と、それに対してどのように対処してきたかを書いてみたいと思います。
この記事が、pg_repackを利用中の方、あるいはこれから利用しようと考えている方の一助となれば幸いです。

発生した問題点と解決策

前提として、実装当初は以下のようなコマンドを実行していました。

pg_repack  -j 2 -D

コネクション数はPostgreSQLのコア数に合わせて指定し、60秒経過してもロックが取得できない場合はテーブルの更新をスキップします。
また、すべてのスキーマに対して実行するためスキーマ名は指定していません。

【問題その1】毎回同じスキーマで失敗する

ある日、pg_repackの実行が途中で失敗する事象が発生しました。
統計情報が更新されないのが1日であれば性能にはそれほど影響はないため、翌日に成功すれば問題はありません。
そのため、最初に失敗した際は特に対応はせず、様子を見ることにしました。
ところが、翌日も途中で失敗となりました。
ログを確認すると前日と同じ個所で失敗しているようです。
ログには以下のようなエラーが出力されていました。

ERROR: query failed: ERROR:  type "pk_123456789" already exists

データベースを確認した結果、pg_repackというデータベースに、エラーに出力されている情報が残っていることがわかりました。

sample_database => \dt
              List of relations
 Schema |      Name       | Type  |  Owner   
--------+-----------------+-------+----------
 repack | log_123456789   | table | postgres

sample_database => \d log_123456789   
                                       Table "repack.log_123456789"
 Column |             Type             | Collation | Nullable |                  Default                  
--------+------------------------------+-----------+----------+-------------------------------------------
 id     | bigint                       |           | not null | nextval('log_123456789_id_seq'::regclass)
 pk     | pk_123456789                 |           |          | 
 row    | schema_sample.tables |           |          | 
Indexes:
    "log_123456789_pkey" PRIMARY KEY, btree (id)

本来は統計情報更新後に削除されるはずの一時データが、何らかの契機で残ってしまったようです。

【解決策】pg_repackのデータベースを毎回作り直す

公式ページトラブルシューティングに解決策が記載されていました。

FATALエラーが発生した場合、手動でクリーンアップを行う必要があります。
クリーンアップするには、pg_repackをデータベースから一度削除し、再度登録するだけです。
PostgreSQL 9.1以降では、DROP EXTENSION pg_repack CASCADEをエラーが起きたデータベースで実行し、続いてCREATE EXTENSION pg_repackを実行します。

障害が発生するたびにコマンドを実行するのは手間がかかるため、毎回pg_repackを実行する前にDROPおよびCREATEコマンドを実行するようにしてこの問題を回避しました。

【問題その2】処理が終わらない

DIGGLEではApartmentを利用してマルチテナントを実現しています。

diggle.engineer

PostgreSQLでApartmentを利用する場合、テナント毎にスキーマが作成されます。
一方、pg_repackはスキーマ/テーブル毎に処理を行うため、DIGGLEを利用する企業の増加に比例して、pg_repackの処理時間も増加していくことになります。
また、pg_repackはAmazon ECSのコンテナ上で他の処理とリソースを共有して実行していますが、キャッシュによる空き容量のひっ迫や万が一のメモリリークに備えて、コンテナを定期的に再起動させています。
DIGGLEの利用者が増えることは大変喜ばしいことですが、それによりコンテナ再起動の時刻までにpg_repackが終わらないという問題が発生しました。
pg_repackコマンドでスキーマを指定しない場合、統計情報の更新はスキーマ名の昇順で実行されます。
したがって、毎回途中で処理が止まってしまうと昇順で後半となるスキーマの更新される機会が失われることになります。

【解決策】同時実行処理数を増やした

公式ページのオプション-jに以下の説明があります。

PostgreSQLサーバのCPUコア数およびディスクI/Oに余裕がある場合には、このオプションを利用することでpg_repackの処理を高速化するための有力な手段になりえます。

もともと実装当初から同時実行数は指定していましたが、値の更新は行われていませんでした。
一方、PostgreSQLはユーザの増加に伴いスケールアップしてコア数を増やしていたため、同時実行数はコア数よりも小さい値となっていました。
そこで、同時実行数をコア数と同じ値に変更したところ、全体の処理時間が短縮されるという効果を得られました。
ただし、当然ですが同時実行数を増やすとPostgreSQLに対する負荷も増加します。
他の処理に影響を与えないよう、この対応を行う場合は普段のpg_repack実行時のPostgreSQLのCPU使用率などを確認しながら、段階的に行うことをお勧めします。

【問題その3】まだ処理が終わらない

同時実行数を増やすことでコンテナ再起動の時刻までに処理できるスキーマも増加しましたが、全スキーマに対する処理完了までには至りませんでした。

【解決策】開始時間の前倒し

統計情報の更新が実施されない時間が長くなると、性能が劣化する恐れがあります。
すでに数日間更新が行われていないスキーマが発生してたため、早急に対処する必要がありました。
そこで、応急的な対応としてpg_repackを開始する時刻を早めることにしました。
その結果、コンテナ再起動の時刻までにすべてのスキーマに対して統計情報の更新が実行されるようになりました。

【問題その4】不定期に失敗するようになった

pg_repackの開始時刻を変更してから、処理がすべて成功する日と一部で失敗する日が発生するようになりました。
失敗の発生が不規則で、1日だけ失敗する時や、数日間に渡って失敗が続く日がありました。
調査したところ、PostgreSQL側に原因となるログがありました。

ERROR:  schema "schema_sample" does not exist

DIGGLEでは解約や不要となったデモ環境を削除する際、一旦スキーマを論理削除して一定期間経過後に物理削除しています。
物理削除は毎日自動で行われますが、pg_repackの開始時刻を変更したことにより、スキーマの削除される時刻がpg_repackの開始後となっていました。
一方、pg_repackはコマンド実行時に対象となるスキーマをすべて取得して順に統計情報の更新を行っていくようです。
そのため、すでに削除されたスキーマに対して統計情報の更新をしようとして失敗となっていました。
また、上記エラーが発生するとDBとのセッションが切れてしまうようで、失敗となったスキーマ以降の全スキーマに対する処理が失敗となっていました。

【解決策】スキーマ削除はpg_repack開始前に終わらせる

毎日実行されるスキーマの物理削除処理を、pg_repack開始時刻よりも前に実行するようスケジュールを変更しました。
これにより、pg_repackの実行中にスキーマが変化することは無くなり、エラーは発生しなくなりました。

【問題その5】リトライ時に最初からやり直しになる

pg_repackの失敗は性能の劣化につながるため、何らかの要因により失敗した場合に備えてリトライを行うように設定していました。
pg_repack自体は処理が途中で失敗しても、どこまで実行したかという情報は保持していません。
また、各テーブルに対して統計情報の更新が必要か(統計情報が古くなっているか)の判断は行わず、指定された全スキーマに対して処理を実行します。
そのため、リトライ処理でも最初から処理をやり直すことになり、長時間動き続けることになります。
DIGGLEではアクティブユーザの少ない夜間にpg_repackを実行していましたが、リトライが発生するとユーザが利用を始める時間にもpg_repackが動き続け、PostgreSQLのリソースを一部占有し続けるようになりました。

【解決策】どこまで実施したか保持して、リトライ時は続きから実施する

pg_repackの機能では賄えないため、呼び出し元で対処を行いました。
初回実行時、メモリ上にスキーマ一覧を保持し、pg_repackコマンドで各スキーマを指定して実行します。

pg_repack  -j 4 -D -c schema_sample

実行したスキーマはメモリ上から削除します。
リトライ時はメモリに残っているスキーマに対してのみpg_repackを実行します。
これにより、リトライが実行された場合でもユーザが本格的に利用を始める時間帯よりも前にすべてのスキーマに対して統計情報の更新が実行されるようになりました。

今後の課題

現状のテナントに対する統計情報の更新は想定時間内に完了するようになりましたが、DIGGLEはさらなる成長を目指しています。
契約数が増加するとそれに比例してスキーマも増えるため、統計情報の更新も現在より時間がかかることになります。
今後はPostgreSQLのリソースとの兼ね合いを見ながら、並列実行などの対処も考えていく必要があると考えています。

Rails7.1に上げようとしたらハマりまくった話

DIGGLEエンジニアのreenjです。
2024年4月下旬ごろにDIGGLEで使用しているRailsを7.0から7.1へアップデートする作業を行いました。 マイナーバージョンのアップデートであるため、当初それほど問題なくアップデートできるだろうと考えていました。
実際に、簡単にアップデートできたという記事を多く見かけます。

しかしDIGGLEでのアップデートの場合は、思いの外いろいろとハマってしまい苦戦しました。
対応内容と変更箇所、そしてハマってしまったことについてこちらの記事で紹介していきます。

対話形式によるファイル変更

基本的には「Railsアップグレードガイド」の内容に従い、アップデートします。

railsguides.jp

まずは、Gemfileを編集しRailsのバージョンを7.1へアップデートします。その後、以下のコマンドを実行します。

$ bin/rails app:update

コマンドを実行すると、対話形式で変更するファイルの確認が行われます。
ここではとりあえずすべてyesと返答し、すべての変更を加えます。

差分が出るため、各差分に問題がないかを確認します。

差分となっている箇所の多くは、リリースノートやガイド、設定項目などで説明されています。
問題があるかどうかは各説明などから判断します。

railsguides.jp

railsguides.jp

例えば、環境固有の設定ファイルに以下のような差分があります。

- config.cache_classes = false
+ config.enable_reloading = true

config.enable_reloadingは新しく追加された設定項目です。!config.cache_classes と同等であり置き換わっています。
設定内容としては、これまでと同等であるため特に問題ありません。

一通り差分の確認をし、ガイドやリリースノートの内容で修正が必要な箇所は適宜修正します。
その後は自動テストを行います。
もし自動テストでエラーが発生していれば、エラー箇所を修正します。

自動テストがすべて通った後は、念のため本番同等の環境で各機能の確認をざっと行います。
これで問題が発生しなければリリースへ向けた流れとなります。
しかし、実際に上記の流れで進めていったところいくつもエラーが発生し、アップデートが難航しました…...。

ハマった点1 サードパーティのライブラリがRails7.1に対応していない

DIGGLEではros-apartmentactive-hashというライブラリを使用しています。

しかし、これらサードパーティのライブラリは2024年の4月下旬ではRails7.1に対応したリリースを出しておらず、これらライブラリを使用した機能を使うとエラーが発生してしまいました。

ros-apartmentへの対応

ros-apartmentは、2022年2月7日に2.11.0がリリースされて以降、2年以上リリースがされていませんでした。しかしながら、全く更新されていないということではなく、Rails 7.1への対応はすでにマージされており、リリースはされていないといった状況でした。
そのため、Gemfileを以下のような特定コミットを指定して使用するように変更しました。(この方法は、もしrebaseが行われるとハッシュ値が変わりライブラリが取得できなくなるため、少し注意が必要な方法です。)

gem 'ros-apartment', git: 'https://github.com/rails-on-services/apartment.git', ref: 'xxxxxxx', require: 'apartment'

※DIGGLEでRailsのアップデートを行った直後(2024月5月21日)にRails7.1に対応した3.0.0がリリースされました。
そのため、現在はライブラリのバージョンを更新すれば解決します。

active-hashへの対応

active-hashも同様に、2024年4月下旬時点では、Rails7.1のサポート対応が入っているものの、リリースされていませんでした。必要だったRails7.1のサポート対応としては、こちらのPRの内容です。
大きな対応ではないため、こちらはパッチで対応することとし、以下のモンキーパッチを作成しました。

module ActiveHashMonkeyPatch
  def composite_primary_key?
    false
  end
end

require 'active_hash/version'
require 'active_hash/base'
if ActiveHash::Gem::VERSION == '3.2.1'
  ActiveHash::Base.singleton_class.prepend(ActiveHashMonkeyPatch)
end

※こちらも現在は最新リリースに、Rails7.1の対応が入っているため、パッチの対応は不要です。

ハマった点2 Zeitwerkへの対応

RailsではZeitwerkというオートローダーを使用しています。
オートローダーは、プログラム実行時に必要に応じて自動的にソースを読み込む機能です。
自動読み込み対象のクラスやモジュールはrequireincludeせずに使用できます。

Rails 7.1からはlib配下がデフォルトの自動読み込み先として追加されています。DIGGLEでは、Zeitwerkで自動読み込みすることを想定しておらず、lib配下にZeitwerkの規則に従っていないディレクトリやファイルなどを多数配置していたため、これらが問題になりました。
そのため、それらを適宜Zeitwerkで自動読み込みできるようにするか、ディレクトリ自体を除外するかを整理する必要がありました。

techracho.bpsinc.jp

自動読み込みからの除外

Rails 7.1から設定項目config.autoload_lib()が追加されています。
これにより、lib配下が自動読み込みされます。

lib配下で自動読み込みされることを想定していないディレクトリは、以下のように除外する対象に追加します。

config.autoload_lib(ignore: %w([除外するディレクトリ] [除外するディレクトリ] ...))

また、lib直下に格納されているファイルで自動読み込みしないものは、適宜ディレクトリを作成し移します。作成したディレクトリも除外対象に追加します。

この操作によりパスが変わるため、移したファイルを使用している箇所は適宜修正が必要になります。

Zeitwerkの形式に修正

Zeitwerkでファイルを読み込む際にはクラス、モジュール名とファイル名が以下のように対応している必要があります。

lib/my_gem.rb         -> MyGem
lib/my_gem/foo.rb     -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo

参考: GitHub - fxn/zeitwerk: Efficient and thread-safe code loader for Ruby

Zeitwerkにより自動読み込みされるようになったら、不要なrequireは削除します。

その他の修正内容

  • Rails.env.local?メソッドが追加されました
    Rails.env.development? || Rails.env.test?と等価です。 使用していたので置き換えました。

  • #to_s(:format)メソッドが非推奨化され、#to_fs(:format)に置き換わりました (参考)
    こちら一部#to_s(:format)使用していたため、置き換えました。

  • ActionController::Parametersの変更点への対応
    アプリケーションでparamsを呼び出すと、ハッシュではなくオブジェクトが返されるようになりました。
    paramsを呼び出しオブジェクトが返されるようになった箇所にto_hを追加しました。

  • Active Support変更点への対応
    ログをbroadcastするpublic APIが追加されました。
    この変更の影響で、エラーが発生していたためloggerの設定を修正しました。

まとめ

マイナーバージョンのアップデートにしては、思いの外修正する内容が多く、またサードパーティのライブラリがRails 7.1に対応したリリースを出していない、といったところでどうしようかと悩まされたアップデートでした。
現時点では、すでにRails7.1への対応がリリースされているため、ros-apartmentactive-hashのライブラリの問題をどうこうする必要はなくなっていますが、本記事が同じようにRailsのアップデートをする方の助けになれば幸いです!

最後にチームメンバーの方々、エラー原因が特定できずに行き詰まっていた際や対応方針に困っていた際に助けていただき、また、本記事をレビューし文章を改善いただきありがとうございました!

RubyKaigi2024 に参加しました

DIGGLEは、2023年に続いて2024年も継続してRubyKaigiスポンサーを努めました。5/15〜5/18に沖縄で開催されたRubyKaigi2024にnikuが参加したので、その際の感想を公開します。

RubyKaigiの発表は「Ruby自体をよくする、Rubyの適応領域を既存から押し広げてゆく」という観点で捉えると私にはすっきりとまとめられるものが多いように感じました。全部面白かったのでどの感想コメントにも「面白かったです」というのがついていると思ってください。 (参加していなかった時間帯もあります)

1日目(5/15)

Writing Weird Code - RubyKaigi 2024

Weirdなコードを書くのは当人は楽しくやっていると思いますが、Rubyのコードってどこまで無茶なことできるの、変わったことできるのという探求は、Rubyのコードの(読み手からみた)整合性や一貫性に大きく影響すると感じました。Weirdなコードを書くときに、普通に書いてからWeirdな形に変えていくの、知らなかったなあ。言われてみれば、なるほどという感じですね。

Unlocking Potential of Property Based Testing with Ractor - RubyKaigi 2024

このトークはタイトルからはRubyを使ったライブラリを作った話という捉え方にもなりそうですが、IOではなくCPUがボトルネックになりやすいPropertyBasedTesting(PBT)をRactorを使うとどうなるか、現在のRactorの現実的なユースケースでの性能評価をするというのが目的に含まれていて、単なるライブラリを作った話ではありませんでした。着想が素敵ですね。もう一点、多くの言語ではExampleBasedTestingが最初にあり、PBTやFuzzingはここから認知されていくという感じだと思います。(Go言語はFuzzingを標準ライブラリとして組み込んでいますね)。PBTは考え方に慣れればそれほど大変ではなく書け、役に立ち、かつメンテナンスコストも現実的かなと思うので、Rubyでもこれから使われていくとどうなるのかなという点で私も興味がありますし、こういった品質担保のやり方が普通なコミュニティとなることでRubyの適用領域がさらに広がっていくといいですね。

Strings! Interpolation, Optimisation & Bugs - RubyKaigi 2024

"#{foo}#{bar}"のような文字列補完を実行するときの処理を8行消すことで、場合によっては2倍速くなる変更を加えたお話でした。Stringは文字列のサイズ(長さ)によって収める領域が異なります。過去にコミットされていた文字列補完最適化の処理が誤っていたので、収める領域が変わるときは遅くなっていた。またその文字列補完最適化は過去は速かったが現在は意義の薄いものだった。といったように複数にこみいった事情をかかえた状況で、最終的には単にその最適化ルートを取り去ることで速くなったそうです。結果は簡単ですが、こういった状況の見つけかた、調査、対処のアプローチといったプロセスが丁寧に解説されていて門外漢にもわかりやすかったです。ObjectSpaceってこうやって使うんだあ。

irb
irb(main):001> require 'objspace'
=> true
irb(main):002> my_str = 'hello'
=> "hello"
irb(main):003> puts ObjectSpace.dump(my_str)
{"address":"0x12035a1f8", "type":"STRING", "shape_id":0, "slot_size":40, "class":"0x10296ec60", "embedded":true, "bytesize":5, "value":"hello", "encoding":"UTF-8", "coderange":"7bit", "memsize":40, "flags":{"wb_protected":true, "old":true, "uncollectible":true, "marked":true}}
=> nil

The depths of profiling Ruby - RubyKaigi 2024

新しくpf2というプロファイラを作った理由と、どのように取り組んだかというお話でした。素朴なプロファイラでは得られない、Rubyの複数Thread動作の記録、C言語のスタックトレースとRuby言語のスタックトレースを並べて記録できるなどが特徴です。プロファイラは観察対象より下の層の情報も集める必要があるということをおっしゃっていて、なるほど確かにそうだが、作るのも大変だし、作ったあと観察対象の(内部構造が変わる)Ruby最新版に追従し続けるのも大変そうだなという気持ちになりました。使うときは今以上に感謝の気持ちをもって利用します。

Vernier: A next generation profiler for CRuby - RubyKaigi 2024

こちらもプロファイラのお話でした。機能としてはpf2とも重なっている部分が多く、Rubyの複数Thread動作の記録やGCでの時間停止、待ち受け時間(idletime)などを記録できます。時間帯が連続しているのはおもしろいですね。どちらの実装でも情報を集める役の(プロファイラ専用の)スレッドはGVLを避けるためC言語側で作っている、結果をビジュアルで見るにはFirefoxProfilerを使っているなど、複数のプロファイラ作りの視点から得られる、いろんな人が試行錯誤した結果今はこれが王道なのかなというものの見方が得られたのがおもしろかったです。今回のRubyKaigiではあともう一つRubyのプロファイラのお話がありました。三人が並んで親交を深めている写真が素敵でした。

Ractor Enhancements, 2024 - RubyKaigi 2024

Rubyの並列処理をいい感じにするRactorの現時点での状況と、今後のお話でした。Ractorの制限によりRubyコードで使うrequireやtimeoutに対応できないところを解決する試み、GCに時間がかかるのを解決する、メモリ管理の改善を挙げられていました。Ractorをよくするのと既存コードをRactor向けにしていくのを進めていくと、どこかのタイミングで違和感少なく特別な心構えなしに使えるようになると嬉しいですね。引き続き応援しています。RubyKaigiで話題になっていたネームスペースがうまくRubyに入ったらRactorはそれぞれ独立したネームスペースで動きますという形にするとRactor同士のメッセージングは難しくなりますがrequireの問題やsharingobjectの問題などの多くが解決しないかなあと妄想しました。

2日目(5/16)

Finding Memory Leaks in the Ruby Ecosystem - RubyKaigi 2024

Rubyのメモリーリークを発見するためのアプローチと、その結果たくさんのメモリーリークを発見して実際に改善できたというお話でした。一般にメモリーリークの発見に使われるValgrindというツールがあるのですが、Rubyに対して適用すると(機械的に見ると)メモリーリークの可能性があるけれど実際には問題ない処理が多量に列挙されてしまうためあまり役立たなかったそうです。そこでRuby3.3からRUBY_FREE_AT_EXITという環境変数を有効化するとValgrindから見てもメモリーリークに見えなくなる処理を足したそうです。そうするとValgrindで列挙されるメモリーリークの可能性は、本当にメモリーリークの可能性のあるものだけに絞られます。実際にこれを使ってたくさんのメモリーリークの発見に役立っているそうです。発想がすばらしいですね!

Optimizing Ruby: Building an Always-On Production Profiler - RubyKaigi 2024

常に本番環境でプロファイラをオンにしておく話です。プロファイラはプログラムの内部状況を取得するオーバーヘッドがあるため、プロファイラを使っているときはプロファイラを使っていないときに比べて処理能力が落ちてしまいます。ですから本番環境で常にプロファイラをオンにしておくというのは難しく、そんな夢のような話があるんだろうかと発表を聞く前は思っていました。発表者はDatadogにお勤めの方で、Datadogへデータを送るのに有用なddtraceという本番環境で常にプロファイラをオンにしても大きく支障ないgemを紹介してくれました。ちなみに本番環境でプロファイラを常にオンにするための秘訣は、データの一部のみを抜きだす(けれど全体像が掴めるようにする)サンプリングでした。ddtraceを使っていればDatadogにデータを送らずともローカルでも使えることをスライドの紹介で知りました。手元で様子をみるのに便利ですね。 https://docs.google.com/presentation/d/1hKiEQvqzuzkXDxhDiH36nc2Eew80_QYBSLp2vaXWurU/edit#slide=id.g1f7f8fc8955_0_313

class ExportToFile
  def export(flush) = !!File.write("hello-profiler.pprof.lz4", flush.pprof_data)
end
Datadog.configure { |c| c.profiling.exporter.transport = ExportToFile.new }
$ bundle add ddtrace && bundle install
$ DD_PROFILING_ENABLED=true bundle exec ddprofrb exec ruby hello-profiler.rb
$ lz4 -d hello-profiler.pprof.lz4
$ go tool pprof -http :8987 hello-profiler.pprof

Unlock The Universal Parsers: A New PicoRuby Compiler - RubyKaigi 2024

PicoRubyという、mruby互換の軽量なRuby実装があります。そちらをコンパイルするにあたって従来のパーサー、Lramaから生成されたパーサー、Prismの3つの候補が選択肢にある中、どれが向いているのかを比較していました。Lrama由来の場合はコンパイル時のメモリ消費やコンパイル結果のファイルサイズが他のものに比べて大きすぎたのですが、CRubyでは使うけれどPicoRubyでは使わない部分が取り外せず含まれているためで、それを外せるようにしてPicoRubyでも実用に近づけたよという話でした。馴染みのない分野だったのですが説明や図がわかりやすく、状況と問題がつかめたのでどうやって解決しているのかなというのをワクワクしながら聞けました。ちなみにPrismのほうは既にかなり完成されていてPicoRuby実用においても問題は少ないようでした。私の誤解があるかもしれませんが、そこからパーサーという領域では重なっているLramaとPrismのRubyKaigi2024時点での成熟度というのを感じとりました。あくまでRubyKaigi2024時点の話で、今後についてはわかっていません。引き続き楽しみですね。

Squeezing Unicode Names into Ruby Regular Expressions - RubyKaigi 2024

Unicodeにはcharacterpropertiesという、文字の種別が定義されています。たとえば「ひらがな」という種別は\p{Hiragana}で表わすことができます。次のように、既存のRubyでも正規表現で多くのものが使えるようになっています。

#2文字目に「に」が含まれている
'Юに코δ'=~/\p{Hiragana}/
=>1

こういったpropertyは既に大量にあり、またユニコードのバージョンが上がるごとに増え続けています。これをRubyの正規表現の中で扱うには、propertyの定義をRubyプログラムの中にデータ構造として持たなければいけません。単純に保持するのでは、データ量が多くなったり、該当の名前を検索する速度が遅くなります。うまく収めるためにRadixtreeという方式をとったというお話でした。大きいデータを、データの特性に着目してコンパクトにかつ使いやすいように収納していくさまを目にする知的興奮がありました。格好よかった。

3日目(5/17)

Turning CDN edge into a Rack web server with ruby.wasm - RubyKaigi 2024

RubyのWebフレームワークであるところのSinatraで作られたアプリケーションは通常PumaやApacheやNginxといったサーバーの上で動きます。アプリケーションとサーバーの間にはRackと呼ばれるミドルウェアがいて、サーバーの種類毎の違いを吸収した統一されたインターフェースを提供してくれています。そのおかげで、アプリケーションに変更を加えずにサーバーの種類を入れ替えることも可能になっています。今回はCDNEdgeでwasmが動くことと、Rubyがwasm化したことの2つを利用して、RackというミドルウェアがCDNEdgeをサーバーとして扱えるようにしてみよう。それが可能だとすると、アプリケーションの変更を行わなくてもCDNEdgeでコンテンツが配信できるという挑戦のお話でした。説明も平易で伝わりやすく、取り組みは簡単ではなかったのだなとわかりました。実際にCDNEdge上でSinatraアプリケーションが動いているのを見せるデモは説得力がありました。

Speeding up Instance Variables with Red-Black Trees - RubyKaigi 2024

Ruby3.2からはObjectShapeというパフォーマンス最適化方式が導入されています。これによりキャッシュしたインスタンス変数を速く読み取れるようになりました。一方でキャッシュしていないインスタンス変数へのアクセス速度に対しては素朴な実装のままでした。Ruby3.3ではキャッシュしていないインスタンス変数へのアクセスを速くするためにデータ構造として赤黒木を導入したそうです。実際にキャッシュミスのときの速度がRuby3.2より上がっていました。すごい。OkasakiStyleという論文の形式で赤黒木を実装したそうで、その論文のなかに「有能なプログラマなら15分未満で実装できる」と書かれていて実装のときに緊張したというくだりが好きです。

ERB, ancient and future - RubyKaigi 2024

RubyのテンプレートエンジンERBの歴史と、25年経った今そのときの判断が概ね正しかったというふりかえり、2012年には試したけれどできなかったRails互換のERBが最近思いついてできるようになりそうだという話でした。25年何かについて考えたり、10年くらい前にできなかったことを思い出して改めて挑戦してみるというのはその年数のあいだ生きていないといけないわけで、素敵だし、すごいことだなと感じました。歴史がある。

The state of Ruby dev tooling - RubyKaigi 2024

Rubyを使った開発で利用するツールの状況や提言のお話でした。Rubyの開発ツールは、選択肢が多く、多様性が高いとは言えるものの、初心者にはどのツールが適していてどれを入れると始めやすいか悩む原因ともなっているということをRustとの比較で示してくれました。発表者はこの状況をfragmentation(断片化)という表現をつかって説明していました。最近だとLanguageServerが開発ツールのハブになりつつあるので、ここをきっかけに状況を改善できるのではないかとお話してくれました。私も統合されたツールを持つ言語として挙げられていたGoやRustでの開発経験はよかったなと感じているので、一定うなずけるところがありました。ただ、めいめいが頑張って作りメンテしていたものを絞りこんで初期設定おすすめを選んでいくのは思い入れや歴史があり困難な道程になりそうだとも感じています。そうなれば、特に開発を始める人にとって便利なのは間違いないと思うんですけれどもね。うまくいくよう応援したいです。

Matz Keynote - RubyKaigi 2024

Matzのキーノートが最終日にあるのは珍しいですね。Ruby4についての言及もあり刺激的な内容でした。パフォーマンスがよくなって文句を言う人はだれもいない、そのとおりだなと思いました。DIGGLEプロダクトもパフォーマンスを上げ続けていますが、まだまだ速くできるところがあるので、新しい機能の導入とパフォーマンス両方をおいかけていきたいです。

DIGGLEは、RubyKaigi 2024にシルバースポンサーとして協賛します!

※ 上記画像中央のロゴはさわらつきさんの著作物です
※ Rubyのロゴはまつもとゆきひろ氏の著作物です

DIGGLEのCTO水上です。

DIGGLE社は前年に引き続き、RubyKaigi 2024のシルバースポンサーとして協賛いたします。微力ながらRubyコミュニティに貢献ができればと思っております!

rubykaigi.org

DIGGLEとRuby

当社サービスは、2016年のローンチ以来、RubyとRuby on Railsをバックエンドの言語、Webフレームワークとして採用しております。私たちが事業として成長を続けられているのは、RubyとRubyコミュニティのおかげです。

特に、サービス初期の立ち上げと、頻繁かつ大胆な変更の繰り返しを乗り越えた上で、大きな負債なく継続できていることは、Rubyという言語の持つ柔軟性と設計思想、充実した周辺ライブラリがあってのことだと考えています。

DIGGLEで活躍するgemの一部

gem 'active_hash'
gem 'activerecord-import'
gem 'acts_as_list'
gem 'clockwork'
gem 'devise'
gem 'factory_bot_rails'
gem 'haml'
gem 'kaminari'
gem 'mini_racer'
gem 'mustache'
gem 'oj'
gem 'paranoia'
gem 'pundit'
gem 'rails'
gem 'ransack'
gem 'ros-apartment'
gem 'rubocop'
gem 'sidekiq'

今こそRuby / Railsを学ぶべき理由

もっとRuby界隈を盛り上げていきたいので、これを機に、特に初学者であったり、サービスを立ち上げる検討をしている方向けに、私の個人的見解でRubyやRailsを選ぶべき理由をつらつらと書いてみます。よければ選定の参考にしてください。

1. MVC Architecture

Ruby on Railsは、いわゆるMVCアーキテクチャです。

MVCは銀の弾丸ではありません。MVCを使っていると、だいたい私の体感的に、8割は非常に簡単に作れて、残り2割はレールから外れる必要があります。私たちが作るサービスの機能は、複雑度の観点でパレートの法則のような形をしているのだと考えられます。大事なことは、8割でしっかり楽して、2割でしっかり考えぬくことです。初めてサービスを作る方にとって、MVCはきっと丁度よいフレームワークです。

2. HTMLを基本として、適応的にJavaScriptを使える

Ruby on Railsは、シンプルなHTMLベースのWebサーバが基本です。

きっと多くの方が、イケてるUIを目指したいので、フロントをリッチにしたいと思うでしょう。しかし、良いフロントエンドを作るには、JavaScriptの前に、HTMLとCSSについてよく知ることが肝要です。特に、CSSはどんどん進化し多くのことはCSSでカバーできるようになっています。JavaScriptより描画パフォーマンスが良い場合がほとんどです。

Railsは、フロントエンドの節度を守るレールを用意しつつも、時々JavaScriptを使いたくなりそうな課題に対して、いくつか丁度よい道具を提供しています。最近では、それらがすべてHotwireという形で集約されました。もちろん、必要に応じて、Reactなども導入できます。

これらをRailsのレールの上で習得するメリットは、うまく手を抜く技術があがることです。スピード感のあるデリバリーが求められる現代において、頑張る必要がない所で手を抜くのは悪いことではなく、むしろビジネスとして適切な選択ができる能力として重宝されます。

3. 圧倒的なコミュニティとその「近さ」

Rubyという国産言語で、ここまで世界的に使われており、毎年カンファレンスで盛り上がり、多くの人に愛され続け、メンテナンスされている言語はRubyしかないでしょう。

RubyKaigiという一大イベントですぐに最高のハッカー達と、日本語で(あるいは英語で)会話だってできます。エンジニアとしての最高の環境がとても近くにあります。

diggle.engineer

あとがき

DIGGLEとしては、Ruby界隈に引き続き貢献していき、Rubyとともに成長していければと感じております!一企業として一つの言語に向き合いつづけることこそが、Rubyと自社の中長期的な成長に繋がると考えています。

RubyKaigi当日は弊社メンバーも参加しますので、ぜひお声がけください!

水上

herp.careers

DIGGLE React SPA開発におけるアーキテクチャの勘どころ

はじめに

初めまして。 2024年2月にDIGGLEにエンジニアとして入社したfujitaです。

私はこれまで、バックエンドの開発が中心で、フロントエンドはVanilla JS、JQueryでの簡単なDOM操作やAjax通信の実装を行った程度の経験しかありませんでした。DIGGLEでは基本的にエンジニアはバックエンド・フロントエンドの両方の開発に携わるため、フロントエンドの開発に取り組むにあたって、いくつかの困難に直面しました。具体的には、DIGGLEではReactを用いたシングルページアプリケーション(以下、SPA)として構築されており、これまで経験したことがないほどにフロントエンドのコードベースの規模が大きく複雑なため、以下のような難しさがありました。

  • 機能を追加・改修するために、どのファイルのどの部分を修正すべきかがわからない
  • コードを修正した際に、その影響範囲を把握することが難しい

しかし、フロントエンドにおけるアプリケーションアーキテクチャ(以下、アーキテクチャ)、簡単にいえば各ディレクトリ、ファイル、コンポーネントの役割・責務を理解していくにしたがって、これらの困難は徐々に解消され、開発スピードも向上してきました。

本記事では、私がReactを用いた大規模なSPA開発に初めて取り組む中で得た知見や、DIGGLEのフロントエンドアーキテクチャがどのように設計されているかについて共有したいと思います。

Reactを用いたSPAのアーキテクチャ

SPAのアーキテクチャ

基本的なSPAのアーキテクチャとしては、バックエンドでも用いられるMVCや、それが発展してできたMVVMがあり、これらは総称してMVW(Model-View-Whatever)と呼ばれます。これらはいずれもアプリケーションのデータやロジックをもつModelと、UIでありModelの視覚的表現であるViewを要素としてもちます。そして、第3の要素(C: ControllerやVM: ViewModel)によってViewとModelを適切に分離しつつ連携できるようにします。この分離によって、コードの可読性、保守性、テスト容易性などが向上します。

React

ReactはUI構築のためのJavaScriptライブラリです。大雑把にいうとReactコンポーネントはMVW のVを担当し、アーキテクチャとは直接関係しません。宣言的にコンポーネントを作成することができ、状態の変化に応じたUIの更新はReactが裏側でやってくれますが、どのようにアプリケーションのデータを取得・管理し、Reactコンポーネントへ渡すかなどは別に考えなければなりません。(公式では、Next.js や Remix などのフレームワークの使用が推奨されています)

さらに、先ほどReactはMVWのViewに相当すると述べましたが、Reactコンポーネントは自身の状態を管理・更新したり、副作用を扱ったりでき、View以上のものといえます。そのため、一般にReactではMVWとは異なるアーキテクチャが採用されます。代表的なアーキテクチャとしてFluxなどがありますが、開発チームの事情に応じて様々な選択肢があるようです。次項で、DIGGLEのアーキテクチャの一部をご紹介します。

DIGGLEのフロントエンドアーキテクチャ

DIGGLEでは、UIとロジックを分離するために Container/Presentationalパターンが、関心の異なるロジックを分離するためにHOCパターンが用いられています。

なお、状態管理については記事がありますので、ぜひご覧ください。

diggle.engineer

Container/PresentationalパターンによるUIと状態・ロジックの分離

各ページのコンポーネントを実装する際、UIに関心をもつ Presentational Componentと、状態やロジックに関心を持つContainer Componentに分割することで、UIと状態・ロジックを分離します。具体的には以下のようになります。

  1. Container Componentは状態管理APIからグローバルな状態(ログインユーザーの状態など)を取得します。
  2. Container ComponentはバックエンドAPIをコールし、リソースのデータを取得します。レスポンスからViewModelを作成し、それを状態として保持します。 ViewModelはMVVMにおけるものとは若干異なり、APIで取得したデータとそれに関連するメソッドを含んだオブジェクトです。

  3. Presentational Componentは表示に必要なデータやロジックをContainer Componentからpropsとして受け取り、UIを構築します。

以下の記事を参考にさせていただきました。

zenn.dev

コードの例を見てみましょう。以下は、レポートの作成年月やタイトルなどを表示するコードです。

ViewModel

ReportsView は、レポートに関するデータとメソッドをもちます。

ReportsView.tsx

import { Report } from 'Models';
import { Record, OrderedMap } from 'immutable';

const ReportsViewRecord = Record<{
  reports: OrderedMap<any, Report> | null;
  // その他のデータ
  ...
}>({
  reports: null,
  ...
});

export class ReportsView extends ReportsViewRecord{
  // jsonデータからViewModelを組み立てる
  load(){ ... }
      
  // その他のメソッド
  ...
}

Container Component

APIをコールしてレポートのデータを取得後、ViewModelを作成し、状態として保持します。

ReportsContainer.jsx

import { ReportsView } from 'ViewModels';
import { ReportsPresentational } from './ReportsPresentational';

const ReportsContainer = () => {
  const [model, setModel] = useState(new ReportsView());
  // その他の状態
  ...
    
  useEffect(() => {
    fetch('https://xxxx/reports')
      .then((res) => res.json())
      .then(data => setReports((model) => model.load(data));
  }, []);

  return (
    <ReportsPresentational
      model={model}
      ...
    />
  );
};

Presentational Component

レポートの作成年月、タイトルを表示します。

ReportsPresentational.jsx

export const ReportsPresentational = ({ model: { reports, ... }}) => {
  return (
    <ul>
      {reports.map(({ id, title, year, month, ... }) => (
        <li key={id}>
          <p>{year}{month}月:{title}</p>
          // その他のレポート情報を表示する
          ...
        </li>
      ))}
    </ul>
  );
};

メリット

前述したアーキテクチャを取り入れることのメリットであるコードの可読性、保守性、テスト容易性などの向上が得られます。特に主要なデータやロジックが、他のコンポーネントと分離された形でViewModelに含まれるため、ViewModelに対するテストを重点的に書くことで、効率的にテストの恩恵を受けられます。

HOCパターンによる関心の異なるデータやロジックの分離

例えば、あるコンポーネントをモーダル表示したい場合、モーダル表示に関連する状態やロジックを高階コンポーネント (HOC、Higher Order Component)に分離します。高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。

例を見ましょう。以下のModalizeはモーダル表示に関連するデータや状態をもつコンポーネントです。

import { Modal } from '...';

export const Modalize = (Component) => (props) => {
  const [isOpen, setIsOpen] = useState(false);
  const open = ... // モーダルを開くメソッド
  ...
  return(
    <Modal
      open={open}
      ...
    >
      <Modal.Header>
        ...
      </Modal.Header>
      <Modal.Content>
        <Component {...props} />
      </Modal.Content>
    <Modal />
  );
};

コンポーネント MyComponent をモーダルで表示したい場合は、以下のようにします。

const EnhancedMyComponent = Modalize(MyComponent);

メリット

モーダル表示に関連する状態やロジックが分離されているため、これを様々なコンポーネントをモーダル表示するために再利用することができます。また、複数の高階コンポーネントを自由に組み合わせることもできます。以下は、モーダル表示のための高階コンポーネントである Modalize と非同期処理のための高階コンポーネントである WithAsync の機能を MyComponent に組み込むコードです。

const EnhancedMyComponent = WithAsync(Modalize(MyComponent));

ただし、高階コンポーネントの数が増えるとコードが読みにくくなります。

const EnhancedMyComponent = HOCn(...(HOC2(HOC1(MyComponent)))...);

デコレータを利用すると以下のように書けます。

@HOCn
...
@HOC2
@HOC1
MyComponent

デコレータの詳細は以下をご覧ください。

github.com

www.typescriptlang.org

今後の展望

ここまで紹介したコードを分離するためのパターンは、コンポーネントを利用したものでした。そのため、不必要な再レンダリングを引き起こす可能性があったり、テストが難しかったりする場合があります。これらの問題を解決し、より簡単かつシンプルにコードの分離を行うために、今後、Hooksの活用を積極的に進めていきたいと考えています。

より根本的な目標は、ReactのHooksを活用した宣言的アプローチへの進化に追随することで、Reactの利点を最大限に引き出し、プロダクトの価値を高めることです。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers