2024年|CANARY Cloud Web の技術スタック
はじめに
こんにちは。カナリーでソフトウェアエンジニアをしている @berry99 です。
私たちは 【もっといい「当たり前」をつくる】
をミッションに掲げている不動産テックカンパニーです。弊社では、現在下記のプロダクトを運用しています。
- 「CANARY」: BtoC の部屋探しポータル(アプリ/Web)
- 「CANARY Cloud」: BtoB SaaS(不動産の仲介会社様向けの顧客管理システム)
この記事では、 CANARY Cloudで利用している技術スタックをまとめています。
この記事を読んでわかること
- CANARY Cloud のフロントエンドについて技術スタックの全体像・概要が理解できる
- CANARY Cloud のフロントエンドで利用しているモジュールとその歴史
話さないこと
- CANARY Cloud 視点以外でのアーキテクチャ
- 各ライブラリの細かい内容・主要でないライブラリ
注意書き
- この記事は、自分含めて CANARY Cloudの開発をメインで行っていたエンジニア @yoshi-jr / @snamiki1212 / @miyasan_dev と共同で執筆しています。
- この記事は、執筆開始時点での内容となり日々リファクタ・技術スタックの変更なども行われている関係から公開日時点の情報が最新というわけではありません。
アーキテクチャ
まずは、CANARY Cloud の Web について全体感がわかるインフラ図となります。
(わかりやすさを優先して図を作っているため、厳格に正確ではありません)
ここからはインフラ図にある各要素について説明します
弊社 CANARY が提供している不動産の仲介会社様向け顧客管理システムです。対象となる利用者ごとに、大きく 3 つに分類できます。
-
canary_cloud console
- (利用者)不動産仲介業の営業担当者。
- 不動産仲介業者の人とお部屋探しをしているお客様とのコミュニケーションを主に行います。
-
canary_cloud management
- (利用者)不動産仲介業のマネージャーライン。
- CANARY Cloud を利用している不動産仲介業の営業担当者への権限管理、情報の閲覧、全般などの設定をします。
-
canary_cloud customer
- (利用者)お部屋探しをしているお客様。
- お部屋探しの条件を更新したり、提案を受けているお部屋情報を閲覧できたりします。
GitHub Actions
GitHub Actions は GitHub のリポジトリに紐づいて実行される CI/CD ツールです。CI にて下記のようなフローを設定し運用しています。
- PR をオープンしたときに自動のラベリング
- マージしたときに自動デプロイ
- 自動でテストや linter などを実行
Firebase
Firebase Authentication を用いて、認証管理を行なっています。
CANARY Cloud はマルチテナントなため組織ごとに見られる情報が異なったり、また複数組織へのログインを行うようなユースケースがあります。その中で、認証に関する基盤として Firebase Authentication を利用しています。
Sentry
Sentry を用いて、CANARY Cloud のフロントエンドの各種モニタリングを行なっています。
- ログモニタリング
- アラートモニタリング
- パフォーマンスモニタリング
毎日新しいエラーのチェックと毎週エラーのトリアージを行なっています。
Datadog への移行予定
執筆時点ではSentryでしたが、現在Datadogへの移行を行なっています
Notion
Notion を用いて、ドキュメントの管理・公開をしています。主な使用例として下記の通りです。
- FAQ ページ
- ヘルプページ
- 使い方ガイド
メンテナンスコストが低く、ビジネスチーム側でも運用・コントロールできる面が多いため採用して活用しています。
Onboarding
Onboarding を用いて、ノーコードによるプロダクトツアーを実現しています。具体的には下記のような箇所で利用されています。
- チュートリアル:初回ログイン時や新機能に対するガイダンス
- リリースノート:情報のユーザーへの周知
限られた開発工数の中でサービスの拡張をする手段として、プロダクトのコア機能以外かつ外部サービスで実現できる範囲として Onboarding を利用しています。また、他のメリットとして運用オペレーションや拡張作業の一部をビジネスチームでも進めることもできています。
技術スタック
ここからは、CANARY Cloud で使用している言語・フレームワーク・ライブラリなどの技術スタックについてまとめていきます。
React
React は UI 定義のライブラリです。選定した理由は、React を書けるエンジニアが複数名在籍していることと、先行リリースされていた toC 向けサービスの CANARY でも React が使われているからです。
TypeScript
TypeScript は、一言で言うと、型をつけられる JavaScript です。選定理由は、型安全にアプリケーションロジックをかけることと、型補完や型チェックなどを行って、開発効率を上げ、実行時のバグを減らせるためです。
バージョンは TypeScript(4.9.4)React と併用して利用しています。
Next.js
Next.js は React のフレームワークです。選定理由は先行リリースされていた toC 向けサービスでも Next.js が使われていたからです。もともと CRA でしたが、Next.js に移行しました。
背景としましては CRA は現在メンテナンスモードであり、活発なアップデートがなさそうなのと CRA が依存しているreact-scripts
がさまざまなライブラリに依存していること。
また webpack4 でビルドが遅いのも移行理由となります。
SSR 時にはローディングページをレンダリングし、クライアントは API 経由でユーザーデータ取得後にあらためてクライアントでレンダリングしています。
TanStack Query
TanStackQuery は、主に API 通信する際に利用しています。
API 呼び出しをするレイヤー(usecase ディレクトリ)を設けており、そのレイヤーで各機能が必要な API 呼び出しを useQuery
useMutation
を用いて定義しています。
レイヤーを設けていることでキャッシュキーの管理やキャッシュ更新処理の影響範囲がわかりやすくなっています。
React Hook Form
React Hook Form は入力時のレンダリングを抑えつつ、React でフォームを簡単に扱うことのできるライブラリです。React の Hooks を使用して、フォームの状態管理やバリデーションを行うことができます。
form 機能が大量に必要になるサービスであるため、それらの form を簡単に扱うために React Hook Form を導入しました。
もともと Formik を利用していましたが、API 設計が RHF(Hooks ベース)とは異なりコンポーネントベースであるために、複雑な処理では難読化しやい・扱いにくいという問題があり、移行を決めました(現在も移行中)。
Zod
Zod は、入力フォームを通してユーザの入力する値が、適切な値かどうかチェックするためのバリデーターとして利用できます。
型が複数あり、それぞれ特有のバリデーションを作ることが可能です。
用途は、React Hook Form のバリデーションです。
Formik を使っていたときは、yup を利用していましたが、React Hook Form 移行と同時に Zod に移行しています。Zod の方が軽量であり型推論に優れているからです。
atlassian/react-beautiful-dnd
react-beautiful-dnd は、ドラッグアンドドロップが簡単に実装できる便利なライブラリです。
選定理由は、Storybook にサンプルがあって、実装イメージがわきやすく、実装コストが低かったからです。
CANARY Cloud では、カンバン機能を備えており、そこで利用しています。
※ 現在 Deprecated になっており、@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-migration
に移行しております。
bokuweb/react-rnd
react-rnd は、リサイズ・ドラッグ&ドロップが可能なコンポーネントを作成できるライブラリです。
CANARY Cloud ではメール送信や LINE・SMS 送信などさまざまなアクティビティ機能が備わっており、
その際エディターが必要になりますが、それを自由に配置したり拡大縮小したりできるようにするため利用しています。
Lexical(Draft.js)
Lexical は、WYSIWYG エディターが実装できます。低レイヤーからカスタマイズ可能で、好みの操作感を実現できます。
Meta 製の OSS であり、リリース当初は Draft.js を使用していましたが、不具合が多く乗り換えを実施しました。
色々候補があった中、営業チームから見た時の利便性や使用感の良さから Lexical を選択しました。
FullCalendar
FullCalendar は、Google カレンダーライクなカレンダー表示が可能です。現状、日別・週別・リスト表示に対応しています。
選定理由は、Google カレンダーライクで UI も良く、カレンダーを自作しなくても使用できることです。
用途としては、顧客ごとの予定やタスクを週別・日別などで一目でわかるようにしています。
Jest
Jest は JavaScript のテストフレームワークです。統合された環境を提供し、高速なテストの実行が可能です。モック・スパイの作成やスナップショットテストなどの便利な機能も備えています。React Testing Library との組み合わせができて、React のテストが書きやすいため使用しています。
まだ完全ではありませんが、設定画面などで局所的にテストを実装しています。
設計
ディレクトリ構成
全体像
CANARY Cloud の主要なディレクトリ構造は以下の様になっています。
src
├── apps
│ ├── crm # Canary Cloud の顧客管理画面に関するモジュール
│ │ ├── components # サービス内で共通利用されるコンポーネント
│ │ ├── hooks # サービス内で共通利用される Hooks
│ │ ├── lib # サービス内で共通利用されるライブラリ・汎用関数
│ │ ├── providers # サービス内のグローバルステート
│ │ ├── usecases # サービス内の API 呼び出し
│ │ └── views # サービス内の画面コンポーネント
│ ├── admin # Canary Cloud のアカウント管理画面に関するモジュール
│ │ └── (crm と同じ構造)
│ ├── auth # Canary Cloud の認証画面に関するモジュール
│ │ └── (crm と同じ構造)
│ ├── management # Canary Cloud の顧客管理画面に関するモジュール
│ │ └── (crm と同じ構造)
│ └── portal # Canary Cloud のポータル管理画面に関するモジュール
│ └── (crm と同じ構造)
├── components # サービス間で共通して利用されるコンポーネント
├── hooks # サービス間で共通して利用される Hooks
├── lib # サービス間で共通して利用されるライブラリ・汎用関数
├── pages # Next.js の pages ディレクトリ
├── routes # ルーティングに関するロジック
└── usecases # サービス間で共通する API 呼び出し
ディレクトリ構造では以下のようなことを意識しています。
サービスごとの分割
CANARY Cloud では複数のサービスを展開しています。
これらを効率的に管理するため、それぞれのサービスを独立したモジュールとして扱っています。これにより、サービス間の相互依存を防ぎ、他のサービスへの影響を最小限に抑えつつ機能の追加・修正が可能になっています。
今後は、サービスの増加とチームの分割、そしてプラットフォームチームの拡大に伴い、各サービスを個別のリポジトリで管理するような方針です。
依存方向の制限
モジュールの依存関係を一方向に制限しています。
具体的には、サービス内で views → components/hooks → usecases → lib/utils/providers という依存方向を設けています。この一方向の依存により、モジュール間の結合度を低く保ち、実装をシンプルで理解しやすいものにしています。また、この制限によってモジュールの入れ替えが簡単になっています。
課題感
ディレクトリ構造の課題として、技術駆動パッケージングが挙がっています。
技術駆動パッケージングが行われていることで、以下のような問題を感じていました。
- 特定の機能に関するロジックが utils の中に入っており、機能のロジックを探すのが大変
- 新しくロジックを追加したいときに、毎回どこにおくべきか迷ってしまう
この問題を解決するため、ディレクトリ構造を「packaged by feature」へと移行しています。
機能ごとに関連するロジックをまとめることで、コードの配置場所が明確になり、モジュールの追加・修正がより簡単にできると考えています。
コンポーネント設計
CANARY Cloud では、以下のことを意識したコンポーネントを作成しています。
コンテナー・プレゼンテーションパターン
コンテナー・プレゼンテーションとはコンポーネントをロジックやデータの保持部分と UI の表示部分に分割するパターンです。
コンテナーコンポーネントでは状態管理や API 呼び出しをし、プレセンテーションコンポーネントでは props を受け取って UI を定義しています。
CANARY Cloud では、このパターンを使うことでプレゼンテーションコンポーネントでは依存関係が少なく、テストや Story の作成を簡単に行えています。
コンテナ・プレゼンテーションパターン|フロントエンドのデザインパターン
Storybook の導入
Storybook は、UI コンポーネントを独立して開発、テスト、文書化するためのツールです。インタラクティブなドキュメントなどを簡単に作成できます。
CANARY Cloud では Storybook を導入することで、コンポーネントごとに開発をしています。
課題感
現状の課題は以下のようなものがあります。
-
ロジックが肥大化したコンポーネントの存在
コンテナー・プレゼンテーションパターンを意識しすぎた結果、1 つのコンポーネントに多くのロジックを詰め込んでしまい、ファイルの行数が 2000 行を超えるコンポーネントが生まれてしまいました。ファイル行数が長すぎるため、コードの把握や修正に多くの時間を要する状態となっています。
この課題に対して、コンポーネントをより細かい単位に分割することで、管理をしやすくしていく予定です。
-
データフェッチのロジックが冗長
現状、データフェッチには TanStack Query のフックを利用しています。このおかげで、データフェッチとキャッシュ管理はシンプルに実現できています。ただ、isLoading や error などのデータで状態を処理する必要があるため、データフェッチごとに類似のロジックを繰り返し記述する冗長さがあります。
この課題を解決するため、Suspense への移行を考えています。ローディングやエラー処理を Suspense や ErrorBoundary に委ねることで、コンポーネント内のロジックをよりシンプルにします。
-
VRT の運用が止まっている
VRT を導入したのですが、以下の理由から運用が止まっていました。
- VRT の実行時間が長く、このテストのためレビューに出すまでの時間がかかる。
- そもそも Storybook でエラーが発生していたり、flaky な Story が存在していたりで、テストに対する信用がない。
これらの課題に対応するため、まずは Storybook の運用見直しから着手します。具体的な手段は未定ですが、Storybook をより積極的に運用することで VRT の再開を目指しています。
デザインライブラリ
デザインライブラリは主に以下の方針で管理しています。
Chakra UI の利用
Chakra UI は、CANARY Cloud のデザインシステムの基盤となっております。コンポーネントをそのまま利用したり、カスタマイズしたものをデザインライブラリとして他コンポーネントで利用したりしています。
Chakra UI の利点として以下のようなものがあります。
- 基本的なコンポーネントがあらかじめ実装されている
- リッチなアニメーションが組み込まれている
- アクセシビリティへの配慮がなされている
ディレクトリ単位でのカプセル化
デザインライブラリはディレクトリを用意して、アプリケーションから Chakra UI を直接呼び出すことを避けています。
Chakra UI をデザインライブラリとして隠蔽することで、プロダクトごとの汎用コンポーネントをデザインライブラリに追加できるようしています。
また、ディレクトリを用意することで、デザインライブラリの範囲を明確にし、アプリケーションの実装がデザインライブラリに入り込まないようにしています。
課題感
現状のデザインライブラリの課題感として、仲介業者様向け画面と顧客向け画面でデザインライブラリが共通化されていないことがあります。それぞれの画面ごとにコンポーネントを作成しているため、その分の工数がかかっています。
解決策として、フロントエンドのサービスをモノレポ化することでデザインライブラリをパッケージとして共有することを考えています。
リリースフロー
全体像
リリースフローは以下の様になっています。
リリースフローとしては以下のことを中心に考えています。
GitHub Flow での開発
CANARY Cloud では GitHub Flow を採用しています。
Dev ブランチや STG ブランチを設けず、master から直接機能開発用のブランチを作成しています。この方法で、機能開発環境と本番環境との差分を最小限に抑えています。結果として、コンフリクトを抑え、本番リリース前の確認でバグが発生するリスクを下げています。
また、PR の粒度にも注意を払っています。PR は機能ごとに作成し、1 つの PR が大きくなりすぎないようにしています。レビュアーの負担を軽減し、確認範囲を絞ることが目的です。
品質担保の負担軽減
ブランチにコミットをプッシュすると、GitHub Actions が自動的に CI を実行します。CI プロセスでは、ESLint と TypeScript による静的解析、および Jest による単体テストを行っています。
このおかげでレビュー前にも不具合が分かり、レビューイとレビュアーの両方の負担を減らせています。
また、これらの自動化を積極的に利用していることで、コーディング規約を必要最小限に抑えられています。
自動ビルド・デプロイ
コミット時の CI では、自動ビルドも実行しており、これにより以下のようなメリットを得ています。
- ビルドの失敗をすぐに検出できる。
- デプロイに必要なビルドが常に最新の状態で用意されている。
加えて、デプロイも GitHub Actions で自動化されています。ワークフローを実行するとデプロイが行われ、Slack への完了通知が届くようになっています。
このおかげで、新規メンバーでもすぐにデプロイを実行できています。さらに、障害発生時の切り戻し作業でも、焦ってデプロイ手順を間違えるリスクを軽減し、二次障害の発生を防いでいます。
課題感
リリースフローには主に 2 つの課題があります。
-
リリース前の動作確認に時間がかかる
主な課題の 1 つは、リリース前の動作確認に時間がかかることです。現在、本番環境へ 1 日 1 回リリースしていますが、リリース前には新規リリース分のコミットをまとめて確認する必要があります。この方法では確認項目が多くなり、結果として多くの時間がかかっています。
この課題の改善策として、PR ごとにリリースすることを検討しています。動作確認項目を減らすことで動作確認負担の軽減を考えています。
また、この方法では障害発生時の対応も簡単になることも見込んでいます。PR のリバートのみで切り戻しが可能となるため、一時対応がより迅速かつ簡単になると考えています。
-
リリース前の動作確認でデザインと実装の差分が大きい
CANARY Cloud では、新機能追加時に「デバッグ会」を実施しています。これは、デザイナーやプロダクトマネージャーを含むチームが、リリース前に機能を試用し、動作を確認する会です。
この会で実装とデザインの間に細かな差異が頻繁に発見されます。その結果、エンジニアの手戻りが発生したり、デザイナーが細部までデザインを確認する必要が生じ、チームの負担が大きくなっています。
そこで、リリースフローに UI レビューを組み込むことを検討しています。具体的な内容は未定ですが、たとえば PR 単位でデザイナーに UI レビューを依頼することを想定しています。これにより、リリース前の動作確認でのデザインと実装の差異をほぼ解消できると考えています。結果として、チーム全体の作業効率が向上し、負担が軽減されることを見込んでいます。
おわりに
CANARY Cloud の Web についての技術スタックをまとめた記事でした。
リリースから期間も経っているので、ライブラリの見直しや Update、開発環境の見直しも行っています!
「より詳細に話を聞きたい」「自分もこの開発に興味がある」と感じて頂けましたら、カジュアル面談などでお気軽にコンタクトをとっていただければと思います!
Discussion