44
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

WEBアプリ開発初心者が最近話題の「T3 Stack」について調べてみた

Last updated at Posted at 2023-01-29

1.はじめに

フロントエンドのお勉強をしたいと思い、流行っているものを調べたところ「T3 Stack」というワードを発見。
公式のgithubのstarは13kであり、その人気がうかがえる。

そこで今回は、フロントエンド初心者がT3 Stackについて手を動かしながら学んでいきたいと思う。

2.T3 Stackとは

昨今のWeb開発では、Typescriptによる型安全なWebアプリの開発が求められている。
また、フロントエンド、BFF、バックエンドによる構成においていかに型安全で効率よく開発するかは非常に重要な課題である。ここで、Web初心者の私は、「BFFとは?」となったのでBFFについても調べた。

BFFとは

BFF(Backend For Frontend)とは、クライアント(フロントエンド)とバックエンドの中間に位置し、双方の複雑性を吸収するために作られたサーバである。なぜ双方が複雑になったかというと、クライアントの端末の種類が増加したことに伴うロジックの複雑化である。例えば、PC版ブラウザ、スマホ版ブラウザ、デスクトップアプリ、スマホアプリなどクライアントの種類は増えた。それぞれにメッセージやコンテンツの出し分けを実装しようとすると、クライアント側の実装が複雑化する。

BFF.png

このような背景から、複数のバックエンドから情報を取得し適切な形に整形するなどのロジックを持つサーバをクライアントとバックエンドの間に位置することで、クライアントはUI/UXの画面部分に注力することを可能にしたのがBFFである。またBFF登場前は、クライアントとバックエンド間の通信プロトコルは一致させないといけなかったが、BFFを挟むことで、クライアント<->BFF、BFF<->バックエンドで別のプロトコルを使用することが可能になった。

T3 Stackを構成する6つの技術

話は少し逸れたが、フロントエンド、BFF、バックエンドにおいて型安全で効率よく開発するために注目を集めているのが「T3 Stack」という技術スタックである。

T3 Stackにおいては、以下の3つの思想に焦点が当てられている。

  • simplicity:可読性・拡張性が高い
  • moduliarity:コンポーネント化しやすい
  • full-stack typesafety:アプリケーション全体で型の一貫性を保持

そして、これらの思想を実現するためにT3 Stackでは以下の6つの技術を採用している。

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

フロントエンド初心者の私は、これらの技術は名前を聞いたことがある程度なので1つずつ調べようと思う。

①Next.js

Next.jsとは、Reactをベースとしたフロントエンドフレームワークである。Reactの機能に加えて、RouterやSSR、SSG、ISR、画像最適化などの機能が提供され、Webアプリの開発が大幅に改善された。
ついでに、MPA、SPA、SSR、SSG、ISRについて調べてみた。

MPA

MPA(Multi Page Application)とは、Railsなどに代表されるバックエンド側のフレームワークのみを利用する形式で、従来よく使われてた。
クライアントからHTTPリクエストを受け取ると、バックエンドのWebサーバは内部でHTMLを組み上げ、JavaScriptやCSSと一緒にHTTPレスポンスとしてクライアントへ返す。この処理をページが遷移するごとに毎回行う。そのため、ユーザの待ち時間は長くなるというデメリットがある。この問題を解消・緩和するために登場したのがSPA。

MPA.png

SPA

SPA(Single Page Application)とは、ホスティングサーバから取得したページを基軸にし、表示したいものがあればその差分を都度APIから取得してくるアーキテクチャである。つまり、MPAとは異なり、ページを取得するのは初回のみで、2回目以降はAPIからデータを受け取ることで差分箇所のみを置き換える方法である。SPAは、MPAの課題であるページ遷移に時間がかかる点を解消してくれる。しかし、APIを叩いてコンテンツを取得しにいく部分には多少時間がかかる点も課題としてある。それを解消・緩和するのがSSRである。

SPA.png

