TUNAGの新フロントエンドを支える技術と設計

株式会社スタメンでフロントエンドエンジニアをしている神尾です。普段は、エンゲージメントプラットフォーム「TUNAG」の開発をしています。

TUNAGでは、2023年1月からWebフロントエンドのリプレイスプロジェクトが始まり、今もプロジェクトが進行中です。現在のWebフロントエンドでは技術選定の選択肢が多く、選定にあたっての検討事項がとても多いと思います。

この記事では、リプレイスの際に採用した技術の選定理由や設計についてまとめたので、この記事を通して、読んでくださった方のお役に立てれば嬉しいです。 また、リプレイスの背景やインフラについては別記事になっているので、そちらもご覧ください。

tech.stmn.co.jp

技術構成 / 選定理由

React / Next.js / TypeScript

ベースとなる技術として、React / Next.js / TypeScriptを採用しました。

React

社内のプロダクトはReactを多く使用しており、社内に知見を持つエンジニアが多かったため、ここは特に議論はなく決定しました。

Next.js

Next.jsを採用した理由はいくつかあります。以下が主な選定理由です。

  • ルーティングやCode Splittingなどデフォルトで備わっている機能の恩恵を受けたかったこと
  • 一部の機能でSSRを使いたかったことや、プロダクトの性質上、様々な機能があるため、それに合わせてレンダリング方法を選択できる余地があることにメリットを感じたため

TypeScript

デファクトスタンダードになっているため、採用しない理由がなく採用をしました。

SWR

データ取得ライブラリ&状態管理ライブラリとして、SWRを採用しました。

リプレイス前の環境では、Reduxを使用していましたが、コード量の多さや学習コストの高さからReduxの採用はしませんでした。

また、Reduxで管理していた状態のほとんどがAPIから取得したデータであった為、SWRで問題がなかったのも採用理由です。

SWR以外にも同じようなライブラリとしてTanStack QueryRTK Queryがあります。 その上でSWRを採用した理由は、開発元がVercelのためNext.jsの新機能が出た時に一番早く対応されるだろうということやRTK QueryがSuspenseに対応していなかった等が挙げられます。

aspida

バックエンドとしてRailsを使ってスキーマ駆動で開発しているため、OpenAPIからHTTPクライアントとリクエスト / レスポンスの型を自動生成して開発効率の向上を図っています。 そのためのライブラリとして、aspidaを採用しました。

同じようなライブラリとして、Orvalopenapi-typescriptも候補に上がりました。どのライブラリでも「OpenAPIからHTTPクライアントとリクエスト / レスポンスの型を自動生成」という要件を満たせていましたが、aspidaが社内で一番知見があったため採用しました。

msw

モックサーバーとしてmswを採用しました。 aspidaで自動生成したレスポンスの型と組み合わせることで、型安全なモックサーバーを立てています。 そのおかげで、OpenAPIがあれば、APIの実装を待たずに、ほとんどのフロントエンドの開発を完了することが出来るので開発効率が爆上がりしました。

開発だけでなくテストにも活用しています。

storybook / chromatic

現在のフロントエンド開発において、必須とも言えるstorybookを採用し、ホスティング先とVisual Regression Testing(VRT)のためにchromaticを採用しました。

storybookは、カタログになるというメリットだけでなく、モックサーバーすら立てることなくプレゼンテーショナルなコンポーネントの開発が始められる利点もあります。

また、storybookを導入したけど、そのままメンテナンスが行われないという話は、よく耳にするので、そうならないために、storiesはhygenで自動生成するようにしています。 そうすることで、propsを追加するだけでstorybookに載せることができ、実装コストがほとんどないため、継続的に運用できています。

styled-components

CSSライブラリとしてcss-in-jsのstyled-componentsを採用しました。

選定理由としては、旧環境で使用していたので知見があり、使い慣れていたというのが大きいです。

ですが、これに関してはNext.jsのApp Routerがstableになった現在においては、適切な技術選定ではなかったと思っています。(当時はギリギリexperimentalでした)

現在は Pages Routerを使っているため、特に問題になっていませんが、将来的にApp Routerに移行するタイミングで、改めてCSSライブラリ選定し直したいと思っています。

hygen

コンポーネントの雛形作成のためにhygenを採用しました。

