MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

約10万行の規模のフロントエンド環境にTypeScriptをマイグレーションした話

お久しぶりです。フロントエンドエンジニアの川上です。
業務では、UNIVERSE Ads のフロントエンド開発、フロントエンドの開発環境改善などを担当しています。
 

はじめに

マイクロアドには様々なプロダクトがありますが、私の担当している UNIVERSE Ads について、 フロントエンドフレームワークにVue.js を利用しています。
jp.vuejs.org

開発構成としては、MVVMでバックエンドとフロントエンドはそれぞれ、Spring Boot(Kotlin, Java), Vue.js(TypeScript, JavaScript)で開発されています。フロントエンドのテスティングフレームワーク/ライブラリには、Jestvue-test-utils を利用しています。また、バンドラには webpack を利用しています。今回は UNIVERSE Ads のフロントエンド開発環境へ TypeScript を導入する際に考慮した、それらのツールとの兼ね合いや、経緯、現状の開発環境との折り合いなどを踏まえつつ説明できたらと思います。

TypeScriptの導入について

UNIVERSE Ads では、たくさんの機能改修や機能追加が日々行われています。そのため、機能開発を行う中では必然的に保守性を考慮する必要が出てきます。フロントエンド開発において、TypeScript はその点においてとても優秀だと思っています。長期的な目で見て、保守性と開発規模を考えれば自ずと導入するという選択肢をとることになりました。

また、UNIVERSE Adsはフロントエンドのコードだけでもおおよそ10万行ほどの規模である、同時並行で複数の機能開発が走っているため、前提としてプロダクション環境への影響が出ない、かつ既存の開発効率を落とさない範囲で徐々に導入する方法をとることがベストでした。

そのため、まず TypeScript の導入にあたって、どのレベルで導入するかを検討しました。
検討の中で、すでに利用している Vueコンポーネントに対して TypeScript を導入するにあたって以下の懸念点が出てきました。
 

  • webpack で利用している Vueコンポーネント用の設定の更新が必要だった
  • 対応内容や方針決めにも工数がかかってしまう
  • チームメンバーに当時、TypeScript に詳しいメンバーが1人もいなかった

上記の理由から、 Vueコンポーネント に対しての TypeScript 導入は、技術コストなどを鑑みて一旦見送ることにしました。 1

ただ、TypeScript の導入メリットが他にもないかなと考えたとき、既に APIリクエスト 用の utilsメソッド として切り出している JavaScriptコード、既に導入していた Jest(導入時に TypeScript で記述可能にしていました 2)と JavaScriptで書かれている Vuex の Storeモジュール に対しての TypeScript化 を行うだけでも、メリットとして充分ありそうという結論になりました。

下記に上記のざっくりとした構成図を記します。

構成図1

余談にはなりますが、この決めと同時に、webpack の設定を JavaScript と TypeScript が共存できるように改修しました。
また、webpack の設定に extensions が設定されていなかったので、設定に .js.ts を追加することによって、JSファイルをTSファイルに置き換える際に、わざわざファイルパスを書き換えなくてもいいように環境を構築しておきました。

導入にメリットがあると結論を出した理由

TypeScript の導入メリットが他にもないかなと考えたとき、既に APIリクエスト 用の utilsメソッド として切り出している JavaScriptコード、既に導入していた Jest(導入時に TypeScript で記述可能にしていました)と JavaScriptで書かれている Vuex の Storeモジュール に対しての TypeScript化 を行うだけでも、メリットとして充分ありそうという結論になりました。

上記のように書きましたが、以下でその理由について解説したいと思います。 理由として大きく2つのメリットが挙げられました。3

① フロントエンドでテスト駆動様開発が可能になる

「構成図1」のような構成であればバックエンド開発とフロントエンド開発を並行で進める中、APIからレスポンスされるデータを事前に定義しておけば、prisma などを用いて想定されるレスポンスのデータパターンを事前に用意しておくことで、フロントエンドにおいても部分的にテスト駆動での開発が可能となります。
ただ、いろいろな兼ね合いから現状は型データを記載した簡易な API仕様書 をバックエンド開発の担当者に書いてもらうという方法をとっているため、テスト駆動開発とはいえず「様」とつけています。とはいえ、テストデータの型定義だけでもある程度機能することは大きなメリットだと判断しました。

② Vueコンポーネント に対して TypeScript の恩恵を与えられる