SSR

SSR(Server Side Rendering)とは、ページ遷移の度にサーバにリクエストを送り、そのままサーバ側でAPIと連携してレンダリングが行われ、生成されたHTMLを返すアーキテクチャ。SPAではクライアント側で行っていたレンダリングをサーバ側で行うので、サーバサイドをJavaScriptで表現できるNode.jsがのったサーバが必要になる。

SSR.png

SSG

SSG(Static Site Generation)とは、ビルド時にサーバ側でAPIを叩いてデータを取得し、あらかじめHTMLを作成しておき、クライアントからリクエストが飛んできた際にHTMLを返すアーキテクチャ。SSRはリクエスト時にデータを取得するのに対し、SSGはビルド時に行う点がポイント。そのため、両者はよく比較される。SSGのメリットは、初回のロードがないためユーザがすぐにページを見れる点である。一方でデメリットは、常に最新の情報を反映できるわけではない点である。

SSG.png

ISR

SSGは表示速度は早いが、内容を更新できないというデメリットがある。それを解消するのがISR(Incremental Static Regeneration)である。SSRとSSGのハイブリット型とも言える。ISRでは、SSRのようにリクエストに応じてHTMLを作成するが、生成はSSGのようにバックエンドで事前に作成しつつ、既に生成されている1つ前のバージョンのHTMLを返す。そのため、表示される内容は少し古いが、更新はされ続ける。

ISR.png

②tRPC

ここでは、RPC、gRPC、tRPCの順に説明していこうと思う。

RPC

gRPCやtRPCに含まれているRPCってそもそもなんやねんと思ったので調べてみた。
RPC(Remote Procedure Call)とは、ネットワークで繋がっている別のコンピュータのプログラムを実行できるようにする技術のこと。
クライアントサイドがサーバサイドのメソッドを実行するときに、具体的な通信手段やプロトコルについて自分で実装する必要がない点が特徴。
一方で、普段行っているローカルでのメソッドの呼び出しはLPC(Local Procedure Call)と言う。

gRPC

gRPCは、GoogleがRPCを実現・実装しやすくするために開発した手法。
まず、.protoというエンドポイントを定義するファイルを作成し、それをもとにクライアントサイドとサーバサイドのコードを自動生成する。
生成されたコードには、HTTP/2で通信するコードが全て書かれており、生成されたメソッドを呼ぶだけでクライアントから違うサーバの処理を呼び出すことができる。また、通信時のシリアライズにはProtocal Buffersというフォーマットが使用される。

tRPC

tRPCは、TypescriptでRPCを実現するもので、クライアントサイドもサーバサイドもTypescriptで実装する。つまり、TypescriptでgRPCを実装したものがtRPCである。tRPCの素晴らしいポイントは、クライアントとサーバー間で型を直接共有できるところである。
フロントからREST APIなどを呼び出すことでバックとやり取りさせようとした時に、OpenAPIによるAPIドキュメントを用いて開発を進めることはしばしあると思う。その際に、APIドキュメントが正しくメンテナンスされていないと、疎通に失敗することがあるかと思う。(私は経験済みです...。)
このような問題を解決してくれるのがtRPC!

③Tailwind CSS

CSSフレームワークの一種で、ユーティリティファーストを謳っており、たくさん用意されたクラスを組み合わせて使用していくフレームワーク。
CSSを書かずに済んだり、クラス名を考えなくて済んだりとメリットがある反面、クラスを探すのが面倒だったり、指定するクラスが多くなるというデメリットもある。

④TypeScript

説明不要かもしれませんが、TypeScriptは、JavaScriptを拡張して開発されたプログラミング言語。TypeScriptで書かれたコードはコンパイルするとJavaScriptのコードへと変換されるため、JavaScriptの開発・実行環境があればすぐに使用可能。
では、JavaScriptとの違いは何でしょうか。それは、型付けの仕方にある。JavaScriptは、実行時にデータ型を決める動的型付けであり、実行するまでエラーの存在に気づきにくく、バグが起こりやすいという問題点があった。
この問題を解決するために、TypeScriptではあらかじめデータの型を定義する静的型付けを採用している。

