1 年越しくらいに Vue/Nuxt のアプリをリファクタリングした話
Leaner Technologies で業務委託している西辻です。
去年のクリスマスに娘( 1 歳 3 ヶ月)へ子どもが足で蹴って進む車をプレゼントしたのですが、全然乗らないまま年を越し、 滑り台を買ってあげたらめっちゃ滑るんですよね。
前置きはこのくらいにして、今回の記事では 1 年越しくらいに Vue/Nuxt のアプリをリファクタリングした話を書いていきます。
モチベーション
昨年の秋頃から既存機能に割と大きめの改修を入れる話がでてきており、既存コンポーネント群にそのまま修正を加えると、分岐のパターンが増えて大きめの負債になる雰囲気がありました。
そこで、チーム内でも話して機能追加前に大きめのリファクタリングを入れていこうという合意形成できたのでやっていくことになりました。
現状のコンポーネントの課題
対象のコンポーネントがある環境は Nuxt + Vuetify 上で構築されており、基本 Options API(Vue.extend()
) + pug template + scss で SFC を書いている状態です。
リファクタリングしていくという言葉だけだと抽象度が高いので、まずは現状のコンポーネントの課題が何かを列挙してみることにしました。
どんなコンポーネントかというと Stepper で構成されており、以下の項目数が入力できる割とデカめなコンポーネントです。
- テキストフィールド: 16 (ネストした可変フォームも含む)
- モーダル: 5
- ファイル添付フォーム: 2
コンポーネントの構成としては以下のように Step に依存した名前をつけてました。 名前は一例で、こんな感じになっているくらいをイメージしやすいようにつけています。
- Form.vue
- Step1.vue
- Step1SomeInput.vue
- Step2.vue
- Step2SomeInput.vue
- Step3.vue
- Step3SomeInput.vue
- Step4.vue
- Step4SomeInput.vue
- Step1.vue
つらみを列挙してますが、全て自分の所業なので 1 年前の自分へ向けたメッセージです。
- 表示と編集が同一のコンポーネント内でフラグを切り替える形で実現されている。
- コンポーネントをみる時に参照と入力どちらも考えないといけないので機能追加するたびにメンテコストが上がっていく状態。
- 言い訳としては、当時はその方が実装コストを抑えられるため共通化したんですが、状況は変わるものですね、柔軟に対応していかねば。
- emit パターンで実装されているところが、コードを追いづらい。
- コードジャンプで追えないので、最初に書いた自分でもわからない状態になりつつあった。
- 親コンポーネント(上記でいうところの Form.vue )に Form のデータを更新するロジックが集中してしまっていた。
- Nuxt の automatically load components 機能を使っている。
- 当時は import を書かなくて便利、で使ってたんですがコンポーネントの数が増えてくると参照元を追う手段が grep になるので大変。
- pug template で書いてる部分に対して TypeScript による型チェックが効いてない。
- 簡単な typo も動かしてみないとわからない状態。
- StepX.vue で名前つけているコンポーネントが変更に弱い。
- リファクタリング前にすでに顕在化していたのですが、Step の中身を変えた時にコンポーネントも変えないといけなくなるので面倒。
リファクタリングの方針
上記で挙げた課題を解決できるようにリファクタリングしていきます。
表示と編集が同一のコンポーネント内でフラグを切り替える形で実現されている。
これは単純にコンポーネントを分けました。
ただ、参照と入力をそれぞれのコンポーネントに分けるのは骨が折れそうだったので、今回機能追加のメインとなる入力だけを新規コンポーネントとして切り出す方針にしました。
この辺りは、リファクタリングにかけられるリソースにもよるので気持ち的に一気にやりたさはありますが、バランスを取るのも大事ですね。
emit パターンで実装されているところが、コードを追いづらい。
自分が当初作った時は emit パターンしか知らなかったのですが、後に props で function を渡すパターンを教えてもらったので、 今回のリファクタリングの対象コンポーネントが大きめなことも踏まえて積極的に props で function を渡す方針で修正していくことにしました。
Nuxt の automatically load components 機能を使っている。
ディレクトリ作って automatically load components の機能は基本使わないようにしました。
import を明示的に書くのは面倒だなと思っていましたが、コンポーネントが大きくなると参照箇所を辿れないことの方がクリティカルな問題になるので今後自分がこの機能を使うことはなさそうです。
pug template で書いてる部分に対して TypeScript による型チェックが効いてない。
ちょっと前に現在のプロジェクトに tsx が導入されてテンプレート内でも型チェック入るようになり、これはいいなと思い今回のリファクタリングでも積極的に tsx に書き換えていくことにしました。
また、 props のチェックも入るようになったので required な props を渡し忘れたり、渡す型を間違えてることにも気づけるようになったので開発体験が向上しました。
StepX.vue で名前つけているコンポーネントが変更に弱い。
書いてる通りで、 この辺はリネームしてリファクタリングした。UI コンポーネントに依存する名前をつけちゃダメだぞ(自分へ)
リファクタリングを進めていく中で
pug -> tsx 化は思ったよりコスト高かった
これは自分の見積が甘かったのですが、以下の点で思ったよりスピード出ないなとなりました。
- pug から html への変換が結構大変で、
v-
ディレクティブ使ってるところなんかも修正していかないといけないため思ったより変換コストが高い。 - 合わせて Composition API 化もやっていたが、そもそも Options API -> Composition API の変換コストもそれなりにあった。
- 自分が tsx のシンタックスに慣れていない。(慣れてきたらそれなりのスピードになったが、イニシャルコストは高めであった)
リファクタリングした後の追加機能もそれなりにスピード出して開発していかないといけない状況であったので、全てを tsx 化するのは一旦やめて、 pug + Composition API でリファクタリングしたコンポーネントも許容することにしました。
今回のリファクタリングが終わった後に部分的に tsx に移行可能なのでなんなら最初にまとめて pug + Composition API で一気にやった後に tsx 化をしていく方針も選択肢としてはありましたね。
tsx 化したところの型チェックありがたい
上でコスト高いと書いてはいるのですが、それを持ってしても有り余る利点がありました。
- template 内で型チェックが効く。
- props の不足、型チェックが効く。
- 子コンポーネントが Composition API になっている必要があるが、 IDE 上でチェックできるようになったので簡単なミスを防げるようになった。
- template に prettier のフォーマッタが完全に当たる。
- pug でもフォーマットできるライブラリはあったが、素の html になったことで prettier の恩恵を最大限受けられる形になった。
既存コンポーネントを全て tsx にする必要はない
できなかったら終わってなかったんですが、 Single File Component で作っていれば基本 emit などでの連携は取れるので、大きめの子コンポーネントのインターフェイスを変えずにリファクタリングできたのは良かったです。
新規機能を取り込みながらすすめる必要あり
なんか久しぶりに大きめのリファクタリングする機会があったので、他の新機能とコンフリクトすることをすっかり忘れてました。
コンフリクトするだけよく手の入る機能はリファクタリングする価値が高いのでやりきらねばと強い気持ちが持てました。
とはいえ、リファクタリングの最後らへんで、いよいよリファクタリングした新規コンポーネント群に差し替えるって時にはちょっと機能開発止めてもらえるようにアナウンスは必要ですね。
まとめ
リファクタリングってよく「そのコスト今払うの?」ってなりがちなのですが、その辺はセールスチーム含めてスムーズにコミュニケーションをとってやれるとなったのでありがたい環境だなと思いました。
また、チーム内でも tsx に知見のある方にサポートしてもらったりして一人では終わらなかった部分を手助けしてもらえたのも大きいです。
リファクタリング後は、無事大きめの改修が入れられたので、やって良かったなという感想しかないです。
やり残した部分もあるので引き続きやっていきたいですね。
宣伝
Leaner Technologies ではリファクタリングしつつ新機能をどんどん出していける環境に興味のあるエンジニアを募集しています!
Discussion