導入経緯でも話した通り、Vueコンポーネント に対して、 TypeScript を導入することはコストの兼ね合いなどから断念という形にはなりました。
とはいえ、テストファイルが TypeScript で記述されていて、APIリクエスト用の utilsメソッド の型データが定義されてさえいれば、Vueコンポーネント に対して、型情報の担保をした上で開発することが可能になります。
そういう意味で、間接的に TypeScript の恩恵を受けられるため、
TypeScript の恩恵を与えられる」ということになります。

具体的に解説

TypeScript の導入にメリットがあると結論を出した理由に大きな2つのメリットを挙げましたが、以下で具体的にコードを交えて解説できればと思います。

1つ目のメリットについて

下記のような型のレスポンスデータがあったとします。

// testResponse.d.ts

type Response = {
  response:  Record<string, unknown> & {
    shop: {
       isOpen: boolean,
       stateShop: "開店" | "閉店" 
    }
  }
}
export {
  Response
}

仮にStore getters から response.shop を参照する下記のような Vueコンポーネント を作成したい場合、

// Test1.vue
<template>
  <div>
    <div v-if="shop.isOpen">
      <p>shop.stateShop</p>
      ...
    </div>
    <div v-if="!shop.isOpen">
      <p>shop.stateShop</p>
      ...
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters('testStore', [
      'shop'
    ])
  }
}
</script>

テストパターンは、shop.isOpentrue | false の場合に、それぞれ、開店閉店と表示されることを確かめるテストコードを書いた上で、 Vueコンポーネント の開発をすることで障壁なくテスト駆動での開発ができるようなります。
ここで、Vuex の Storeモジュール や テストコードが API からレスポンスされるデータの型情報を保持していない場合、その渡される値の型が担保されていないため、テスト駆動で開発する場合には、mapGetters から取得できるデータの型の整合性からいちいち確認する必要が出てきてしまい、本質ではないところに時間がとられてしまいますね。

2つ目のメリットについて

仮に下記のようなコンポーネントがあるとします。 この Vue コンポーネントだけでは、this.$props.shopObject であるということはわかりますが、含まれるプロパティがなにかはわかりませんし、型の担保はできません。

// Test2.vue
<template>
  <div>
    <div v-if="shop.isOpen" data-test-tag="shop-open">
      <p>shop.stateShop</p>
      ...
    </div>
    <div v-if="!shop.isOpen" data-test-tag="shop-close">
      <p>shop.stateShop</p>
      ...
    </div>
  </div>
</template>

<script>
export default {
  props: {
      shop: {
        type: Object,
        required: true
      }
  }
}
</script>

下記のようにpropsから受け取るデータの型データと一意な型データを保持した上で、テストを追加することによって、オブジェクトに含まれるプロパティ、型の担保についても実質的に可能になります。また、今回は例に上げていませんが、Vuex の Store getters から Vueコンポーネント がデータを受けとる場合についても、Storeモジュール と Vueコンポーネント それぞれの単体テストによって、より堅牢なコンポーネントを開発できるでしょう。

import { Response } from 'testResponse'
// @ts-ignore
import Test2 from 'Test2'

const testPropsData: Response['response'] = {
  shop:  {
    isOpen: true,
    stateShop: '開店'
  }
}

describe('Test2.vue', () => {
  test.each(Object.keys(testPropsData).map((key) => {
      [`${key}`, testPropsData[`${key}`]]
  }))('型担保のテスト', (keyData, expected) => {
    const wrapper: Wrapper<Vue> = shallowMount(Test2, {
      propsData: testPropsData
    })
    // 型情報の担保
    expect(wrapper.props(keyData)).toStrictEqual(expected)
  })
})

おわりに

This will be the last minor release for 2.x and be offered as LTS (long-term support) for 18 months. It will continue to receive critical security updates even after the LTS period.
https://github.com/vuejs/vue/projects/6#card-34430029

今回、約10万行の規模のフロントエンド環境に TypeScript をマイグレーションした話をさせていただきました。
現在、Vue2 の LTS が2023年度末で終了 という情報もありますし、Vue3 への移行をしたいといいつつも、TypeScript 移行が残っているなど課題はたくさんあります。マイグレーションというのはエンジニアリングの中でもとても悩ましい議題の1つかと思います。この記事がみなさんのお役に立てるとうれしいです。
 
 


  1. いずれ対応することになるという認識はありつつ。

  2. 導入した際の記事を参考にしてください 。- Jestを使ったVueコンポーネントのマウントテストを導入した話

  3. その他にもたくさんメリットはありますが大きな理由として2つあげています。