📚

SimpleでStaticなNext.jsを中心としたフロントエンド技術選定

2024/09/17に公開

シンプルで、スタティックで、ネクストジェーエスなフロントエンドで行かせていただきます

はじめに

こんにちは。最近、地面師が流行っているNstockに所属しているmatanoです。普段は株式報酬SaaSチームでソフトウェアエンジニアをしています。前職では新卒でフロントエンドエンジニアとして入社して活動していました。2024年1月にNstockに入社して以来、バックエンドやインフラの開発をしつつもフロントエンド周りの技術選定や刷新を行う機会が多くありました。
この度、資金調達(リリース)や内部の開発組織体制の再編など色々と節目となったので株式報酬SaaSのフロントエンド技術選定について簡潔にまとめることにしました。

株式報酬SaaS 事務局と権利者アプリケーション

Nstockには現在、株式報酬SaaSとセカンダリー事業それぞれのプロダクト開発を行うチームが存在します
株式報酬SaaSは既にお客様への有償提供が始まっており私はこのチームに属しています。
https://nstock.com/
この記事の内容も株式報酬SaaS開発の内容となります。

株式報酬SaaSのフロントエンドは2つのアプリケーションが存在します。

  • 権利者向け: ストックオプションを付与された従業員等が利用するスマートフォン向けWebアプリ
  • 事務局(管理者)向け: 社内の一部の担当者が利用するPC向けWebアプリ

権利者側:行使申請の入力・行使申請完了画面
権利者側:行使申請の入力・行使申請完了画面

管理者(事務局)側:行使申請内容の確認画面
管理者(事務局)側:行使申請内容の確認画面

画像は「株式報酬SaaS「Nstock」、ストックオプションの行使申請・承認機能を提供開始」より引用

基本的には同じような技術構成をしていますが一部差分があるので表でまとめます。
✅ 本格利用中
🟡 利用開始したばかり / お書き換えるかも
🔶 導入検討中
🛑 利用なし

事務局 権利者
Next.js
orval + TanStack Query
SmartHR UI 🛑
Tailwind 🔶
shadcn/ui 🛑 🟡
React Hook Form
yup 🟡 🛑
valibot 🟡 🟡
Vitest
Biome
Storybook 🛑 🔶
axios
native fetch 🔶 🔶

Next.js

事務局と権利者で利用
https://nextjs.org/

アプリケーション基盤となるフレームワークとしてNext.jsを利用しています。2022年の4月頃の開発立ち上げ当時から使い続けています。時期的にもReactで中規模以上のアプリケーションつくるならデファクトスタンダードだったと認識しています。
開発初期はとにかく事業の立ち上げのスピードを優先しており、ゼロコンフィグで簡単にアプリケーションが立ち上がること、ファイルベースルーティングが直感的でわかりやすいことなどが採用の理由でした。

export: static

事務局と権利者アプリケーションともに全てのページでログインしないと利用できないサービスなのでstaticビルドをしています。APIサーバーはJava(Spring Boot)で動いており、ログインが確認出来次第、全てブラウザからAPIコールしています。
ビルド時にAPIサーバーへの通信は発生していません。事前生成されるHTMLはヘッダーやフッター以外はほぼ空のままです。

Pages Router

開発立ち上げ当時はApp Routerは存在しておらず、そのままPages Routerを使い続けています。
今後もstaticビルドを続ける予定であるためApp Routerに移行する強いモチベーションは現時点ではありません。とはいえLayouts and Templatesなど我々のアプリケーションでは便利ですし、他にも一部の機能はApp Routerでしか使えないのでタイミングをみて移行する可能性は高いです。

基本方針

全体的に薄めに使ってます。利用頻度が高いAPIは

  • next/link
    • legacyBehaviorが一部残っているが間もなく撲滅予定
  • next/head
  • next/router

くらいです。getStaticPropsgetStaticPathsは存在していません。next.config.jsも20行弱です。useMemouseCallbackも極わずかです。(来たるべきReact Compilerに期待。)

フロントエンド専任チームが存在するわけではないのでなるべく初学者でも扱いやすいようにしています。version 15の新機能やRSCなどについて個人的にウォッチはしていますがプロダクトに導入するものはあまりなさそうです。

orval + TanStack Query (React Query)

事務局と権利者で利用
https://orval.dev/
https://tanstack.com/query/latest

スキーマ駆動開発をしておりOpenAPIを活用しています。openapi.yamlからのコード生成にはorvalを利用しています。
当初はopenapi-generatorを利用していましたがいくつか懸念点が存在してました。生成コードにオーバーヘッドが多かったり、カスタムフックの生成までは対応しておらず以下のようなコードが多く存在していました。

import { Tenant } from '@/types/typescript-axios';

const { data: tenant } = useSWR<Tenant, AxiosError>(
    '/tenant',
    myFetcher<Tenant>() // myFetcherはheaderつけたりなど
);

これでは煩雑かつエンドポイント文字列ミス、型指定ミスと危険性もあります。そのためorval + TanStackへの移行をしました。以下のようなコードとなり簡潔かつ安全性が増しました。

