Oisix ra daichi Creator's Blog(オイシックス・ラ・大地クリエイターズブログ)

オイシックス・ラ・大地株式会社のエンジニア・デザイナーが執筆している公式ブログです。

Oisix EC サイトの Web フロントエンド刷新プロジェクトの紹介 - 利用している技術・サービス等について

はじめに

Webフロントエンド開発を中心に行っている寺島です。

前回までにに引き続いて、Web フロントエンド刷新プロジェクトで利用している技術やサービスの一部を紹介します。

この記事は、以前の記事を前提として記述しているので、まだ見ていない方は以下を参照ください

creators.oisix.co.jp

creators.oisix.co.jp

利用技術とサービス・利用方法

このプロジェクトで採用している技術やその利用方法について紹介します。 いくつかのカテゴリに分けて順に紹介していきます。

開発言語・フレームワーク

TypeScriptVue.js を使っています。これ自体は、以前の記事で紹介した前身プロジェクトで採用されていたこともありそれを継承しているという状態です。 React.js でもよかったといえばそうなのですが、現時点では React.js にすべきだったなどの後悔はありません。

プロジェクトの開始時期などからわかると思いますが Vue.js の 2.x 系バージョンを使っています。 @vue/composition-api プラグインをプロジェクト開始当時から利用しており、Vue.js SFC (Single File Component) が肥大化・複雑化しないように実装をしています。 具体的には、SFC に記述するのはテンプレートとスタイル、そして、外部化した composition 関数の呼び出しや provide / inject によって提供された値の取得、初期化処理のみとしています。

以降で開発や Vue.js に係る個別トピックについて紹介します。

実装スタイル

リアクティブな値を利用したロジックは再利用の有無にかかわらず composition 関数として外部ファイルに実装を分けています。 加えて、実装時にはロジック部分と composition 関数で使うリアクティブな値の宣言を分けています。これで、比較的簡単にロジックの単体テストが記述できます。

例えば、カウンターのインクリメント、デクリメントを行うような composition 関数を以下のように実装するとします。

export const useCounter = () => {
   const count = ref(0)
   return {
      count: readonly(count),
      increment: (num: number = 1) => {
         count.value = count.value + num
      },
      decrement: (num: number = 1) => {
         count.value = count.value - num
      },
   }
}

これをテストしようとすると、count という状態を意識しテストを書く必要があります。このように状態とロジックが密になった実装は、実行順序などにテスト結果が左右される可能性があります。 この程度ならおそらく問題になりませんが、状態の種類が多くなったり、ロジックが複雑になったりするとテストが難しくなることがあります。

これを以下のように書くと、

// ロジック
export const increment = (c: Ref<number>) => (num: number = 1) => {
   c.value = c.value + num
}

export const decrement = (c: Ref<number>) => (num: number = 1) => {
   c.value = c.value - num
}

// 状態の宣言と注入
export const useCounter = () => {
   const count = ref(0)
   return {
      count: readonly(count),
      increment: increment(count),
      decrement: decrement(count),
   }
}

incrementdecrement の挙動は引数だけで決定するため、テストがしやすくなります。 useCounter は状態の宣言と注入のみなので、試験は記述せずに型などの静的な検証のみとしています。

このような工夫をしつつ、単体テストがロジックの大半をカバーできるようにしています。

テンプレートの型チェック

現在、テンプレートの型チェックには VSCode 上では Volar を利用し、 CI などでは vue-tsc を用いています。 これによって、よくある Vue.js (特に 2系) におけるテンプレートの型チェックに関する問題は殆ど感じない状態で開発をしています。

また、components パッケージからは vue-tsc でコンポーネントの型情報を出力しパッケージから公開しているため、別パッケージからコンポーネントを利用した時でも同様にテンプレートに対して型チェックが効きます。

ライブラリメジャーバージョンアップへの姿勢

Vue.js を 3.x にアップデートすることは必要だと考えていますが、開発体験に関していえば前述のとおりそれほど困っていません。強いて言えば script setup が使えませんが、これも unplugin-vue2-script-setup を使えば実現できるはずです。 (個人的に SFC が荒れそうな気がしているので script setup や reactivity transform macro などは使いたくないなと思っているところもあります...)

