✍🏻

Webフロントエンドを長期運用するために

2024/12/16に公開

この記事はMICIN Advent Calendar 2024の 16 日目の記事です。

https://adventar.org/calendars/10022

前回はMYさんのBigQuery と Gemini で手軽に画像から情報抽出でした。

https://zenn.dev/micin/articles/fed57b881ee91f


はじめに

フロントエンド開発の現場では技術やツールの進化が非常に速いです。トレンドに追随して導入したつもりなのに気づけば陳腐化し、技術的負債になっていることも少なくありません。

新しいツールの導入と同時に、既存のコードベースを健全に保ちながら長期的に運用するためのメンテナンスは欠かせません。これを怠ると、技術的負債やセキュリティ上のリスクが積み重なるだけでなく、チームの生産性やプロダクトの健全性にも悪影響を及ぼします。

この記事では私自身が長く携わっているプロダクトの開発経験を踏まえ、フロントエンドの健全な状態を保ちながらプロダクトを発展させるためのアプローチを考えてみます。

失敗と苦労

この記事で紹介する内容はうまくいっている事例の紹介というよりはむしろ、失敗や苦労から生まれた反省によるところが大きいです。うまくいかなかった、あるいは失敗した具体的な事例を紹介します。

増えすぎたライブラリ

外部ライブラリは開発を効率化する一方で、増えすぎると管理コストとセキュリティリスクが大きくなります。私たちのプロジェクトでは以下の問題が発生しました。

不要ライブラリの放置

使用箇所がほとんどないライブラリや、使われなくなったStorybook用のライブラリがそのまま残り、脆弱性アラートを引き起こしました。

役割が重複するライブラリ

プロジェクトの途中でReduxからContextとJotaiへの移行、styled-componentsからChakra UIへの切り替えといったイベントが発生しました。

新しいツールのメリットを享受するために変更を進められたこと自体には満足していますが、結果として同じ役割(状態管理やUIの実装)のライブラリが共存する状況が生まれました。

残されたライブラリの整理やメンテナンスに関する考慮は少し不足していたように感じています。

重すぎたメジャーアップデート

破壊的変更を伴うライブラリのメジャーアップデートも課題でした。特にreact-router v5からv6へのアップデートはルーティング定義が複雑だったこともあり非常に骨の折れる作業でした。

[参考] v5からv6へのマイグレーションガイド

https://reactrouter.com/6.28.0/upgrading/v5

Chakra UIのメジャーアップデートも大変でした。アプリケーションコードがChakra UIに大きく依存していたため、propsの名称変更や予期せぬビルドエラーの解消に労力を要しました。

新旧方針の混在

役割の重複するライブラリの話と被る部分もありますが、途中で大きな方針の変更や刷新を進めると新旧の状態が併存する状態が生まれます。

例えば、途中でディレクトリ構成をアップデートした結果新しい方針と古い方針がコードベースに混在する状況が生まれました。変更せず残した部分に関してはいずれ削除して置き換えられる想定でいましたが、いまだに完全移行ができていない状況です。

元からいたメンバーにはコンテクストが共有されていましたが、新たなメンバーが参画した際に「どこに実装すれば良いのか」という混乱を招く原因となってしまいました。

対策

ここからは失敗を踏まえて導入した対策、あるいは取るべきだった対策を紹介します。

仕組みでカバーする

ライブラリのアップデートや不要なパッケージの削除を手作業で行うのは大変です。そこで、以下のような仕組みを取り入れることで、日常的に対応できる環境を整えました。

定期的なライブラリアップデートを習慣化

私たちのチームでは2週間に一度リリースをしており、リリースに合わせてライブラリのアップデートを行うようにしています。

Jiraで自動的にライブラリアップデートのチケットが作成され、定期的(2週ごと)に作成されるDependabotのPRをエンジニアがマージすることで完了としています。

Dependabotの設定も工夫しており、group機能を使うことでpatchバージョンとminorバージョンは同時にアップデートできるようにしています。フロントエンドは依存パッケージが多いのでこの仕組みにより常にアップデートに追随しつつ古いバージョンを放置することによるセキュリティリスクも低減できています。

https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups
dependabotにより作成されたPR
dependabotにより作成されたPR

Knipでdead codeを削除

https://knip.dev/
KnipはTypeScript/JavaScriptの不要コードやファイルを削除してくれるCLIツールです。package.json の中身もチェックして、未使用のパッケージがあればこれも削除してくれます。

Knipを導入してCIにも組み込むことでリファクタリングや変更に伴って不要になったコードやパッケージをマージすることを防げます。不要ライブラリの放置という問題は根本的に発生しない状況が作れています。

CIでDead Codeのチェック
Dead Codeの有無をCIでチェック

ライブラリが規定する通りに実装する

