T3 Stack を用いて社内ツールを開発した所感

2023-05-12

はじめに

社内ツールを作るのに T3 Stack を使ってみたので、その所感を書きます。
Belong の公式フロントエンドスタックは React (Next.js) + TypeScript であり、型安全性を重要視しています。T3 Stack は Typesafety Isn't Optional という言葉が示す通り型安全性を特に重視したフロントエンドの技術スタックなので興味はありましたが、 tRPC など新しめの技術を使っており、私としては重要な本番システムにおいて不安定な技術によるリスクを取りたくはありません。 Bleeding Edge に触れて血を見たくはないのです。

しかし同時に、新しい技術の調査は必要であり、将来的なチームのケイパビリティを高めることに繋がります。(エンジニアの楽しみでもあります :)
そこで、最悪動かなくなっても良いレベル感の社内ツールで導入し使用感や導入のしやすさを確認してみました。 私は Next.js はたまに触ることがある程度で普段から Web UI のフロントエンドを書いているわけではないので”専門家”の意見ではありませんが、 感想としては T3 Stack は扱いやすく今後の安定化が進めば本番サービスへの導入もありだと思いました。

T3 Stack

ここではまず T3 Stack が何なのかの紹介をします。

T3 StackTheo 氏によって提唱されたフロントエンドを構築するための技術スタックで、 簡潔さ(simplicity)・モジュール性(modularity)・フルスタックの型安全性(full-stack typesafety)を重視しています。
Typesafety Isn't Optional というスローガンが示す通り、T3 Stack は TypeScript による型安全性を重視しています。

T3 Stack の定義では以下の技術を採用しています。

  • TypeScript
  • Next.js
  • tRPC
  • Prisma
  • Tailwind CSS
  • NextAuth.js

tRPC や Prisma、NextAuth.js などの構成からは、認証機能を持つ Web サービスのフロントエンドからバックエンドまでを Next.js 用いつつ TypeScript で一気通貫で構築しようという思惑が汲み取れます。

以下では公式ドキュメントを参考に、各技術の採用理由について触れます。

TypeScript

TypeScript の必要性は言うまでもなく、型安全性を重視する T3 Stack にとっては当然の選択です。

Next.js

React は UI 開発に有用でとても良いですが制限もあり、また単体ではフルスタックのアプリケーションを構築するには不十分です。 Next.js は、React を使ったアプリケーションを作成するにあたり、思想に拘りすぎず、かつ強く最適化したアプローチを提供しているため採用したようです。

余談ですがこの React とフレームワーク辺りの話は React がフレームワークと共存していく話とも関連するので興味のある方は読むと面白いと思います。 ちなみにこの議論を生んだ GitHub の PR を作ったのは T3 App 提唱者の Theo 氏です。

tRPC

tRPC はスキーマやコード生成なしで型安全な API のレスポンスを提供することができるライブラリであり、バックエンドとフロントエンドの通信間で型を安全に保つ事を可能にします。
tRPC は現状 GraphQL が実現してくれる、「サーバーに対するシームレスで型安全なクライアント開発」を、定型文を一切使わずに実現することを可能にするため採用されました。

Prisma

Prisma は、データベースの日常的な操作をより簡単にするためのツール群を提供する ORM で、TypeScript でデータベースのエンティティやリレーションを表現することが出来ます。 SQL に対する Prisma は 「JS に対する TypeScript」のようなもの と表現されています。

Tailwind CSS

Tailwind は、優れたデフォルトの色、間隔(spacing)、その他のプリミティブな形でビルディングブロックを提供することで、見栄えの良いアプリケーションを簡単に作成することができます。 これまで開発ではスタイルを当てたいタグに対してクラス名を定義することでスタイルを定義していましたが、最近のコンポーネント指向な UI 設計においてはもっと柔軟にスタイルを当てる事が期待され、 Tailwind はそれを可能にします。
また、コンポーネントライブラリとは異なり独自のスタイルを作ろうとした場合にも足かせとならず、解決しようとしている問題には直接関係しないことがらには影響を与えないため扱いやすいです。

NextAuth.js

Next.js で認証システムを導入したいとき、NextAuth.js は複雑なセキュリティ条件下に導入できる優れたソリューションであるためです。

Desk Booking App

ここでは何を作ったのか、どうやって作ったのかを紹介します。

私が T3 Stack を作ったものはオフィスのデスクの予約ツールです。 Belong 成長に伴い社員も増えており、赤坂本社は各部門のメンバー全員が集まるとデスクの確保が難しくなっています。 私達はリモートと出社のハイブリッドの勤務形態を取っているので出社をする人数を調整したらよく、 部門間で出社日のローテーションを決めて調整をしたりスプレッドシートを用いた管理を行うという選択肢もありましたが、 やはりエンジニアであれば仕組みを用いて課題を解決したいという気持ちがあります。
私はこれをしばらく気になっていた技術を使う絶好の機会だと考え、とにかく尖った技術スタックを用いたサービスの構想を練りました。