とはいえ、いずれアップデートが必要なので、計画をしています。 現状は、数個の依存パッケージが 2.x にのみ対応しているのでこれを取り除く必要があります。 それらの解決を行いつつ、3.x のバックポートがされるはずの 2.7 (これのリリースから 18カ月で EOL となることになっています) を経由してバージョンアップすることを基本姿勢としていますが、 2.7 に関する情報はいずれも古く信頼できるかの判断が難しいので、2.6 からアップデートすることも視野に入れています。

そのケースでも、components パッケージでは Vue.js コンポーネントの実装に vue-demi を使っているので、ビルド設定の調整だけで大半のコンポーネントはそのまま 3.x 用のライブラリとして利用することが可能です。結果、アップデートの困難さは軽減されるだろうと思っています。

静的チェック

前述の vue-tsc による型チェックに加えて、eslint, stylelint によるコーディングルールチェック、prettier によるフォーマットを行っています。 また、コーディングルールチェックについては huskylint-staged でコミット時にチェックを走らせ、ルール違反の状態でコミットに入らない仕組みを作っています。 この辺りは、最近だと利用していないケースの方が少ないと思うので特に深く言及しません。

テスト

テストの観点ごとに紹介します。

単体テスト

単体テストには jest を使っています。 単体テストでは、domain パッケージなどに含まれるロジックや、components パッケージなどで実装されている リアクティブな値に依存する composition 関数のロジックの検証のみを対象としています。 @vue/test-utils など用いたコンポーネントの振る舞いテストは行っていません。

その理由ですが、ロジックは基本的に TypeScript ファイルで記述されるためそれを jest で検証すれば大部分がカバーできます。そして、テンプレートへのそれらのバインドは宣言的であるためレビューや型チェックである程度誤りがわかります。

また、開発時には Storybook を利用しているので振る舞い自体の適切さは開発時にある程度のレベルで確認ができます。 加えて、それ以外のレイヤのテストで振る舞いの確認を実施する必要があるので、費用対効果を考え、単体テストレベルでは振る舞いの確認は行わないことにしました。

コンポーネントの表示に関する回帰テスト

前述の単体テストではロジックの大部分はカバーされますが、どのように表示されるかという部分についてはカバーできません。 E2E テストである程度担保される部分はあると思いますが、あるコンポーネントが別のコンポーネントでも利用されているようなケースは一般的にありえます。 その際、対象がたまたまテストスコープに入っていなければその変化を見逃す可能性があります。

そこで、@storybook/addon-storyshots@storybook/addon-storyshots-puppeteer で DOM と 画像のスナップショットを用いた回帰テストを行っています。 スナップショットは実装と同じリポジトリ内にコミットして実装と同時に PR に出すような運用にしています。

この方針は、セットアップが比較的簡単であり、取り除きやすいという点がプロジェクト開始時では重要だったので選択しました。

GitHub の PR 画面での画像比較機能は割と優秀なので、レビュー時に変化点の確認するうえではそれほど困りません。

画像のスナップショットは実行環境に応じて表示が変わる可能性があるので、環境をそろえるために Docker を利用しています。 ただ、素朴に Docker コンテナ内で、Storybook のビルド・スナップショットテストの実行を行うと、Docker Desktop for Mac など I/O のパフォーマンスが劣化することが知られている環境では満足に動かない状態になりました。

そこで、Docker コンテナでは Chrome を実行するだけにし、ホスト側から CDP (Chrome DevTools Protocol) で接続してスナップショットを取得するような形をとっています。 こうすることで、Storybook のビルドやスナップショットテストなどは、ホスト側で実行されるため大幅にパフォーマンスが良化しました。

コンポーネントに対するインタラクションの結果に対するスナップショットを取得したい場合は、@storybook/addon-storyshots の beforeSnapshot にフックして対象コンポーネントに対するインタラクションを行っています。

