【Next.js】新規プロダクトのフロントエンドにおけるディレクトリ構成 - 通信レイヤー編 | Offers Tech Blog
概要
こんにちは、Offers を運営している株式会社 overflow でフロントエンドのテックリードをしている Kazuya です。今回は、筆者が担当しているプロダクト「Offers MGR(オファーズマネージャー) 」で採用しているディレクトリ構成の一部について書かせていただきます。
後述しますが、「Offers MGR」では求められる要件が複雑且つ通信で取得する情報量が膨大であることからAPI関連のディレクトリ構成もやや特殊なものになっています。ベースは以前こちらの記事で紹介した「Viewsレイヤー」を拡張させる形になっています。
専用構成になっている感があるため、参考になるかは分かりませんが、ぜひ最後まで読んでいただけると幸いです。
Offersのディレクトリ構成はこちら
弊社フロントエンドのボスであるAhomu先生が担当されているOffers側のディレクトリ構成は以下の記事をご参照ください。「メディアなどでSEO重視したい!」という方にはおすすめです。
[AD] Offers MGR(オファーズマネージャー)
本記事で紹介する方法は、筆者が担当しているプロダクトである「Offers MGR(オファーズマネージャー) 」で活用されています。「Four Keys分析」や「サイクルタイム分析」など開発組織の生産性を最大化するために必要となる指標を可視化させることができます。開発組織の健全性・生産性を中長期的に改善していきたい方はぜひお問い合わせください!
はじめに
前述した通り、本記事では弊社の新規プロダクト「Offers MGR」で採用しているディレクトリ構成の一部を紹介します。チームメンバーのスキルアセット、要件定義など様々な要因で本記事で紹介する内容とマッチしない場合があります。一例であることをご理解の上、参考にしていただけると幸いです。
前提
まず、「Offers MGR」の開発における前提条件についてまとめておきます。基本的な技術スタックについては以下の通りです。
技術スタック
- Framework: Next.js(Page Router)
- API: GraphQL, REST(一部)
- Fetch: Axios, SWR, Apollo
サポート終了が年末に迫っているVue(2系)を採用していたNuxtから、Next.jsへのリプレイスを2023年3月に実施し、上記のような技術スタックになっています。RESTに関してはOpenAPIによるドキュメント化がされていますが、社内の技術戦略によりGraphQLへの1本化が現在進行しています。
サービスの性質
「Offers MGR」は、GitHubやSlack、Notionなど開発組織において採用されているケースの多いツールのデータを集積/可視化させる分析ツールです。そのため、取り扱うデータ量が膨大であり、相対してAPIのリクエストにかかる時間も長くなってしまうケースがあります。また、新規プロダクトのため機能追加/削除が高頻度で行われることから、コンポーネントの付け外しが容易にできる仕組みづくりも必要になっています。
APIに関する補足
RESTに関してはOpenAPIによるドキュメント化がされていますが、社内の技術戦略によりGraphQLへの1本化が現在進行しています。「Offers MGR」では現時点で全体の80%がGraphQL化されており、新規開発分に関してはすべてGraphQLで実装されています。
Fetchに関する補足
RESTはOpenAPIで運用されているため、こちらの記事でも紹介されているコードジェネレーターでAxiosの通信メソッドを生成しています。それをSWRでラップして取得する方式を採用しています。
GraphQLも同じくコードジェネレーターを採用しており、Apolloのメソッドを自動生成させています。
前提のまとめ
長くなりましたが、前提に関して少し簡単にまとめさせていただきます。
- サービスの性質上、通信量が非常に多い
- 新規プロダクトで機能追加/削除が高頻度行われる
- APIはRESTとGraphQLの2つが混在しているがGraphQLがメイン
- データ取得のメソッドはコードジェネレーターで自動生成している
上記に加えて、権限によるコンポーネントの表示制御や特定企業に対しての部分的なリリースなど様々な要件を満たす必要があり、それに合わせたディレクトリ構成にチューニングする必要がありました。
ディレクトリ構成
前置きが長くなりましたが、本題に入っていきます。まずは兎にも角にも構成を見てもらうほうが早いと思いますので、以下をご覧ください。
L layers/
L Layer
L view/ (第1階層:表示することを責務とする層)
L request/ (第2階層:データ取得/受け渡しを責務とする層)
L process/ (第3階層:権限など表示制御を責務とする層)
L index.ts
各層の説明
上記のように1レイヤー毎に3つの層が存在しており、それぞれに1つの責務を持たせてあります。これは筆者がコードを書く上で重要視している「単一責任の原則」に基づいたものになっています。
View層
Viewは最も深い層で「表示することを責務とする層」になります。この層はコンポーネントの描写のみを責務としているため、通信メソッドなどは存在しません。簡単に表現すると「テレビ本体」のようなイメージで、見た目だけで番組などはまだ表示されない状態です。
弊社におけるView層のファイル構成
表示することを責務とする層で通信等がない世界線のため、StoryBookもモック無しで表示させることができています。また、使わないケースも多々ありますが、軽微なスタイル調整をすることもあるので、SCSSファイルも配置しています。
L view/
L LayerName.tsx (本体)
L LayerName.stories.tsx (StoryBookファイル)
L style.module.scss (SCSS)
L index.ts (Indexファイル)
Request層
RequestはViewの上である第2階層で「データ取得/受け渡しを責務とする層」になります。この層では、API経由でデータを取得して、取得したデータをViewに渡します。権限による表示制御など、より上位における制御はこの層では行わず、あくまでそのレイヤーを表示させるために必要な情報を取得して渡すだけに責務をとどめています。簡単に表現すると「テレビに番組情報を流すアンテナ」のようなイメージで、ようやくこれでテレビで番組を見ることができます。
弊社におけるRequest層のファイル構成
この層では本体である「LayerName.tsx」内から該当のView層を呼び出しています。カスタムHooksなどでデータを取得してView層にPropsで渡してあげることでコンポーネントが描写されます。(本体である「LayerName.tsx」内に取得のロジックを直接書いても問題はありません)
L request/
L LayerName.tsx (本体)
L useFetch.tsx (データ取得のカスタムHooks)
L index.ts (Indexファイル)
Process層
Processは最も浅い第3階層で「権限など表示制御を責務とする層」になります。この層では、該当レイヤーが表示対象であるかを精査して制御することを責務としています。最も簡単な例は権限ですが、それ以外にも特定企業のみ公開など様々な条件があります。簡単に表現すると「テレビ/アンテナの電源」のようなイメージで、電源が入らなければ、テレビが映ることはありません。
弊社におけるProcess層のファイル構成
この層では本体である「LayerName.tsx」内から該当のRequest層を呼び出しています。権限などの表示制御を「LayerName.tsx」で行っています。
L process/
L LayerName.tsx (本体)
L index.ts (Indexファイル)
なぜ上記の構成にしたか
なぜ上記の構成にしたかですが、開発的な要因とプロダクト的な要因の2つがあります。
開発的な要因
まず開発的な要因ですが、それは「エンジニアチーム全体でAPI結合をしやすくするため」です。現在のチームはフロントエンドエンジニアが2人、バックエンドエンジニアが4人とリソースに差があり、フロントエンド側がリソース的に逼迫するケースがあります。そこでAPI結合をフロントエンドエンジニア以外でもできる仕組みを作ろうと考えました。ただ、バックエンドチームがそこまでフロントエンドに関して慣れていないということもあったため、できるだけシンプルで見るべきディレクトリ/ファイルを限定化させる方向で調整し、その結果、Request層に変更をいれるだけでAPI結合ができる構成になりました。
プロダクト的な要因
プロダクト的な要因は、主に「表示制御」と「機能追加/削除の容易さ」の2点でした。まず表示制御ですが、サービス内にはログインユーザー毎に権限があり、それに応じて表示できる内容が異なるという仕様が存在しています。それ以外にもFeatureToggleと呼ばれる機能で特定企業やユーザーのみ限定で機能を公開するというものも存在しており、表示制御ロジックが複雑化する未来が見えていました。そこで表示制御に関して責務を持つ層を独立させることで、コードの可読性を高めるとともに運用しやすい構成になりました。
2つ目の「機能追加/削除の容易さ」ですが、これは1レイヤーの粒度に関わってくる話になるので、後述で詳しく解説します。
1レイヤーあたりの粒度
1レイヤーあたりの粒度ですが、Offers MGRでは1ページ内における1セクションという単位で配置しています。以下の画像だと、「主要アクティビティの推移」と「アウトプットの推移」のそれぞれで1レイヤーということになります。この粒度にしたのは、前述の「機能追加/削除の容易さ」の要件を満たすためであり、リプレイス前の環境で特定の機能の引き剥がしに時間がかかっていたため、その課題を解消するために採用しました。これにより、「アウトプットの推移」を別ページに移動してほしいなどの要望に対して迅速な対応が可能になり、開発生産性の担保に一役買っています。
上記の画像におけるディレクトリ構成
L layers/
L 主要アクティビティの推移
L view
L request
L process
L アウトプットの推移
L view
L request
L process
よくある質問(FAQ)
Layerはどのように呼び出しますか?
各Layerは、Next.jsのPageファイル内から直接呼び出しています。Pageファイル内には、レイヤーとMetaタグ以外はない状態で、非常にシンプルな内容になっています。一部データをSSRで取得させたい場合は、LayerにPropsを定義してバケツリレーすれば実装自体は可能です。
データのフォーマットなどはどうしていますか?
個人的な誓いで、コンポーネント内でコードジェネレーターで作成された型はできるだけ使わないを徹底しているので、取得したデータのフォーマットをする必要があります。現在は、別でヘルパーのようなメソッド(弊社における開発名称:ゴミ箱)を作ってフォーマットさせていますが、本来であればプレゼンテーション層のようなものを用意してあげたほうがいいかもしれません。(バケツリレーがうっ頭が...
レイヤー同士で同じエンドポイントを使う場合オーバーフェッチになりませんか?
結論なります。弊社の場合は許容としてしまっていますが、気になるようであれば、レイヤーの粒度を少し上げるなどしてもらえれば、解消は可能だと思います。また、昨今のFetchにはキャッシュが考慮されていることが大半なので、ある程度はそれでカバーできる問題かなと思っています。
まとめ
かなりの特殊事例で参考になったか怪しいところではありますが、以上が自分が担当している「Offers MGR」におけるディレクトリ構成の一部でした。開発的要因、プロダクト的要因の課題を解消するために色々考えた結果、前例のない構成になってしまいましたが、結果的に1ヶ月で平均2つ以上の新機能提供を行っている状態になっています。そのままの状態では活用し辛いかと思いますが、少しでも参考になれば幸いです。
質問等あれば、ぜひコメントいただければと思います。少々長くなりましたが、本記事を最後まで読んで頂き、ありがとうございました。
関連記事
副業転職の Offers 開発チームがお送りするテックブログです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 jobs.overflow.co.jp
Discussion