Reactをはじめとする主要ライブラリは、公式のベストプラクティスや推奨の実装を提供していることが多いです。これを無視して独自の方法を取り入れると、アップデートや修正時に余計な手間が発生します。

公式ドキュメントに則り、ライブラリが期待する実装をすることが結果的にメンテナンスのしやすさにつながります。

例えば、Reactが推奨しないuseEffectの使い方をするとメジャーアップデートのタイミングで期待通りに動作しなくなる可能性があります。常に期待通りの実装をしておくことでアップデートにも追随しやすくなります。
https://react.dev/learn/you-might-not-need-an-effect

言語やフレームワーク標準の機能やAPIを使う

外部ライブラリを使わずとも、言語レベルの機能やフレームが実装するAPIで賄えるケースは少なくありません。

例えば、Lodashには便利な関数が多数ありますが実際にプロジェクト内で使用しているケースを見ると置き換えが可能そうでした。すなわち、TypeScriptの型やECMA Scriptの標準メソッド、ReactのAPIのみで賄えるケースが多そうでした。

import _, { Dictionary } from "lodash";

// ❌
const someRecord: Dictionary<SomeType> = {};
// 👍 TypeScriptのRecord型でOK
const someRecord: Record<string, SomeType> = {};

// ❌
const id = _.uniqueId();
// 👍 ReactのuseIdが使える
const id = useId();

外部ライブラリを使う際は適度にラッパーを使う

react-routerのアップデートに際して強く感じたことですが、外部ライブラリを使う際にラッパーを設けて依存が限定的にすることで大きな変更の影響を最小限にできます。

いわゆる、腐敗防止層という考え方です。

私たちのプロダクトではreact-routerが提供するuseHistoryやuseLocationなどのAPIが各所で直接importされており変更が大変になった一因でした。

もしラッパーを設けて間接的にreact-routerのAPIを使う形にしていれば変更は格段に容易でした。

簡単な例ですが、以下のような自前で定義したuseNavigationを利用する形にしていれば中身の実装を更新するだけでメジャーアップデートの対応が完了していました。

router-wrapper.ts
- import { useHistory } from 'react-router-dom' // for v5
+ import { useNavigate } from 'react-router-dom' // for v6

export const useNavigation = () => {
-  const history = useHistory()
+  const navigate = useNavigate()
  return (to: string) => {
-    history.push(to)
+    navigate(to)
  }
}
必ずラッパーを設けた方が良いか

routerに関してはこの方針をとっていれば確実にメンテナンスが楽になっていたでしょう。

しかし、同じくメジャーアップデートの対応に苦慮したChakra UIの場合について考えてみると無条件でラッパーを設けるのが正しいとも思いません。

Chakra UIもアプリケーション全体で直接コンポーネントをimportして使用していました。自前のラッパーを設けていれば変更範囲は限定できたでしょう。しかし、ラッパーの実装にもコストがかかります。BoxやFlexなどのレイアウト系コンポーネントも含めてUIはChakra UIに大きく依存しているので直接使う方がメンテナンスしやすく、ラッパーを設ける方が却ってコストが増してしまいます。

このようにケースバイケースにはなると思いますが、自前のラッパーを設けることは長期間プロダクトを運用していく上では極めて有効な手段となりうるでしょう。

ドキュメントを書く

少ないメンバーで開発していたことやフロントエンドを見るエンジニアがほぼ自分一人しかいないようなタイミングもあったこともあり、ドキュメントの整備が疎かになっていました。

あまりフロントエンドを触らないメンバーや新しいメンバーが実装する際に困る時はドキュメントが不足していることが多いです。

大きな方針変更や刷新をする際は、どういった背景でその変更をしたのか、どういった意図で変更を加えたのかドキュメントにまとまっていると後から参画したメンバーはもちろん自分自身を救うことにもなります。

PRのdescriptionや簡単なメモ程度は残っていても、後から見返すと内容が不十分なことが多々あります。後から書こうとしても正確性に劣ってしまうこともあるため、実際に変更を加えるタイミングで背景や意図をドキュメントに残すことが重要だと感じています。

ADR(Architecture Decision Record)は、こうした記録を残すのに最適なフォーマットです。こうしたプラクティスも取り入れながら、意思決定の際にドキュメントを残しておくようにしたいです。
https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions

まとめ

仕組みでカバーする、外部ライブラリを慎重に扱う、ドキュメントをしっかり書く。改めて見ると基本的なことばかりに思えます。それでもなかなか徹底できていないもので、反省が多いです。

これからも開発は続いていきます。フロントエンドを取り巻く環境も進化していくことでしょう。変化に追随しながらも左右され過ぎずに健全なプロダクトを維持し続けていけるよう、工夫していきたいです。


MICINではメンバーを大募集しています。
「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!
https://recruit.micin.jp/

株式会社MICIN

Discussion