後述でディレクトリ構成について書きますが、現在はそのうちfeatures, ui, pages, screens, usecasesはhygenで雛形を自動生成できるようにしています。 初めは、開発効率の向上のための雛形作成だと考えていましたが、現在は雛形によってディレクトリ構成が崩れにくいというメリットの方が大きいと感じています。

今後も雛形化できるものは積極的にしていき、方針が変わった場合は、hygenの雛形をメンテナンスするということを継続的にしていきたいと思います。

eslint / prettier / stylelint

静的解析として eslint、コードフォーマッターとしてprettier / stylelint を導入しています。

huskyでコミット前に実行することで、ルールに違反したコードが含まれるのを防いでいます。

これによる恩恵はコードレビュー時においても大きいです。 「コメントする程ではないけど気になる」といったものを、これらのツールで弾くことでレビュワーもレビュイーもハッピーになれます。

現在は、レビュー内容でルールを決められるものは、規約ではなく、これらで自動修正するようにしています。 具体的には、以下のようなプラグインやルールを設定しています(全てではありません)

プラグイン

eslintのルール

  • react/jsx-boolean-value
    • 例: propsがisOpen={true} の場合 isOpenに省略する
  • react/jsx-curly-brace-presence
    • 例: propsのstringがtitle={'text'}の場合、波括弧は不要なのでtitle='text'に変更する
  • react/self-closing-comp
    • コンポーネントにchildrenがない場合、タグを省略記法に変更する

以上が技術構成と選定理由でした。

ディレクトリ構成

├ src
│ ├ apis // API層
│ ├ constants // 共通の定数
│ ├ features // 特定の機能を実現するコンポーネント
│ │ ├ Todo
│ │ │ ├ hooks // 機能を実現するカスタムフック
│ │ │ ├ logics // 機能を実現するためのロジック(データ整形など)
│ │ │ ├ presentations // プレゼンテーショナルなコンポーネント
│ │ │ │ ├ index.tsx.  // 正常系  
│ │ │ │ ├ loading.tsx // ローディング
│ │ │ │ ├ error.tsx   // エラー
│ │ │ ├ tests // 統合テスト
│ │ │ ├ index.stories.tsx // storybook
│ │ ├ index.tsx // hooksとpresentationsの繋ぎ込み
│ ├ pages // Next.js のルーティング
│ ├ screens // featuresコンポーネントをページ全体に配置する。URLのクエリパラメータ取得
│ ├ ui // 共通のコンポーネント
│ ├ usecases // 共通のhooks
│ ├ utils // 共通のロジック

現在のディレクトリ構成はこのようになっています。 (他にもtestConfigs(テストの設定)など細かいものはありますが割愛しています)

この中からいくつかピックアップして紹介させていただきます。

apis

基本的にaspida等を使って自動生成したものを置いており、ほとんどは yarn prepare にscriptを入れているので、そこで自動生成するようにしています。 具体的には以下を置いています。

  • HTTP クライアント
  • レスポンスの型
  • OpenAPIのexampleから抜き出したモックデータ
  • mswのモック

features

featuresでは、以下の2種類のコンポーネントに分けることで責務を明確にしています。

  • 副作用を持つコンポーネント(Container Component)
  • 副作用を持たない純粋関数のコンポーネント(Presentational Component)

副作用を持つコンポーネント(Container Component)

このコンポーネントは、同じfeature内のhooksやlogics、src/utlilsやsrc/usecasesなどを使って機能を実現し、Presentational Componentに流し込む役割があります。 そのため、どのように表示されるか(見た目)は知りません。

ここに副作用を集中させることで、多くのコンポーネントを副作用を持たない純粋関数のコンポーネント(Presentational Component)にすることが目的です。

副作用を持たない純粋関数のコンポーネント(Presentational Component)

副作用を持たないコンポーネントで、見た目に責務を持ちます。 このコンポーネントに渡ってきたpropsがどのようなロジックで作られているのかについては知りません。

pages / screens

pages

pagesはルーティングのみに責務を持ち、ページに表示する内容は知らず、対応するscreensを呼び出すことのみを行なっています。

screens

実際にページに表示する内容です。 基本的にはfeaturesを並べることを行いますが、ページに依存しているURLのクエリパラメータの取得もここで行います。

ui

共通で使えるUIコンポーネントを置きます。 実際には、TUNAGにはデザインシステムがあり、基本的には、そこにUIコンポーネントがあるので、このディレクトリに置くものは多くありません。