⑤Prisma

Prismaとは、オープンソースのORM(Object Relational Mapping)。ORMとは、RDBに対するデータ操作をオブジェクト指向的に扱えるようにした手法。ORMを使うと、DBへの操作をSQLではなくクラスで定義されたメソッドで行うことが可能になる。
Node.js上のアプリケーションで直接DBに接続し、クエリー発行が可能なため、Next.js(Node.js上で構築されている)アプリケーションでDBを扱う際に特に有用とされている。Schemaファイルから型情報が生成されるため、クエリ結果が型安全になる。

⑤NextAuth.js

NextAuthとは、Next.jsやSeverlessアプリのためのオープンソース認証ライブラリのこと。OAuthだけでなく、SNSでの認証も可能なので柔軟な設計ができる点が特徴。

3.実装方法

ここでは、T3 Stackを用いたWEBアプリの開発方法について紹介していく。
開発は大きく以下の項目で構成されている。

  • 雛形作成
  • NextAuthで認証機能実装
  • tRPC実装(serverサイド)
  • tRPC実装(clientサイド)
  • UI実装

雛形作成

ここでは、経験豊富なT3 Stack開発者らによって作られたCLIツールであるcreate-t3-appを使用して、T3 Stackの雛形を作成する。
冒頭でも紹介しましたが、create-t3-appのgithubのstarは13kを超えており、T3 Stackのデファクトスタンダートになっているみたい。

雛形の作成方法は簡単で、以下のようにコマンドを叩くだけでいい。

npm

$ npm create t3-app@latest

yarn

$ yarn create t3-app

pnpm

$ pnpm dlx create-t3-app@latest

npx

$ npx create-t3-app@latest

コマンドを叩くと、以下の文字が表示され、設定における質問が飛んでくる。

