こんにちは。Restaurant Service Devグループの高岡です。現在ぐるなびウエディングのフロントエンド開発・運用を行っています。
ぐるなびウエディングは結婚式場検索・予約サービスで、検索・会場詳細・特集・ランキングなど多様な機能を持つ大規模なウェブアプリケーションです。
そのぐるなびウエディングはリニューアルプロジェクトが進行しており、今年の7月に二次会検索と会場ページをリリースしました。
今回は大規模なウェブサービスのリニューアルにおいてチーム開発の効率性と保守性を両立するフロントエンドアーキテクチャをどう設計するか、ぐるなびウエディングリニューアルプロジェクトで実践した取り組みについて紹介します。
二次会検索と会場ページをリリースするまで、以下の体制で開発を進めていました。
この高速開発環境で品質を保ちながら効率的に開発するため、以下の設計方針でアーキテクチャを構築しました。
実績と移植性を重視し、以下の技術スタックを採用しました。
高速開発と保守性を両立するため、以下の3つを軸にアーキテクチャを設計しました。
同様のリニューアル事例で社内の共通ナレッジとして共有されているレストラン検索では Atomic Design を採用して開発していました。しかし、レストラン検索では開発していく中で以下の課題が挙げられていました。
この問題を解消するため、Atomic Design ではなく Package by Feature の仕組みを採用することにしました。
bulletproof-reactでも採用されているベストプラクティスで、以下の利点があります。
src/├── app/ # Next.js App Router├── features/ # 機能別ディレクトリ│ ├── search/ # 検索機能│ ├── venue/ # 会場詳細機能 │ ├── special/ # 特集機能│ └── ranking/ # ランキング機能└── components/ # 共通コンポーネント
ただ、一般的な Package by Feature では features にすべての機能が features 直下に並びますが、それだと管理が難しいため大分類(search/venue/special/ranking)を設けることで数多くの機能を整理しています。
Next.js v13.4.0以降、App RouterとReact Server Components(RSC)が本格利用可能になりました。Page Router がメンテナンスモードとなり、今後のアップデート追従を考えると App Router 移行は必須の選択でした。
ただし、App RouterとRSCを使った本格的な開発実績は少なく、Server Component と Client Component の使い分けに関する調査と認識合わせを徹底的に行いました。
実装・調査を通じて発見した重要な注意点をご紹介します。
よくある誤解:Client Component はブラウザ上でのみレンダリング実行される
実際:Client Component もサーバーサイドでレンダリング(SSR)される
そのため、SEO対策を理由にServer Componentにする必要はありません。
Client Component に import されたコンポーネントは、自動的に Client Component となります。
// ❌ 問題のあるパターン'use client'import ServerOnlyComponentfrom'./ServerOnlyComponent'// これもClient Componentになってしまう// ✅ 推奨パターン'use client'import{ ReactNode}from'react'functionClientComponent({ children }:{children:ReactNode}){return <div>{children}</div>}// Server ComponentをpropsとしてUIツリーに組み込む
ただし、propsとして渡すことは可能です。これを活用してモーダルの実装などに応用できます。
// モーダルの例functionModal({ children, isOpen, onClose }:ModalProps){// モーダルの表示制御はClient Componentreturn isOpen ? <div>{children}</div> :null}// Server Componentの内容をchildrenとして渡す<Modal isOpen={isOpen} onClose={handleClose}> <ServerSideContent />{/* Server Componentのまま */}</Modal>
Container/Presentational パターンは、React初期にFlux全盛期にDan Abramov氏によって提唱された設計手法でした。
従来の責務分割:
しかし、RSCではサーバー処理とクライアント処理が共存できないため、従来のパターンでは以下の問題が発生します。
外部の Container/Presentational Pattern に関する記事を参考に、RSC に適応した Container/Presentational パターンを採用しました。
新しい責務分割:
features/search/├── components/│ ├── result/ # 検索結果表示機能│ │ ├── result-container.tsx # Container Components (データフェッチ)│ │ └── result.tsx # Presentational Components (UI制御)│ └── condition/ # 検索条件機能├── hooks/ # カスタムフック├── types/ # 型定義├── stores/ # グローバル状態管理└── utils/ # 機能に限定したユーティリティ関数
この構成により、以下のメリットを実現できました。
検索・会場詳細・特集など複数機能で利用するドメイン知識を持つコンポーネントや関数の配置場所に悩みました。大分類で整理している関係上、どこに置くかの判断が難しいケースがあります。
この対応として、コンポーネントを細分化してドメイン知識を持たない粒度にしてから共通コンポーネントに配置したり、複数サービスで利用するコンポーネントや関数は共通パッケージ化しています。
高速開発により1日当たりのPR数が多く、レビュー負荷が大きくなる傾向があります。ただし、影響範囲が明確なのでレビューはしやすい印象です。
ぐるなびウエディングリニューアルプロジェクトでは、高速開発と保守性を両立するため、以下のアーキテクチャを構築しました。
この設計により、少人数のフロントエンドチームで毎週大量の PR を作成できる効率の高いフロントエンドのアーキテクチャを実現できました。
今後も技術の進歩に合わせてアーキテクチャを進化させ、より良い開発体験を追求していきます。