Architecture

アーキテクチャとしては以下になります。

t3 gcp architecture

上記で T3 Stack の主要技術として紹介したものの中で NextAuth.js は用いませんでした。 サービスはいつも通り GCP 上に Cloud Run としてデプロイしており、社内のユーザーの利用しか想定していないためユーザーの権限管理には IAP を用いました。

また、データベースに関しては以前から興味があった、SQLite 向けのストリーミング・レプリケーションのツールであり更新内容をオブジェクトストレージ保存ができる、Litestream を採用しました。 Litestream については別の機会に紹介したいと思います。

Development

T3 Stack のアプリケーションの開発は、以下のコマンドを実行し、コマンドプロンプトに答えていくと開発環境が立ち上がります。(参考)

npm create t3-app@latest

インストールが終わると以下のようなディレクトリ構成になります。
私自身は tRPC に加えて Prisma も初めて扱うので多少時間がかかることも覚悟していましたが、 アプリの初期化時点で基本設定は完了しておりサンプルの定義もあるためスムーズに開発が行えました。 T3 Stack は Next.js と TypeScript の理解があれば開発は簡単に始めることができます。

.
├─ public
│  └─ favicon.ico
├─ prisma
│  └─ schema.prisma
├─ src
│  ├─ env.mjs
│  ├─ pages
│  │  ├─ _app.tsx
│  │  ├─ api
│  │  │  └─ trpc
│  │  │     └─ [trpc].ts
│  │  └─ index.tsx
│  ├─ server
│  │  ├─ db.ts
│  │  └─ api
│  │     ├─ routers
│  │     │  └─ example.ts
│  │     ├─ trpc.ts
│  │     └─ root.ts
│  ├─ styles
│  │  └─ globals.css
│  └─ utils
│     └─ api.ts
├─ .env
├─ .env.example
├─ .eslintrc.cjs
├─ .gitignore
├─ next-env.d.ts
├─ next.config.mjs
├─ package.json
├─ postcss.config.cjs
├─ prettier.config.cjs
├─ README.md
├─ tailwind.config.ts
└─ tsconfig.json

T3 Stack を扱う場合、やはり気になるのは Bleed Responsibly としても言及される tRPC だと思います。
tRPC では API を定義すると、その API に対してクライアント側で型定義が生成されるため、補完が効いて開発がしやすくなります。 また、API の定義時に zod で型定義を行うので API のリクエストのバリデーションも行うことができます。 この特徴に関してはやはり開発しやすく感じました。ミスが置きないですし、モデルの冗長な管理なども必要なくなります。

また、React での利用にも向いており、 React Query と同等の使い心地で、型安全にリクエストを行うことができます。(参照)

tRPC ではデータの取得をする場合は useQuery (.query()) を用いてデータの取得が可能です。

import { trpc } from '../utils/trpc';
export function MyComponent() {
    const helloWithArgs = trpc.hello.useQuery({text: 'client'});
...
}

データをサーバー側に送りたいときは useMutation (.mutation()) を用いてデータの送信が可能です。

import { trpc } from '../utils/trpc';
export function MyComponent() {
  const mutation = trpc.login.useMutation();
  const handleLogin = () => {
    const name = 'John Doe';
    mutation.mutate({ name });
  };
  ...
}

HTTP RPC Specification によると、query が HTTP の GET メソッド、mutation が HTTP の POST メソッドに対応しているようです。 ただ、更新や削除の場合も mutation を通して処理を行います。 加えて、 tRPC では query と mutation 以外にも subscription というものがあり、これは WebSocket を用いてリアルタイムにデータを取得することができます。 subscription ではクライアント側でのイベントの発生をサーバー側へ通知し、 サーバーはイベントサブスクライバに対しでデータを送る事仕組みを型安全に定義出来ます。

使ってみて現状の難点はやはり情報が少ないことで、基本的には公式ドキュメントのサンプルを参考に開発を行ったのですがやはり情報が限られている感は感じました。 私が普段触る Go であればクライアントライブラリの実装を読みに行ったりするのですが、TypeScript だとそれが出来ないケースも多いので詰まったらどうするかを考えておく必要があります。

Conclusion

本記事では T3 Stack について紹介しました。 T3 Stack は TypeScript と Next.js を用いて tRPC や Prisma などのライブラリを用いているものを指します。 普段扱わない技術要素も含まれているかもしれませんが、Next.js と TypeScript の理解があれば開発は始めることができます。 また、tRPC の API を行うとクライアントでも型安全に呼び出す事が出来る点は特に開発がしやすいと感じました。Belong でも引き続き研究を行い、今後の開発に活かしていきたいと思います。

最後に、Belong では一緒にサービスを育てる仲間を募集しています。 もし弊社に興味を持っていただけたらエンジニアリングチーム紹介ページをご覧いただけたら幸いです。