今後は、@storybook/addon-interactions を利用すればそれで十分そうであるため、そちらに寄せていこうと考えています。 加えて、@storybook/testing-vue を利用すれば、前述の単体テストでスコープ外とした部分も、開発中に自然とテストを記述することができそうなので、これらの開発状況の様子を見ながら併せて利用することを予定しています。

Cypress による結合テスト

Web Components や pages パッケージで実装されたアプリケーションの動作を検証するために Cypress で結合テストを実施できるようにしています。

一方で Web Components に関していえば、前述のとおり手動での E2E テストもそれなりに手厚く行う必要があるという事情があります。 結果として、そちらでカバーしたほうが短期的にはコスパがいいという判断を開発時に行うことが多く、あまり充実していないというのが実態です。

手動での E2E テスト

JSP で記述された既存の実装を Web Components で部分的に置き換えていくので、もともとの動作が置き換え後に損なわれていないかを検証する必要があります。 アプリケーションの性質的に、自動で試験をするのが困難であるため、開発前に調査した既存の振る舞いをもとにテストケースを作成して、テスト環境で実際に検証するという手段でのテストも行っています。

テストケースの管理などについては、Google Spread Sheets のフォーマットが社内に存在しており、それにテストケースを記述してテスト結果を記録するというような昔ながらのスタイルを継続しています。

ドキュメンテーション

esa を利用しています。

以下のような文書を実装前に作成しますが、これらを esa で管理しています。

  • 対象ページの調査結果 (表示のバリエーションや振る舞いなどを整理)
  • 対象ページに含まれるコンポーネントの定義 (どの部分をコンポーネントにするか・Atomic desgin のどのレイヤーに割り当てるか)
  • コンポーネントの仕様 (props, event, inject, slot など)

これらの文書は基本的に開発する対象を明確にすることを目的に作成しており、長く保守するというスタンスではありません。 保守する文書や信頼できる情報はコードの近くに置くという方針で進めており、ドキュメンテーションには Storybook と TypeDoc を利用しています。

Storybookは主にコンポーネントに対する情報であり、MDX フォーマットで記述します。対象コンポーネントのインターフェースは、defineComponentprops オプションや emit オプションに適切なコメントを記述すれば、自動的に Storybook で表示されます。slot も同様にテンプレート内に適切なコメントを記述することで Storybook で自動的に表示できます。

TypeDoc はコンポーネント以外の関数やモジュールに対する情報として利用しています。eslint で jsdoc コメントをつけることを強制し、コメントの書き忘れがないようにしています。

タスクランナー・CI

本プロジェクトのリポジトリは、機能レイヤごとに分かれた複数のパッケージが含まれる monorepo となっています。 monorepo 内でのローカルパッケージの link は npm workspace で実現しており、各パッケージで利用されるタスクは npm scripts で定義されています。 必要に応じて Node.js の script を書きますが、それらも npm scripts 経由で呼び出すようにしています。

パッケージ間で依存するタスクが存在します。例えば、『componets パッケージをビルドする前に、domain パッケージをビルドする』です。

これを解決する方法として、別パッケージをインポートする時にソースコードを参照することで、そもそもライブラリのパッケージは別々にビルドしないというものがあります。

f:id:oitech:20220329092443p:plain
monorepo でのビルド単位

これもどちらが一般に優れているとかではなく、一長一短があると思いますが、以下の理由で各パッケージごとのビルドを選択しています。

  • 各パッケージからの公開範囲をコントロールできる (ビルド後の js から公開したものしか使えない)
  • ソースは同じなのにビルド設定の差で違いが生じるということが起きない

後者についてですが、例えば、Vue.js 2.x 用と Vue.js 3.x 用などビルドにバリエーションが必要ならライブラリ側が複数種類のビルドをすることにしています (このプロジェクトでは Web Components で使うかどうかでビルドを変える必要があります)。 パッケージの責任が明確になりやすいという点で優れていると感じています。

このような理由で、ビルド手段を変えることで、ビルド依存の解決は行っていません。