ただ、デザインシステムに載せるか判断できていないもの(デザインが未確定など)は一時的に、このディレクトリに置き、随時、デザインシステムへの移行を検討しています。

運用してみて分かった嬉しさ・つらさ

嬉しさ

責務が明確で可読性が高い

各コンポーネントの責務が明確になっているのと依存関係が一方向でシンプルなので可読性が非常に高いです。 現在はやっていませんが、今後開発する上で必要になれば、コンポーネント間の依存関係もeslintで制限することも考えています。

自動生成でコード記述量が少ない & 構成が崩れにくい

hygenとaspidaを使って自動生成できる箇所が多いので、実装量が少なくなりました。 また、それによってフロントエンドの開発になれていないメンバーであってもスムーズに実装することが出来ています。

現在はリプレイス中のため、開発するチームは限られていますが、今後もある特定のチームだけがフロントエンドを開発するわけではないので、この段階から自動生成をここまで充実させられているのはとても良いことだと思います。

SWRが複数リクエストを1つにまとめてくれるので、サーバーの負荷を上げずに、1つのfeatureで機能が完結できる

SWRのようなデータ取得ライブラリを使わずに2つの異なるfeatureから同じAPIにリクエストをした場合、2回リクエストが行われます。

仮に、そのAPIの処理が重くサーバーの負荷を軽減するために1度のリクエストに制限したいとなった場合の解決策として、グローバルに状態管理するという方法があると思います。 ただ、そうなるとfeatureの外との依存関係が生まれてしまい、複雑になる恐れあるので、できれば避けたい実装です。

SWRはデフォルト2秒間は同じkeyに対する複数リクエストを1つにまとめてくれる機能を持っています。 それを活用することで複数の異なるfeatureからの同じAPIへのリクエストを1本にまとめ、グローバルに状態管理することもなく、単独のfeatureのみで機能が完結させることができました。 その結果、実装がシンプルになりとても助かっています。

つらさ

副作用をまとめているためpropsのバケツリレーが多い

featuresでは、副作用を集中させることで、ほとんどをプレゼンテーショナルなコンポーネントで構成することができている一方で、表示に必要なpropsを上から流し込むことが多いためバケツリレーが多くなります。 これに関しては、現在は許容するようにしていますが、複雑な機能であればあるほどpropsが多くなるので、その場合の対応は今後必要になりそうだなと思っています。

また、stateをfeatureのトップのコンポーネント(Container Component)にまとめていることで不要なコンポーネントまで再レンダリングがされるようになっています。 現在、再レンダリングでユーザビリティに関わるほどのパフォーマンスへの悪影響はありませんし、もしあったとしてもメモ化することで対応できるのではないかと思っています。

ドキュメントの運用

ADR(Architecture Decision Records)、README、Wikiを定期的にチームでメンテナンスするようにしています。 そうすることで新しいメンバーが開発に加わる時に、すぐに開発を始められる状態を作りたいと思っています。 また、技術には流行り廃りがあるため、いつかその技術を捨てる時がきます。その時に捨てやすくするためにADRとして技術の選定理由を文書に残すようにしています。

その他にも定期的にメンテナンスすることで、今の開発メンバーも理解に繋がりますし、見返すことで課題を見つけることにも繋がるので、これからも継続していきたいと思います。

今後のこと

App Routerの対応

現在はPages Routerを使っていますが、Next.jsのバージョンは最新になっているためApp Routerが使える状況です。 ですが、App Routerの対応をするよりも新環境へのリプレイスを優先したいという考えで、現在はPages Routerでリプレイスを行っています。

ただ、Next.jsの進化はApp Routerが中心になっていくと思うので、ゆくゆくはApp Routerの対応をしたいと思っています。

SuspenseとErrorBoundary

現在は一部の機能でSuspenseとErrorBoundaryを使っています。 この2つを使うことでfeature内の実装がシンプルになるため、近いうちに全てのfeatureで対応したいと思っています。

おわりに

ここまで読んでいただきありがとうございます。 とても長くなってしまいましたが「TUNAG」のリプレイスの際の技術選定理由や設計について、まとめさせていただきました。

リプレイスブロジェクトは始まったばかりで、まだまだかかると予想しています。 このような長期のプロジェクトを成功させるためにも日々、改善を重ねていきたいと思います。

株式会社スタメンではエンジニアを絶賛募集中です!!

興味を持っていただけたら、下記のリンクからご応募ください!

お待ちしております!

herp.careers