$ npx create-t3-app@latest
   ___ ___ ___   __ _____ ___   _____ ____    __   ___ ___
  / __| _ \ __| /  \_   _| __| |_   _|__ /   /  \ | _ \ _ \
 | (__|   / _| / /\ \| | | _|    | |  |_ \  / /\ \|  _/  _/
  \___|_|_\___|_/‾‾\_\_| |___|   |_| |___/ /_/‾‾\_\_| |_|

まず、プロジェクト名をどうするか聞かれます。デフォルトでは「my-t3-app」になっていますが、ここでは「t3-stack-app」とした。

? What will your project be called? t3-stack-app

次は、TypeScriptとJavaScriptどちらを使うか聞かれます。静的型付けを行いたいのでTypeScript一択。

? Will you be using TypeScript or JavaScript? TypeScript
Good choice! Using TypeScript!

ちなみに、JavaScriptを選ぶと不正解だと怒られるw
TypeScript推しが伺えますねw

? Will you be using TypeScript or JavaScript? JavaScript
Wrong answer, using TypeScript instead...

次は、インストールしたいパッケージを選択する質問がきます。T3 Stackに則ると全て有効化すべきだと思うので、ここでは全て有効化した。

? Which packages would you like to enable? (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ◉ nextAuth
 ◉ prisma
 ◉ tailwind
❯◉ trpc

次は、gitリポジトリとして初期化するか聞かれる。今回は初期化する。

? Initialize a new git repository? Yes
Nice one! Initializing repository!

最後の質問は、どのツールでcreate-t3-appのコマンドを叩いたかによって内容が変わる。
今回はnpxで実行したので、npmでパッケージをインストールするか聞かれる。
最後の質問はどちらでもいいような内容のものなので、基本的にyesでいいと思う。

? Would you like us to run 'npm install'? Yes
Alright. We'll install the dependencies for you!

質問が終わると、アプリの雛形が生成される。

Using: npm

✔ t3-stack-app scaffolded successfully!

Adding boilerplate...
✔ Successfully setup boilerplate for nextAuth
✔ Successfully setup boilerplate for prisma
✔ Successfully setup boilerplate for tailwind
✔ Successfully setup boilerplate for trpc
✔ Successfully setup boilerplate for envVariables

Initializing Git...
✔ Successfully initialized and staged git

Installing dependencies...
✔ Successfully installed dependencies!

Next steps:
  cd t3-stack-app
  npx prisma db push
  npm run dev

次は、「Next steps」に書かれているコマンドを順に実行していく。
以下のコマンドは、Prisma DBのマイグレーションファイルを生成せずに同期することができるようにするもの。

$ npx prisma db push

以下のコマンドで、アプリを起動する。

$ npm run dev

アプリの起動に成功すると、以下の画面が表示される。

init.png

また、雛形のディレクトリ構成は以下の通り。

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

1838 directories, 15195 files

NextAuthで認証機能実装

ここでは、NextAuthで認証機能を実装していく。
NextAuth Providerでは、GitHubやGoogleなどが提供されており、今回はGithubの認証を実装する。

NextAuthを実現するためには、雛形ですでに作成されている.envファイルにGithubのアカウント情報と、NextAuthに任意のsecretを記述するだけでいい。

.env
# Next Auth
# You can generate the secret via 'openssl rand -base64 32' on Linux
# More info: https://next-auth.js.org/configuration/options#secret
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000

# Next Auth Github Provider
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

また、.envファイルにはデフォルでDiscodeのアカウント情報を入力するように定義されているので、Discodeの環境変数をGithubに変更する必要がある。

src/pages/api/auth/[...nextauth].ts
export const authOptions: NextAuthOptions = {
  // Include user.id on session
  callbacks: {
    session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
      }
      return session;
    },
  },
  // Configure one or more authentication providers
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: env.GITHUB_CLIENT_ID,
      clientSecret: env.GITHUB_CLIENT_SECRET,
    }),
    // ...add more providers here
  ],
};

tRPC

続いてRPCの実装をしていく。RPCについては前述していますが、主に呼び出し元のクライアント側と、呼び出し先のサーバーサイド側の実装が必要になる。

サーバーサイド

ここでは、サーバーサイド側の実装について説明する。
実装手順は以下の通りです。

  • スキーマ定義
  • Router作成(メソッド定義)
  • Router追加

スキーマ定義

まずは、スキーマの定義を行う。
src配下にschemaというフォルダを作成し、その中に定義ファイルを配置する。
ここでは、TypeScriptのバリデーションライブラリであるzodを使用してスキーマの定義を行っている。
ここで定義したスキーマをクライアント側とサーバー側で利用するため、型安全なWEBアプリ開発ができるのです。

src/schema/todo.ts
import z from 'zod'

export const createTaskSchema = z.object({
  taskName: z.string().max(20),
})

export type CreateTaskInput = z.TypeOf<typeof createTaskSchema>

Router作成(メソッド定義)

次hに、Routerの作成及びメソッドの定義を行う。
「src/server/trpc/router」配下に各Routerを定義したファイルを配置する。
以下は、todoRouterを作成し、createTaskメソッドを定義した例。

src/server/trpc/router/todo.ts
import {
  createTaskSchema,
} from "../../../schema/todo";
import { t, authedProcedure } from "../trpc";

export const todoRouter = t.router({
  createTask: authedProcedure
    .input(createTaskSchema)
    .mutation(async ({ ctx, input }) => {
      const task = await ctx.prisma.task.create({
        data: {
          ...input,
          user: {
            connect: {
              id: ctx.session?.user?.id,
            },
          },
        },
      });
      return task;
    }),
});

Router追加

最後に、作成したRouterのルーティングを行う。
やることはすごく簡単で、「src/server/trpc/router/index.ts」のappRouterに「パス名:Router名」を追記するだけ。

