BFF層を作ったらFEの負荷を減らせるようになった話。
この投稿はmediba Advent Calendar2021の23日目の記事です。
早速ですがみなさま、APIのレスポンスからViewを構築していくのが主流なこの時代のフロントエンド開発において、クライアント側のコードが複雑になり辛い思いをした経験がありませんか?私はあります。
例を見ていきましょう。
まずmicroCMSでBE層(データ+API)を作成します。
![スクリーンショット 2021-12-19 20.35.20](https://assets.st-note.com/production/uploads/images/68085894/picture_pc_cbc8c87d4db99cd0770d6cefbaca7b80.png?width=1200)
microCMSのコンテンツ内容
- コンテンツ(/v1/contents): コンテンツ一覧
- おすすめ(/v1/recommend): コンテンツ一覧から一部を取得
- ピックアップ(/v1/pickup): コンテンツ一覧から一部を取得
- スタータス(/v1/status): ユーザーのステータス種別一覧
次にmicrocmsAPIからコンテンツをfetchし描画します。
下記のイメージです。
![スクリーンショット 2021-12-21 23.00.44](https://assets.st-note.com/production/uploads/images/68228242/picture_pc_8c1a6957300e1055daebf6b3376e82be.png?width=1200)
コンテンツ描画条件
-コンテンツのスキーマの中に、対象ユーザーのステータスID配列が含まれている
1. QueryStringからステータスを取得
2. /v1/statusを取得し、該当のステータスIDを取得
3. /v1/recommendでおすすめ、v1/pickupでピックアップコンテンツを取得
4. おすすめとピックアップ両方をステータスIDで出し分け
5. 要素数が多い方を表示
const { query, isReady } = useRouter()
const statusName = query?.memberStatus
const [status, setStatus] = useState<Status>(blankStatus)
const [topContents, setTopContents] = useState<ContentsList[]>([])
// ステータスに該当するコンテンツを取得
const filterStatusContents = useCallback((contents: ContentsList[], status: Status) =>
contents.filter(contentsItem => contentsItem.contents.status.some(listStatus => listStatus.id === status.id)),[])
// queryのステータスからステータスを取得
const findStatus = useCallback((statusList: Status[]) => {
const selected = statusList.find(status => statusName === status.name)
selected && setStatus(selected)
return selected
}, [statusName])
// fetchとsetState
const homeInitialize = useCallback(async() => {
const headers = { 'X-MICROCMS-API-KEY': apiKey }
const endpoints = ['status', 'recommend', 'pickup']
const fetchData = endpoints.map(endpoint =>
axios.get<MicroCmsItem<Status[] | ContentsList[]>>(`https://xxx.microcms.io/api/v1/${endpoint}`, { headers }))
const [statusResponse, recommendResponse, pickupResponse] = await Promise.all(fetchData)
const statusList = statusResponse.data.contents as Status[]
const status = findStatus(statusList)
// ステータスが存在しない場合後続の処理を実施しない
if (!status) return
const topContents = ([recommendResponse.data.contents, pickupResponse.data.contents] as ContentsList[][])
.map(contents => filterStatusContents(contents, status))
.sort((a, b) => b.length - a.length)[0]
setTopContents(topContents)
}, [findStatus, filterStatusContents])
useEffect(() => {
if (!isReady && statusName) return
homeInitialize()
}, [isReady, homeInitialize, statusName])
APIで返却されるものとViewの構築に必要なもので大きく差分があり、雑に書いてこれだけの処理がクライアントで必要になってきます。
運用していくにあたっても改修時の担当領域が明確になり辛く、利用するAPIが増えた場合はその都度クライアントから呼び出す必要があります。
スピード感のあるリリースを目指す際には足枷になりそうです。
そこでBFF層を作成してみました。下記のイメージです。
※ フレームワークの説明は割愛させていただいてます。
![スクリーンショット 2021-12-21 23.10.40](https://assets.st-note.com/production/uploads/images/68229275/picture_pc_0099bd4fa991d6b3cd746fa11fdfc02d.png?width=1200)
これによりView構築の為のビジネスロジックをBFFに一任することができ、クライアントで発生する処理をよりシンプルにすることができます。
const homeInitialize = useCallback(async() => {
const fetchData = await axios.get<TopContents>('https://xxx/bff/v1/topContentes', { params: { status }})
setStatus(fetchData.data.status)
setTopContents(fetchData.data.contents)
}, [status])
先程記載したhomeInitializeはfetchしてsetするだけになりました。
運用に関しても下記のように役割が明確でシンプルになり、保守性の向上に繋がりそうです。
各領域の役割
- Frontend: BFFのレスポンスを使用してのView構築
- BFF: View構築の為のビジネスロジック/APIのaggregation
- Backend API: 再利用性のあるAPIの作成
まとめ
上記のようなBackend APIを複数使用しての出し分けの要件が多いサービスにおいては、BFF層を積極的に導入していく価値はありそうに感じました。
また、今回のように世に出ている仕組みや技術を継続的に自分なりに咀嚼して飲み込んで、携わるサービスに落とし込んでいける人間でありたいと思いながら2022年も頑張ります。
この記事が気に入ったらサポートをしてみませんか?