import { useGetTenant } from '@/orval/endpoint';

const { data: tenant } = useGetTenant();

アダプターとしてswrも選択可能でした。ただバンドルサイズを気にするフェーズでもなく多機能で、個人的にも使い慣れていたので同時にTanStack Queryへ移行しました。

Suspenseバージョンのフックをジェネレートするオプション(追加されたcommit)もあるのですが使っていません。ちょうどsequential => parallelに変更する等、揉めているようなので一先ず見送っておいてかったなと思っています。pages内でchildrenが欲しいデータをGETするフックを呼び出すパターンが殆どです。

const UserPage = () => {
    const { data, error, isPending } = useGetUser();

	if (error) {
		return <ErrorScreen error={error} />;
	}
	if (isPending) {
		return <CenteredLoader />;
	}

	return (
		<div>
			<UserDetail user={user} />
		</div>
	);
};

取得系に限らず更新系でも殆どのAPIコールはTanStack Queryを用いて実行しています。

import { usePostTenantMembers } from '@/orval/endpoint';

const {
    mutate,
    isPending,
    error,
} = usePostTenantMembers();

日本語の解説はこちらの記事がオススメです。
https://zenn.dev/hrbrain/articles/3ca5d37dd0b80e

SmartHR UI (SmartHR Desgin System)

事務局で利用
https://smarthr.design/
https://github.com/kufu/smarthr-ui

開発当初から事務局アプリケーションのUIコンポーネントにはSmartHR UIを採用しています。OSSです。

Nstockは現在SmartHRの子会社です。PdMもSmartHRの出身ということで慣れていたことをきっかけにSmartHR UIを採用しました。事務局アプリケーションはtoBということもあり相性がよいです。よくメンテされた国産のデザインシステムで初期の開発効率を上げることができました。子会社である恩恵も受けており共有チャンネルにて気軽に相談できる体制が存在します。実際に相談を経てライブラリが更新されることもあります。
https://github.com/kufu/smarthr-ui/pull/4770

peerDependenciesとしてstyled-componentsが必要でした。ただし最近SmartHR UI本体側のスタイリングがTailwindになったため必要なくなりつつあります。
https://tech.smarthr.jp/entry/2023/12/22/080000#:~:text=Slackでアナウンス-,その他の動き,-今年はstyled

今年はstyled-componentsからの脱却が一つの大きなテーマで、 Tailwind CSS化をチーム一丸となって鋭意進めております。 詳細はブログ記事で出る予定なので、続報を楽しみにお待ち下さい。

我々のコードでも極わずかですが一部のコンポーネントをstyledで拡張している箇所があります。こちらは(権利者と揃えるため)Tailwindに置き換え予定です。

入社してからバージョン30から最新版まで上げきったのはよい思い出です。
Slackでのやりとり 素敵なスプリントゴールが設定されていたので共有します。 Sprint#85 SmartHR UIが最新版になっている スタンプリアクション ハート 手のハート ウォオオオ! matano DONEのスタンプ
Slackでのやりとり SmartHR社の人もリアクションしてくれている

Tailwind CSS

権利者で利用
https://tailwindcss.com/

権利者側はtoCで初期ではスマートフォン利用を前提として開発を進めており、自前でデザインシステムを構築していました。

Sassを利用していましたが権利者アプリケーションのリニューアルをきっかけにTailwindに移行しました。移行の理由としては株式報酬SaaSだけの事情ではありません。第2,第3の別プロダクトを作ることになったときに共通のデザインシステムが必要になるだろうとデザイナー含めて議論されてきました。セカンダリー事業の開発が始まり(詳細は省きますが)Tailwindが採用となったことがきっかけで株式報酬SaaS側も移行を本格検討しました。
SmartHR UI開発チームへのヒアリングや世間の事例の調査、Tailwind CSS実践入門を課題図書としてチームメンバーには指定した章を読んでもらうといったフェーズを経て移行を決定しました。(オススメ本です。以下は著者のZenn)
https://zenn.dev/f_subal/articles/d11b226f1e51b8
移行前と後の具体的なメリット・デメリットは既に多くの資料があるのとまだ移行して間もないため割愛します。

移行は新規開発と平行して3週間程度とかなりの急ピッチで完了しました。アプリケーションの規模がまだ小さかったのとtoken化がキレイになされていたため実現できたスピード感です。
Slackのやりとり matano 9月5日 10:37 Button以外Tailwind化が終わった(≒Buttonがちょいしんどい) (編集済み) matano 月曜日 11:12 完全移行完了しました!
Slackでのやりとり SaaSチームエンジニア全員がリアクションしてくれている

現在はセカンダリー事業と共通のカラーを利用しようとtailwind.configの共通化を議論中です。どこかのタイミングでComponentをinternal packageとして配布する計画はありますが具体的なことは未定です。立ち上げ時はスピードを優先させるため共通化の優先度は低いです。

shadcn/ui