src/server/trpc/router/index.ts
import { t } from "../trpc";
import { todoRouter } from "./todo";

export const appRouter = t.router({ 
    todo: todoRouter 
});

// export type definition of API
export type AppRouter = typeof appRouter;

クライアントサイド

ここでは、クライアントサイドの実装について説明していく。

  • カスタムhook定義

カスタムhook定義

  • src/hooks/usexxxx.ts
    tRPCのserver側で定義したメソッドをclient側からRPCで呼び出せるようにカスタムhookを定義する。
    カスタムhookとは、複数のコンポーネントの中に存在する共通の処理を切り出して作成した関数。
    汎用的なカスタムフックを作成することができれば、React, Next.jsを問わず別のアプリケーションでも再利用することが可能になる。

以下は、server側で定義したcreateTask関数をclient側から呼び出す処理の実装部分になる。

src/hooks/usexxxx.ts
import useStore from '../store'
import { trpc } from '../utils/trpc'

export const useMutateTask = () => {
  const utils = trpc.useContext()
  const reset = useStore((state) => state.resetEditedTask)

  const createTaskMutation = trpc.todo.createTask.useMutation({
    onSuccess: (res) => {
      const previousTodos = utils.todo.getTasks.getData()
      if (previousTodos) {
        utils.todo.getTasks.setData([res, ...previousTodos])
      }
      reset()
    },
  })
  return { createTaskMutation }
}

UI

最後は、UIの実装。
src配下にcomponentsというフォルダを作成し、このフォルダ配下にファイルを配置する。
以下は、Githubの認証画面のレイアウトの実装。

src/components/Auth.tsx
import { signIn } from 'next-auth/react'

export const Auth = () => {
  return (
    <div>
      <button
        className="rounded bg-blue-600 py-2 px-4 font-bold text-white hover:bg-blue-800"
        onClick={() => signIn('github')}
      >
        GitHub Auth
      </button>
    </div>
  )
}

Login.png

以下は、タスクを作成する部分のUI実装。
テキストフォームにタスクタイトルを入力して、「Create」ボタンを押すとcreateTaskMutationを実行してタスクを生成する。

src/components/TaskForm.tsx
import { FormEvent } from 'react'
import useStore from '../store'
import { useMutateTask } from '../hooks/useMutateTask'

export const TaskForm = () => {
  const { createTaskMutation} = useMutateTask()
  const { editedTask } = useStore()
  const update = useStore((state) => state.updateEditedTask)

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    createTaskMutation.mutate({
      title: editedTask.title,
    })
  }

  return (
    <form onSubmit={handleSubmit} className='text-center'>
      <input
        type="text"
        className="border bg-gray-200 focus:bg-white px-3 py-2"
        placeholder="Title"
        value={editedTask.title || ''}
        onChange={(e) => update({ ...editedTask, title: e.target.value })}
      />
      <br></br>
      <button className="rounded bg-green-500 px-3 py-2 text-white hover:bg-indigo-700 focus:outline-none">
        { 'Create' }
      </button>
    </form>
  )
}

Creating.png

4.おわりに

この記事では、「T3 Stack」という最近話題のWEBアプリ開発における技術スタックについて紹介しました。
T3 Stackの良いところは、

  • TypeScriptやtRPCベースなので型安全
  • フロントとバックともにNext.jsで完結している
  • 雛形を簡単に作成できるCLIツールが存在する
    などが挙げられるかなと思う。

しかし、フロントとバック両方TypeScriptである必要があるので、

  • 処理速度の問題で、Node.jsではなくGoやRustの方がいい時
  • GraphQLじゃないといけないくらいアプリの構造が複雑
  • フロントとバックの開発者が違い、APIドキュメントが必要な時
    など使用場面は見極める必要はありそう。

ですが、型安全で効率的に実装できるのでこれからもっと流行るかも?しれない。

参考

44
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?