ビルドの依存関係には当初 npm script の pre / post prefix を付けたタスクで制御していましたが、複雑になるにつれて管理が難しくなりました。 これを解消するために、一時は、execa などを使った Node.js の script で依存解決のロジックを表現する方針をとっていましたが、現在は、 Turborepo を利用しています。 Nx などとは違い、タスクの依存関係の定義と実行にフォーカスをしているという理由で導入しやすいという点と、各ビルド産物をキャッシュできるという点が当時の問題にマッチしていました。

CI には GitHub Actions を利用しており、PR や メインブランチへのコミットごとに 静的チェックや各種テスト、Storybook のビルドが行われます。 ビルド結果らは AWS S3 にデプロイされます。

SRE チームに環境を整えてもらい、CloudFront・Lambda@Edge・Cognito を利用して Google アカウントの認証をかけています。 これのおかげで、リモートワーク下でも安全にS3 にデプロイしたコンテンツにアクセスできる状態になっています。

タスク管理

タスク管理は GitHub Issue と GitHub Project (Beta) を利用しています。

Issue はタスクリスト機能を使って、以下の 3 階層に分けて管理しています。

  • 『○○ページを刷新する』などの要求事項
  • 『○○コンポーネントを Web Components にする』などのリリース粒度となる機能
  • 『○○コンポーネント実装』や『○○ページの現状調査』などの作業

要求事項は、例えば 『2022 Q1』 や 『2023 Q4』 のような粒度で実現予定を大まかにスケジューリングしています。 これは、カンバン表示をするために GitHub Project (Beta) のカスタム属性を使っています。

二週間ごとの開発イテレーションはマイルストーンで表現し、リリース粒度となる機能や、作業をこれに含めて管理しています。

f:id:oitech:20220328175541p:plain
3階層で管理する GitHub Issue

それぞれの階層ごとにカンバンや目的ごとのリスト表示を用意し、各 Issue のステータスがわかりやすい状態にしています。

作業者は基本的にカンバン表示された、作業の Issue からセルフアサインをして Issue のステータスを変更しながら開発を進めていくという形をとっています。

コミュニケーション

チーム内でのコミュニケーションでは、テキストコミュニケーションには Slack を利用し、音声コミュニケーションには Gather.town を利用しています。

本プロジェクトではペアワークやモブワークを推奨していることと、対象が古いソフトウェアということもあり、比較的頻繁に相談や確認をするということが行われます。 そのため、チーム内では同時多発的に会話が行われ、会話の相手が頻繁に変わります。

Slack のハドル機能などでは、同時に会話を行うことができず、いくつかのバーチャルオフィスツールや Discord などを試してきました。 ですが、画面共有時の画質が期待するものでなかったり、会話の相手を変えるのがめんどくさいなどでなかなか要望に合うものを見つけられずにいました。

現在利用している Gather.town はそのような要求にもマッチしておりとても満足しています。

f:id:oitech:20220314152639p:plain
普段の風景

チームメンバー全員が集まって行うようなミーティングも Gather.town を使っています。 みんなで一つの部屋に集まって司会者が画面共有しながら行っています。

f:id:oitech:20220314160529p:plain
夕会の様子 (各々がそっぽ向いているのがシュールですが、同じ部屋の中では音声が共有される設定にしています)

おわりに

Web フロントエンド刷新プロジェクトの開発で利用している技術やサービスなどを一部紹介しました。 全体的に紹介することを目的としていたので、それぞれについてはそれほど深く言及できませんでしたが、どういうものが使われているかはイメージできる内容になったのではないかと思っています。

次回は、このプロジェクトの現状と今後の展望について紹介します。

creators.oisix.co.jp

本プロジェクトは現在進行中で、一緒にこのプロジェクトを推進したい方を募集中です。

興味がある方は以下をご覧ください。

採用メディアORDig

recruit.oisixradaichi.co.jp

求人情報

hrmos.co

hrmos.co

Oisix ra daichi Creator's Blogはオイシックス・ラ・大地株式会社のエンジニア・デザイナーが執筆している公式ブログです。

オイシックス・ラ・大地株式会社では一緒に働く仲間を募集しています