権利者で利用開始
https://ui.shadcn.com/
権利者側ではUIコンポーネントライブラリを活用しておらず、全て1からコンポーネントを作成していました。しかしアクセシビリティの考慮があまり出来ていなかったり、新たにコンポーネントを作成したくなった際のスピード感が失われる要因でした。
冒頭紹介したストックオプションの行使申請・承認機能の新規開発と同時に新規に作成するコンポーネントに関してはshadcn/uiを利用する方針となりました。Headless UI系のライブラリは沢山存在しますがTailwindとの親和性やスタイリングが初期から一定存在していることによる工数削減などを理由に選定しました。
どこまで自前で拡張するのか、カラーはどうするのかなどの課題は山積みでこの先、どの程度利用していくかは未知数です。

React Hook Form + yup / valibot

事務局と権利者で利用
https://react-hook-form.com/
2022年6月頃から利用しています。こちらも当時としてはデファクトスタンダードだったと思います。事務局アプリケーションに関してはフォームが頻出なのでかなり活用しています。
バリデーションに関してはシンプルなものに関してbuild-inのminmaxLengthなどを活用しています。複雑なものに関してはresolverとしてyupを使用していました。しかしTypeScriptの型が上手く効かないなどの課題感がありvalibotへの移行を実施中です。shadcn/uiがzodを組み込んでいるコンポーネントを作成することもあり揃えたほうがよいかもと考え中です。(先にshadcn/uiを導入していたらzodを選んでいた可能性が高かった。)

AWS Amplify

事務局と権利者で利用
https://aws.amazon.com/jp/amplify/

バックエンドのインフラがほぼ全てAWSで動いており、詳しいメンバーもいるので活用しています。StaticビルドしているのでAmplifyである必要もないのですがコスト増もあまりないのでそのままにしています。(隙を見て調査したい。)
筆者は業務でVercel + Next.jsを数年間運用していたのですが、Vercelと比較するとAmplifyが劣る部分もあります。Staticアプリケーションとしてビルドして欲しいのにDynamicアプリケーションと誤検知されてしまうことがありました。これはnext exportが廃止された影響などが原因でした。
他にもNext.jsの最新版への対応、ビルド速度、プレビュー環境のコメント機能などは劣ります。しかし金銭コストや他インフラと同様のアカウント管理が実現できるといった面では優位性があります。Staticビルドしておりmiddlewareなども利用する必要がないので十分であり採用しています。

Vitest

事務局と権利者で利用
https://vitest.dev/

Jestから置き換えました。ただ正直テストケースはまだかなり少ないです。増やしていきたい。

Biome

事務局と権利者で利用
https://biomejs.dev/ja/

ESLintとPrettierから移行しました。シンプルさと速度が主な選定理由です。移行ではありますが基本的にはデフォルトのルールに従うようにしました。
デメリットとしてはeslint-config-nexteslint-config-smarthrは適用出来ていません(そもそも導入されてなかった)。理想としてはこれらだけでもeslintで動かすべきですが現在は見送っています…

検討中

Storybook

https://storybook.js.org/
事務局側はSmartHR UIに強く依存しているのでコンポーネントカタログを整備する優先度はあまり高くありません。一方、権利者アプリケーションにも徐々に機能が追加され、自前のコンポーネント増えるに伴い必要性が増して来ました。開発初期は知見を持ったエンジニアがおらず、そもそもToo Muchそうといった理由で採用を見送っていました。しかしStorybookのエコシステムも当時より成熟しており今なら十分にペイすると考えています。

脱axios (or fetch adapter)

orval + TanStack Queryで言及しませんでしたがaxiosを利用しています。当時は「axiosのほうが簡潔にコードを書ける」、「axiosのほうが直感的に書ける」というのが選定理由でした。しかし最近ではApp Routerではnative fetchを前提とした設計となっていたりとJavaScriptエコシステム全体でfetchに寄せる方がベターとなっていると思います。
とはいえ初期から使っていることもあり依存も深く、現在は最近追加されたfetch adapterを適用出来ないか検討中です。
https://github.com/axios/axios/pull/6371

おわりに(+会社宣伝)

Nstock 株式報酬SaaSのフロントエンドにおける主な採用技術について紹介しました。開発の立ち上げが2022年と比較的最近、かつ私が参画した2024年1月ではコードベースがまだ小さかったので比較的モダンな環境に出来たと思っています。

しかしなるべく実情を伝えたつもりですのでまだまだ整備が足りてない印象を受けた方も多いと思います。そんなNstockに少しでも興味をもった方は以下のコンテンツをご覧ください!

最近公開しました採用サイトです(Next.js + Vercelで動いています)
https://recruit.nstock.co.jp/

兜町でのエンジニア向けMeetupもあります。応募意思・転職意思不要なので気軽に遊びに来てください!
https://nstock.connpass.com/event/330196/
https://nstock.connpass.com/event/330198/

私がファシリテーターになったエンジニア対談記事もぜひご覧ください!
https://nstock.co.jp/blog/engineer_persona

Nstock Tech Blog

Discussion