じぶん対策

日々学んだことをアウトプットして備忘録にしています。

2024年末の今、Rustに入門してみる

はじめに

今回はRustへの入門記事です。

2024年の抱負として、技術書を読んだり、個人開発を習慣化したりといったことを目標にしていました。

2024年の抱負

実際、今年は技術書を読んだり、個人開発をしてはいるんですが、習慣化という点ではまだまだです。

これらをやりたい背景として、自分コンフォートゾーンをガンガン抜け出したいんだなと気づいたので、以前から気になっていたRustに挑戦してみることにしました。

個人的にRustに興味がある理由としては、以下の点が挙げられます。

  • PHP、TypeScriptを中心に業務をしているので、CLIツールを作るのが少し面倒
  • 純粋な静的型付け言語に興味がある
  • 言語設計の良さを聞く機会が多いので、設計面での学びがあるかもしれない
  • バイナリとして実行されるのでコンテナ技術との相性が良さそう

ほかにもGo言語も候補にあがっていましたが、現時点での評価としては簡単に学習できつつも設計の悪さを感じる記事を多く目にしました。簡単に学習できること自体はPHP等も同じだと思っているので、優先順位は設計などを学べそうだったり、最終的な安全性が高いRustが上でした。Go自体もあまり触ったことがないので、将来的には触ってみたいと思っています。

また、Rustは私がWEBエンジニアになった2022年から注目されている言語で、その後も注目度が高まっている印象があります。

ただ、当時は学習コストが高い印象があり、軽く触れてはいたものの挫折していました。

2024年時点で、JetBrainsがRustのIDEであるRustRoverをリリースしたり、Rust関連の書籍が増えたりと、学習環境が整ってきている印象があります。

また、ChatGPTなどプログラミング学習に使えるAI等周辺環境も良くなりつつあるので、再度挑戦してみることにしました。

業務で使いたいなと思うユースケースとしては、下記のようなものがあります。

  • LambdaのカスタムランタイムでRustを使い、高速な処理を実現する
    • コストに直結するので、高速な処理ができると嬉しい
  • CLIツールを作成する
    • PHPやTypeScriptで作成するのがやや大変
  • 基盤周りなど、コード量が多くなくても、アクセス数が多かったり、コンテナ技術との相性が求められるもの

参考資料

書籍も購入しました

基礎知識

Rustとは

Rustは、Mozillaが開発したシステムプログラミング言語です。
現在はRust Foundationが開発をしているOSSです。
このRust Foundationは、AWSGoogleHUAWEIMicrosoftMozilla等によって設立された非営利団体です。

システムプログラミング言語が何かわからなかったんですが、他の言語との区別として、下記のようなポイントがあるようです。

  • システムプログラミング言語はシステムプログラミングのための言語で、CやC++が有名
  • システムプログラミングとは、OSやデバイスドライバ、組み込みシステムなどの低レベルなプログラミングを指す
  • アプリケーション向けの言語との違いは、物理的なハードウェアへのより直接的なアクセス手段を提供していること

参考: システムプログラミング言語 - Wikipedia

ざっくりよく知られている特徴をあげておきます。

  • メモリ安全性を保証する
    • Rustは所有権システム(Ownership)という仕組みがあり、メモリ管理のエラーをコンパイル時点で検出できる
  • 高パフォーマンス
    • C、C++に匹敵するパフォーマンスを持つ
  • 並行性のサポート
    • 安全なスレッド間の共有を保証する

チュートリアル

The Rust Bookにチュートリアルがあるので、ざっくり必要な部分だけを抜粋してみます。

インストール

Linux/MacOSの場合

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Cargoを使ったプロジェクトの作成

Heloo, World!自体はもっとシンプルにできるんですが、Rustを使う場合は基本的に公式ビルドシステムであるCargoを使います。

cargo new [プロジェクト名]
cd [プロジェクト名]
cargo run

セットアップした時点でHello, World!プログラムが作成されています。
cargo runでコンパイルと実行がされるので、Hello, World!と表示されるはずです。

また、セットアップした時点でCargo.tomlというファイルが作成されており、これがnpmでいうpackage.jsonのように依存関係の管理やプロジェクトの設定を行うことができるものです。

全体的な雰囲気を掴みたい場合はThe Rust Bookの数当てゲームのプログラミングの章が良さそうです。

書き方でおさえておくところ

変数

変数はデフォルトでイミュータブルになります。
再代入ができないので、他の言語とはやや扱いが異なります。
Rustの文脈では代入ではなく「束縛」という言葉を使うことが多いようです。(bindの訳かな)

let x = 0;
x = 1; // error[E0384]: cannot assign twice to immutable variable `x`

イミュータブルな変数を作成する場合は、明示的にmutキーワードを使います。

let mut x = 0;
x = 1; // OK

データ型

Rustにおける値は全てデータ型を持っています。

まずはスカラー型。C言語と同じようにbit数によって分かれています。

  • スカラー

    • 整数型
      • i8, i16, i32, i64, isize
      • u8, u16, u32, u64, usize
        • iは符号あり、uは符号なしでそれぞれビット数を表す
        • isize, usizeはアーキテクチャに依存する符号あり、符号なし整数型
    • 浮動小数点型
      • f32, f64
  • 論理値型

    • true
    • false
  • 文字型

    • char型
      • シングルクォートで囲む
      • Unicodeスカラー値を表す
      • 文字列型ではなく、文字型
      • String型は言語ではなく、標準ライブラリで提供される。
      • 結構ややこしい概念なので、The Rust Bookを参照
  • 複合型

    • 複数の値を一つの型にまとめることができる。以下種類
      • タプル
        • 複数の方の値を一つの型にまとめる
        • タプルの位置ごとに異なる型を持つことができる
        • タプルの要素にはドット記法でアクセスできる
          • let tup: (i32, f64, u8) = (500, 6.4, 1);
          • let five_hundred = tup.0;
      • 配列
        • 配列は全要素同じ型でなければならない
        • 固定長であり、一度宣言されたら要素数を変更することはできない
      • ベクタ
        • 配列と異なり、要素数を変更できる
        • ベクタは標準ライブラリで提供される

関数

関数はfnキーワードで定義します。

fn another_function(x: i32) {
    println!("The value of x is: {}", x);   // xの値は{}です
}

仮引数には型を指定する必要があります。

また、関数はそれを呼び出したコードに値を返すことができます。

fn five() -> i32 {
    5
}

関数本体ブロックの最後の式の値がその関数の戻り値になります。returnキーワードは早期リターンする場合に使います。

関数の最後の式が戻り値になるので、セミコロンをつけてしまうと文になり、エラーになります

さいごに

Rustの基礎をざっくりと抑えてみました。
今回の内容はまだ言語の基礎的な部分でRustらしい部分は出てきていませんが、学習を進めながらまた記事にしていきたいと思います。

データベースのマイグレーション時にインデックスを削除する場合に注意すべき外部キー制約とインデックスの依存関係

はじめに

今回は業務のなかで少しつまづいたポイントについて自分の備忘録としてまとめたいと思います。

MySQLにおける外部キー制約作成時の既存インデックスの利用についてです。

対象システム

TL;DR

  • 特定のインデックスを削除する場合、特定の外部キー制約がインデックスを必要としている場合にエラーになる
  • 外部キー制約の作成時には自動的にインデックスが作成される
  • 外部キー制約の作成時に、既存のインデックスが存在する場合はインデックスの作成をせず、既存のインデックスを利用する。条件は以下の通り
    • 外部キーとして利用するカラムが、既存のインデックスの1つ目のカラムと一致している場合
      • これは複合インデックスの場合でも最初のカラムであれば利用される

背景

担当しているプロジェクトにおいて、開発した機能をステージング環境にデプロイする際に、マイグレーションファイルを実行するとエラーが発生しました。

SQLSTATE[HY000]: General error: 1553 Cannot drop index 'unique_index_name': needed in a foreign key constraint (Connection: mysql, SQL: alter table `table_name` drop index `unique_index_name`)

このエラー自体は、特定のインデックスを削除しようとした際にそのインデックスが外部キー制約によって必要とされているために発生します。

つまり、ステージング環境には複合外部キー制約Aと複合外部キー制約B、ユニークインデックスBが存在していました。

この複合外部キー制約BとユニークインデックスBは同時に作られたものです。
複合外部キー制約Bの作成時には自動的にユニークインデックスBが作成されていました。
スキーマを確認し、複合外部キー制約Bを消してからユニークインデックスBを削除する必要があると判断しました。

しかし、実際には外部キー制約Bが存在しない状態で、外部キー制約Aのみが存在する状態でユニークインデックスBを削除しようとするとエラーが発生しました。

外部キー制約AもユニークインデックスBに依存していることが原因でした。

仕様

試行錯誤の結果たどり着いた仮説でしたが、MySQLにおいては外部キー制約の作成時に既存のインデックスを利用する仕様がありました。

MySQLInnoDBストレージエンジンにおいて、外部キー制約を設定する際に、参照元テーブルのカラムに対してインデックスが必要です。
このインデックスは自動的に作成されますが、特定の条件下では既存のインデックスを利用することがあります。
今回のケースではこの仕様を正しく理解できておらず、一見無関係に見える外部キー制約Aを考慮していないことが原因でした。

特に、複合ユニークインデックスの場合に気付きにくい仕様です。

複合ユニークインデックスとは

複合ユニークインデックスとは、複数のカラムに対してユニーク制約を持たせるインデックスのことです。

複数のカラムに対してユニークな組み合わせを保証します。例えば、(A, B)というインデックスはカラムAとカラムBの組み合わせがユニークであることを保証します。

MySQLが既存の複合ユニークインデックスを外部キー制約に再利用するための条件は次のようになります。

  • 外部キー列がインデックスの先頭列であること
    • 外部キーとして指定する列が、既存のインデックスの先頭(最も左側)に位置している必要があります。つまり、外部キー列がインデックスの最初のカラムである場合、そのインデックスを外部キー制約に利用できます。

公式ドキュメント内の記述は以下の通りです。

MySQL では、外部キーチェックを高速に実行でき、かつテーブルスキャンが必要なくなるように、外部キーおよび参照されるキーに関するインデックスが必要です。 参照しているテーブルには、外部キーカラムが同じ順序で最初のカラムとしてリストされているインデックスが存在する必要があります。 このようなインデックスが存在しない場合は、参照しているテーブル上に自動的に作成されます。 外部キー制約の施行に使用できる別のインデックスを作成した場合、このインデックスは後で暗黙的に削除される可能性があります。index_name が指定されている場合は、前述のように使用されます。

例: 外部キー列が既存の複合インデックスの先頭にある場合

CREATE TABLE parent_table (
    id BIGINT UNSIGNED PRIMARY KEY,
    name VARCHAR(255)
);

CREATE TABLE child_table (
    child_id BIGINT UNSIGNED PRIMARY KEY,
    parent_id BIGINT UNSIGNED,
    other_column VARCHAR(255),
    UNIQUE KEY unique_parent_other (parent_id, other_column),
    FOREIGN KEY (parent_id) REFERENCES parent_table(id)
);

この場合、child_table の parent_id 列は既に (parent_id, other_column) という複合ユニークインデックスの先頭に位置しています。したがって、MySQLはこの既存のインデックスを外部キー制約のために再利用します。新たなインデックスを作成する必要はありません。

一見関係のないカラムの外部キー制約を削除しなければならない理由

今回の事象で発生した「一見関係のないカラムの外部キー制約を削除しなければならない」という問題は、インデックスの依存関係と外部キー制約の共有によるものです。具体的には、以下のような状況が考えられます:

  • 複合インデックスの共有利用
    複数の外部キー制約が同じ複合インデックスを共有している場合、インデックスを削除する前に、すべての外部キー制約を削除する必要があります。これは、インデックスが依然として外部キー制約によって使用されているためです。

  • インデックスの再利用による依存関係
    外部キー制約が複数の列に対して設定されている場合、これらの制約が同じインデックスを利用している可能性があります。特に、外部キー制約が複数の列を対象としている場合、それぞれの制約が同じ複合インデックスを使用することがあります。

まとめ

データベースマイグレーションにおいて、外部キー制約とインデックスの依存関係の理解は非常に重要です。 今回のように複合ユニークインデックスを使用する場合、外部キー制約がインデックスの先頭列に依存しているかどうかを確認する必要があります。

  • 外部キー制約を設定する前に、外部キー列がインデックスの先頭に位置している場合は既存のインデックスに依存する
  • インデックスを削除する際は、そのインデックスに依存するすべての外部キー制約を事前に削除する。

2024年にReactを学ぶ人のための資料

はじめに

2024年現在、WebフロントエンドはReactが主流となっています。
LaravelやRuby on Railsのようなフルスタックフレームワークの流行を過ぎ、バックエンドとフロントエンドを分離することが一般的になりました。
今回は、すでにjQueryやLaravel、Ruby on Railsなどを触ったことがある人を対象に、Reactを学ぶための資料をまとめてみました。
個人的に今のフロントエンドの流行を押さえつつ、プロダクトでの採用を考えた際に十分メリットを享受して採用できるレベルの技術を中心にまとめています。 Reactに限らず採用できる技術についても軽く触れるので、フロントエンドやTypeScriptに興味がある人にも参考になるかと思います。

Webフロントエンドの歴史については以前の記事で整理していますので、興味がある方は参考にしてください。

フロントエンド入門 フロントエンドの歴史

Reactとは

Facebookが開発したJavaScriptライブラリで、現在はオープンソースとして公開されています。
主にUIを構築するために使用され、コンポーネント指向のライブラリです。

ほかにもVueやAngularなどのライブラリやフレームワークがありますが、Reactはその中でも特に人気が高いです。
背景としては、Reactはリリースされてからある程度の年月が経過しており、ある程度枯れているため、安定している点や、対抗ライブラリであるVueが2.0から3.0へのアップデートで大幅な変更があり、追従に疲弊した開発者がReactを選択したケースも多いです。

個人的には、Vueも素晴らしい技術で、Reactと比較しても優れている点は多いと思います。
ただ、Reactとのコミュニティの方向性の違いとして、Vue.jsはメジャーバージョンアップで大幅に変更があり、過去の負債を抱えずに理想的な形で進化していくことを目指しているのに対し、Reactはバージョンアップでの大幅な変更は少なく、安定性を重視しているという違いがあります。

技術としてはReactでできることは大抵Vueでもできるので、この記事のスタンスとしてはどちらの技術を採用すべきかという点には言及しません。

参考: React - 2023年度リクルート エンジニアコース新人研修の講義資料です

Reactの特徴

コンポーネント志向

Reactはコンポーネント志向のライブラリです。 コンポーネントはUIの独立した部品であり、再利用可能なコードブロックとして扱うことができます。 これにより、複雑なUIを小さなパーツに分割し、管理しやすくなります。

例: Buttonコンポーネント

import React from 'react';

function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

export default Button;

上記の例では、Buttonというコンポーネントを定義しています。
Reactでは、JSXという記法を使ってコンポーネントを記述します。これは、JavaScript + XMLの略で、HTMLのような記法でコンポーネントを記述することができます。

2024現在のフロントエンドでは、TypeScriptを利用して開発することが主流となっており、JSXとTypeScriptを組み合わせてTSXという記法でコンポーネントを記述することが多いです。

例: Buttonコンポーネント(TypeScript)

import React from 'react';

type ButtonProps = {
  label: string;
  onClick: () => void;
};

const Button = ({ label, onClick }: ButtonProps) => {
  return <button type="button" onClick={onClick}>{label}</button>;
};

export default Button;

データの流れ(単方向データフロー)

Reactでは、ステート(state)とプロップス(props)という概念を使ってデータの流れを管理します。
ステート(State): コンポーネント内部で管理される動的なデータ。ユーザーの操作やAPIからのデータ取得など、変化するデータを管理します。 プロップス(Props): 親コンポーネントから子コンポーネントに渡されるデータ。コンポーネント間でデータを受け渡すために使用します。

Reactでは、Propsを利用して上位の親コンポーネントが下位の子コンポーネントにデータを渡すのが基本的なデータの流れです。

単方向データフローと useStateを組み合わせてみましょう。 useState フックを使ってコンポーネントの状態(state)を管理すると、その状態は変更が必要な特定のコンポーネント内にのみ存在します。状態を持っているコンポーネントが、そのデータを他のコンポーネントに渡すには props を使用します。

import { useState } from 'react';

// 子コンポーネント: 表示とボタンを含む
type CounterDisplayProps = {
  count: number;
  increment: () => void;
};

const CounterDisplay = ({ count, increment }: CounterDisplayProps) => {
  return (
    <div>
      <p>カウント: {count}</p>
      <button type="button" onClick={increment}>
        インクリメント
      </button>
    </div>
  );
};

// 親コンポーネント: 状態を管理し、子コンポーネントに渡す
const CounterContainer = () => {
  const [count, setCount] = useState(0);

  // 子コンポーネントに渡す関数
  const incrementCount = () => setCount(prevCount => prevCount + 1);

  return <CounterDisplay count={count} increment={incrementCount} />;
};

export default CounterContainer;

コンポーネント (CounterContainer) が状態 (count) を管理します。この状態は useState フックを使って定義されています。

コンポーネントは状態を子コンポーネントにpropsを通して渡します。この例では、count と increment 関数が CounterDisplay に props として渡されています。

コンポーネント (CounterDisplay) は渡された count を表示し、increment ボタンのクリックイベントに対応します。このように、状態管理を親コンポーネントに任せることで、子コンポーネントは「状態の表示」と「クリックイベントのハンドリング」という役割に専念できます。

この単方向データフローを利用することで、データの流れがシンプルになり、加えて親コンポーネントがデータを管理し、子コンポーネントがそれを表示するという役割分担が明確になります。このようなパターンは、デザインパターンとしてPresentational and Containerと名前がつけられています。

宣言的UI

Reactは宣言的UIを採用しています。これは、「UIがどうあるべきか」を宣言することで、状態の変化に応じてUIが自動的に更新される仕組みです。 jQueryなどの命令型のUIライブラリ(手動でDOMを操作する方法)と比べて以下のメリットがあります。

  • 可読性の向上: コンポーネントがどのように見えるかを宣言するため、コードが直感的になります。
  • 保守性の向上: 状態管理が一元化され、バグの発生を抑えることができます。
  • 再利用性の向上: コンポーネント志向とも被りますが、コンポーネントを再利用することが容易になります。

このブログ内でも記事を作成していますので参考にしてください。

フロントエンド入門 宣言的UIと命令的UI

Reactを取り巻くエコシステム

Reactはリリースから十分な時間が経ち、エコシステムも充実しています。
以下にReactを取り巻く主なライブラリやツールのなかで、個人的なおすすめを紹介します。

SWR

SWRは先述したNext.jsの開発元であるVercelが開発したデータフェッチングライブラリです。
キャッシュ、リフェッチ、再取得などの機能を提供し、React Queryと並んで人気のあるライブラリです。
React Queryとの違いは、よりシンプルなAPIと、データ取得に特化している点でしょうか。

基本的な考え方はHTTPのキャッシュ戦略であるStale-While-Revalidateを採用しており、キャッシュが古い場合に古いデータを返しつつ、バックグラウンドで新しいデータを取得し、新しいデータが取得できたら古いデータを新しいデータに置き換えるというものです。

基本的な使い方

import useSWR from 'swr';

type User = {
  id: number;
  name: string;
};

const fetcher = (url: string): Promise<User[]> => fetch(url).then((res) => res.json());

function UserList() {
  const { data, error } = useSWR<User[]>('/api/users', fetcher);

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

上記のように、useSWRフックを使用してデータ取得を行います。useSWRは第一引数として、キーを指定します。一般的にはURLのようなかたちでキーを指定することが多いですが、エンドポイントと直接関係はなく、あくまで一意なキーとして扱われます。

const { data, error } = useSWR(userId ? `/api/users/${userId}` : null, fetcher);

のように動的なキーを指定することもできます。

useSWRからはデータの状態であるdataとエラーの状態であるerrorを取得することができます。

Zod

Zodはスキーマベースの型バリデーションライブラリで、TypeScriptとシームレスに統合できます。
Zodを利用すると、オブジェクトや配列に対して、型チェックとバリデーションを同時に行うことができます。

特にバックエンドでTypeScriptを利用するような場合にサーバーとクライアントのデータチェックを統一できるのが特徴で、バックエンドとフロントエンドの整合性を保つ際に便利です。

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  email: z.string().email("有効なメールアドレスを入力してください"),
});

const result = userSchema.safeParse({ name: "", email: "invalid-email" });

if (!result.success) {
  console.log(result.error.errors);
}

上記の例では、userSchemaというスキーマを定義し、nameemailを持っています。
nameは1文字以上であること、emailは有効なメールアドレスであることをバリデーションしています。

safeParseメソッドを使うことで、バリデーションを行い、エラーがある場合はエラーメッセージを取得することができます。

  • resultにはsuccessプロパティが含まれ、バリデーションが成功したかどうかを示します。
  • result.successがfalseの場合、バリデーションエラーが発生していることを意味します。
  • result.error.errorsにはエラー内容が格納されており、どのフィールドがどのような理由で不正であるかが詳細に記述されています。

また、このparseに成功した時点で、スキーマから生成される型に対して推論が効くため、レスポンス等をバリデーションしつつ型付けを行うことができます。

React Hook Form

Reactでフォームを簡単に管理できるライブラリとして、React Hook Formがあります。
シンプルで、パフォーマンスも良いため、Reactでフォームを扱う際にはおすすめです。

React Hook Formでは、useFormという関数を使用してフォームの状態やバリデーションを管理します。

また、先に紹介したZodと組み合わせることで、フォームのバリデーションを型安全に行うことができます。

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const userSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  email: z.string().email("有効なメールアドレスを入力してください"),
});

type UserFormInputs = z.infer<typeof userSchema>;

function UserForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<UserFormInputs>({
    resolver: zodResolver(userSchema),
  });

  const onSubmit = (data: UserFormInputs) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />
      {errors.name && <span>{errors.name.message}</span>}
      
      <input {...register("email")} />
      {errors.email && <span>{errors.email.message}</span>}
      
      <button type="submit">Submit</button>
    </form>
  );
}
  1. バリデーション: 上記の例だと、zodで定義したスキーマをresolverに指定しています。これでzodのスキーマがフォームに適用されます。
  2. register: フォームの入力フィールドを<input {...register("name")} />のように登録します。これにより、フォームの状態が管理されます。
  3. エラーメッセージの表示: バリデーションに失敗した場合、エラー情報がformState.errorsに格納されます。たとえば、nameフィールドが空の場合、errors.nameにエラーが設定され、そのエラーメッセージが{errors.name.message}に表示されます。
  4. handleSubmitは、フォームの送信時にバリデーションを実行し、エラーがなければonSubmit関数にデータを渡します。

その他の特徴としては、React Hook Formは入力ごとに際レンダリングを行わないので、愚直なReactでのフォーム管理よりもパフォーマンスが向上します。

フレームワーク

Reactを利用する際には、フレームワークと合わせて利用することをおすすめします。

Next.jsとの組み合わせ

Next.jsはReactをベースにしたフレームワークで、サーバーサイドレンダリングSSR)、静的サイト生成(SSG)、APIルートのサポートなどの機能を持ちます。

Next.jsはフルスタックフレームワークではありますが、バックエンドとフロントエンドを分離することができるため、バックエンドにLaravelやRuby on Railsといった従来のREST APIを使うこともできます。

主な特徴

  • ファイルベースのルーティング: pagesディレクトリに配置したファイルが自動的にルートとして認識されます。
  • データフェッチング: getServerSidePropsやgetStaticPropsを使用して、サーバーサイドでデータを取得できます。
  • APIルート: pages/apiディレクトリ内にAPIエンドポイントを簡単に作成できます。

Next.js公式ドキュメント

この記事で説明するにはあまりにも広範囲なため、詳細は公式ドキュメントや他のブログ等を参照してください。

個人的な主観としては、2024年時点でReactを使うフレームワークとしては最も人気があるかなと思います。
とはいえ、App Routerによるキャッシュ周りの複雑性やNext.js15での方向の転換などいくつか方向性が変わる可能性があるため、最新の情報を確認することをおすすめします。

Next.jsを利用する場合は、Next.jsの思想に合わせていく覚悟が必要だと感じています。

Remix

RemixはNext.jsと同じくReactをベースにしたフレームワークで、サーバーサイドレンダリングSSR)、静的サイト生成(SSG)、APIルートのサポートなどの機能を持ちます。

Next.jsに迫る勢いで人気が出てきているフレームワークで、Next.jsとは対象的に、Web標準API、つまりMDNに載っているAPIをほとんどラップせずに利用しているのが特徴です。
公式ドキュメントにも、「Web標準と最新のウェブアプリUXに焦点を当てる」というような記載があります。

Remixで取り扱う概念のほとんどは、Webの歴史が作ってきた標準的な概念を利用しているため、フレームワーク独自の概念やAPIを覚える必要がなく、Webの知識がそのまま活かせるというメリットがあります。

過去フレームワーク固有の仕組みに依存して、破壊的変更等によって疲弊した開発者にとっては、Remixは魅力的な選択肢となるかもしれません。

記事執筆時点でのフレームワークの開発の方向性としては、RemixはReact Routerと統合されつつあり、React Server ComponentsベースのRemixの開発が進んでいるようです。

tRPC

tRPCは、TypeScriptのプロジェクトでバックエンドとフロントエンド間の型安全な通信を可能にするライブラリです。

簡単に言ってしまえばHTTP通信のラッパーですが、tRPCを使うと、APIのエンドポイントを定義してからそのレスポンスやリクエストに対して型を設定するのではなく、TypeScriptの型定義を直接共有しながら開発を進めることができます。

入門記事は以前書いたので、興味がある方は参考にしてください。

小中規模の、バックエンドにもTypeScriptを採用するようなプロジェクトでの採用は非常に効果的だと感じています。

ここまで紹介した技術の多くはT3 Stackと呼ばれる開発スタックにも採用され、TypeScriptでフルスタック開発を行う際に人気のある選択肢となっています。

テストの導入

ユニットテストについては、まずはJestかVitestの採用を検討することになるかなと思います。

現状Jestでのテスト環境がない場合はVitestを採用すべきだと思っています。

理由は、JestはJavaScript前提で作成されており、歴史的にもNode.jsの流行やCJSとESMの混在等が問題になっていた時代に作られたライブラリであるため、現代のフロントエンドでESM、TypeScriptを使用したコードに対して、テストを書く場合はVitestの方が困るポイントが少ないと感じています。

VitestでReactのコードに対してテストを書くときの例を示します。

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
import { vi } from 'vitest';

test('ボタンのクリックイベントが発火する', () => {
  const handleClick = vi.fn();
  render(<Button label="Click me" onClick={handleClick} />);
  
  fireEvent.click(screen.getByText(/click me/i));
  
  expect(handleClick).toHaveBeenCalledTimes(1);
});

もちろん、ロジック部分に対してもモック等を利用したりしつつテストを書くことも可能です。

担当しているプロダクトでは、フロントエンドにユニットテストを導入する際にJestを採用するかVitestを採用するかを検討しましたが、Viteに移行 + Vitestの採用の方がJestに比べると辛さが少ないと感じたため、Vitestを採用することにしました。

参考: Vite は使ってないけど Jest を Vitest に移行する

ディレクトリ構造

フロントエンドにおけるディレクトリ構造においては、複雑さと向き合うために、ドメイン駆動設計のエッセンスを取り入れているところが多い印象を受けています。
以前はAtomic Designなどが流行しましたが、必要以上に複雑になることが多いため、最近ではシンプルなディレクトリ構造を採用することが多いです。

ただ、ドメイン駆動設計を採用するとしてもComponentやPageといったフロントエンド固有のものやReactのHooksなどのファイルをどのように扱うかは、プロジェクトによって異なるのかなと思います。

Formatterと静的解析

LinterやFormatterについては、ESlintとPrettierの組み合わせが主流です。最近ではこれらを統合したようなツールとして、Biomeというものが登場しています。

  • ESlint + Prettier

現状のデファクトスタンダードであり、ほとんどのプロジェクトで採用されていると思います。
ただ、設定の競合が発生したり、設定自体が複雑で、設定内容についてはプロジェクトごとに議論が必要となります。

  • Biome

ESlintとPrettierを統合したようなツールで、単一のツールで静的解析とフォーマットを行うことができるため、設定が簡単であったり、パフォーマンスが高いことが特徴です。
ただ、ESlintやPrettierのようにプラグインエコシステムがないため、カスタマイズ性は低くなります。

個人的にはあまり静的解析やフォーマットにこだわりがなく、カスタマイズも最小限に抑えるべきだと考えているため、Biomeを採用することが多いです。

Reactの最新機能とトレンド

ここからは2024年現在のReactの最新機能について紹介します。
どれくらい主流になるかは未知数なので、プロダクトへの本採用を検討する場合は慎重に検討することをおすすめします。

React Server Components

React Server Components(RSC)は、サーバーサイドでコンポーネントレンダリングし、クライアントに必要な部分だけを送信することで、パフォーマンスを最適化する新しいアーキテクチャです。

サーバーコンポーネントを利用すると、以下のようなメリットがあります。

サーバーサイドレンダリングの効率化: RSCは、サーバー上でコンポーネントレンダリングし、クライアントには軽量なJavaScriptとして送信されます。これにより、初期ロード時間が短縮されます。

クライアントとサーバーのコード分離: クライアント専用のコードとサーバー専用のコードを明確に分離できます。また、セキュリティ面でも有利です。

データフェッチングの簡素化: サーバー上でデータを取得し、コンポーネントに直接渡すことができるため、クライアント側でのデータ管理が簡素化されます。

// ServerComponent.server.tsx
import React from 'react';

type User = {
  id: number;
  name: string;
};

const fetchUsers = async (): Promise<User[]> => {
  const response = await fetch('https://api.example.com/users');
  return response.json();
};

const ServerComponent = async () => {
  const users = await fetchUsers();
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

export default ServerComponent;

こうしてみてみると、ややRemixにも似ている気がしますね。

Suspense

Suspenseは、非同期操作(例えばデータフェッチングやコードスプリッティング)を簡潔に扱うための仕組みです。Reactの描画プロセスを一時停止し、必要なデータが揃うまで待機することでロード中の状態を管理します。

非同期データの扱いやすさ: Suspenseを使用することで、データが揃うまでコンポーネントレンダリングを待機させ、ロード中の状態を簡単に管理できます。

コードスプリッティングの簡素化: 大規模なアプリケーションにおいて、必要な部分だけを遅延ロードすることで、初期ロード時間を短縮できます。

import React, { Suspense } from 'react';

const LazyComponent = React.lazy(() => import('./LazyComponent'));

const App = () => (
  <div>
    <h1>Welcome to React</h1>
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  </div>
);

export default App;

まとめ

2024年現在、もっとも主流なフロントエンドのライブラリはReactであると言えます。
その背景には、Reactの安定性やコミュニティの活発さ、豊富なエコシステムなどが挙げられます。
Reactをベースに簡単に、UXの向上やパフォーマンスの最適化を行うためのライブラリやツールも多く存在しており、フロントエンド開発においてReactを採用することは非常に有益であると言えます。

LaravelやRuby on RailsといったフルスタックフレームワークからReactベースに移行する場合にはルーティングやAPIの設計などが課題となることが多いですが、それを差し引いてもReactを採用するメリットは大きいと感じています。

今回の記事での参考資料

プログラミングとカードゲームは実は似ている説

はじめに

今回は完全にポエムです。
SNSなどでよく見かける、「エンジニアに向いている人は〇〇な人だ!」という投稿を見ることがあります。
たとえば、「数学が得意な人はエンジニアに向いている」とか、反対に「実は文系の方がプログラミングに向いている」といったものがあります。
他にも、「コミュニケーション能力がある人」や「集中力がある人」といった、それは大抵の仕事でも必要だろと思われるような特徴を挙げることもあります。

人によるのであまりこの論争自体に興味がないんですが、強いていうなら「カードゲームが好きな人はプログラミングに向いている」という持論があります。

今回は、その持論について書いてみたいと思います。完全にオタクの早口ですが、お付き合いいただけると幸いです。

テレビゲームが好きだったりボードゲームが好きな人がエンジニアには多いと思うんですが、私はカードゲームが好きで、かつあまり他に見ないなと思っていたので書いてみました。

大好きだったカードゲーム

2024年現在で27歳になるんですが、世代的にはデュエルマスターズのドンピシャ世代でした。
遊戯王ポケモンカードゲームは私よりは少し上の世代の人がやっていたイメージがあります。
遊戯王はちょうど5D'sが流行っていた頃ですね。

このデュエルマスターズというカードゲームが、私のプログラミングに対する考え方、と言うより言語能力に多大な影響を与えたと思っています。

この記事では特段デュエルマスターズ特有の概念については触れませんが、ほかのカードゲームと比べるとリソースの管理の仕組みがとても面白く、敗北に近づくにつれて手札リソースが増えたり、リソースなしで呪文を使うシールドトリガーという仕組みがあったりと革命的な要素が多いゲームなので、気になれば調べてみてください。

プログラミングとカードゲームの共通点

ここからは、カードゲームにおける用語をいくつか紹介しつつ、プログラミングとの共通点について考えてみたいと思います。

アド(アドバンテージ)

アドという単語はアドバンテージの略で、カードゲームにおいては、有利な状況やリソースの差を指す言葉です。
アドバンテージがある、というのは相手よりも有利な状況にあるということです。
めちゃくちゃアドバンテージが取れることを「爆アド」と言ったりします。

カードゲームにはいくつかの概念があり、それぞれに有利不利が存在するので、アドバンテージという概念も同じように種類があります。

  • 手札アドバンテージ
    手札の数が相手よりも多い状態。遊戯王のようなスペルリソースを不要とするようなカードゲームでは行動できる数に直結します。また、デュエルマスターズのような、行動にリソースを消費するゲームでも、手札が多いことで行動の選択肢が増えます。
    極端な話、手札が0枚の状態であれば、行動ができないので、手札アドバンテージがあることは非常に重要です。
  • 盤面アドバンテージ
    カードゲームにおいて、場にでているカードの数や強さが相手よりも有利な状態を指します。盤面というのは、モンスターやクリーチャーなどを出す場所、またはその場所の状況を指します。
    盤面アドバンテージがあると、相手に攻撃を仕掛ける、防御するなどといった行動の選択肢が増えます。
  • リソースアドバンテージ
    カードゲームによってはマナ(デュエルマスターズなど)やエネルギー(ポケモンなど)といったリソースを消費して行動することがあります。リソースアドバンテージがあると、相手よりも多くの行動をすることができます。
    もしくは、より大きなコストを払い、より強力なカードを出すことができます。

「アドを取る」ということは、ゲームを有利に進めるための基本的な行動です。
少しずつアドを取りながら、最終的に勝利を目指していきます。
例えば、1枚のカードを使って相手の2枚のカードを破壊するといった行動は、少ない枚数で相手のカード枚数を削ることができるので、手札アドバンテージを取ることができます。
さらに、消費するコストが小さい場合はリソースアドバンテージも取れていると言えます。

ただし、アドを取ること自体が目的ではなく、勝利することが目的なので、個々のアドバンテージに固執せず、より俯瞰的にゲームを進めることが重要です。

人生においてのアドバンテージについては下記のブログが参考になります。

言及している人のブログ記事: 「アドバンテージ」というカードゲームの概念が、人生ですごい役に立った話

プログラミングにおいてのアドバンテージを考えてみると下記のようなものがあります。

  • 手札アドバンテージ
    • 知っている知識の量が該当します。より多くの言語知識、設計方針、ライブラリの使い方を知っていれば、より多くの選択肢を持つことができます。
  • 盤面アドバンテージ
    • プロジェクトの進行状況などが該当します。プロジェクトの進行状況が良いとより多くの価値を生み出すことができます。
  • リソースアドバンテージ
    • リソースアドバンテージは、プログラミングにおいては時間や人的リソースが該当します。時間があればより多くの機能を実装することができますし、人的リソースがあればより多くの作業を行うことができます。
    • プログラミングはやや特殊で、知識があれば、リソースの効率を大幅に上げることができるので、ここでいうリソースアドバンテージの重要度は手札アドバンテージと比べると低いかもしれません。
  • その他
    • 他にも、プログラミングにおいては、アドバンテージを取るための概念があります。例えば、コードの品質を高めることで、バグを減らし、メンテナンス性を高めることができます。これは、盤面アドバンテージに近い概念かもしれません。
      • カードゲーム用語だけではないですが、レバレッジという言葉もあります。レバレッジとは、テコの原理のように、少ない力で大きな力を生み出すことができることを指します。自動テストや品質向上は将来的に無駄なリソースの消費を抑えることができるので、レバレッジが効くと言えます。

テンポ

テンポは、多くのカードゲームにおいて本質的な概念であり、もっとも奥が深いテーマの一つです。

テンポとは何か?

カードゲームにおける「テンポ」とは、試合の進行スピードやリズムを指し、プレイヤーがゲームをどれだけ効率的に、そして有利に進められているかを表す概念です。テンポを意識することで、ゲームの主導権を握り、最終的な勝利に繋がるプレイが可能になります。

言い換えると相手に対してどれだけ効率的に行動をし、主導権を握るか、というテーマです。

カードの効果やリソースの管理を通じて、自分が次の行動でより良い選択肢を得られるかどうかがテンポの良し悪しを左右します。テンポの良いプレイを続けることで、相手が追いつけなくなる状況を作り出しやすくなります。

テンポを制するための要素としては下記のようなものがあります。

  • コストの管理
    カードをプレイするためにはコスト(マナ、エネルギーなど。=リソース)が必要な場合が多く、毎ターンのリソースを効率的に使うことでテンポを保ちやすくなります。無駄なコストを使わず、最大限の効果を発揮するカードをプレイすることで、テンポが安定します。
    ただし、コストを使い切ることが全てではなく、コストを使い切るために手札を消費し、次のターンに選択肢がなくなることもあります。この場合はターンあたりの効率はいいですが、次のターンでテンポを失うことになります。

  • カードの使いどころ
    強力なカードを引きすぎて手札が重くなると、逆にテンポを失うことがあります。適切なタイミングで、適切なカードをプレイすることがテンポ維持の鍵です。場の状況を見ながら、軽いコストで十分な効果を出せるカードを使い、少しずつ有利を拡大していきます。

テンポを失うと、相手に主導権を握られやすくなります。例えば、何もしないターンが続くと、相手が自由に展開を進めてリソースや盤面の優位を拡大してしまい、追いつくのが困難になります。また、重いコストのカードばかりを手札に持っていると、リソースが足りないためにターンごとに何もできない「テンポロス」を引き起こします。

テンポを重視する戦術としてアグロデッキ(攻撃的なデッキ)というものがあります。これは、早い段階でのテンポの優位性を重視します。序盤から積極的に攻撃を仕掛け、相手が防御や展開に回る前にライフを削り切る、主導権を握り続けることを目指します。少しでもテンポを失うと試合が長引き、相手に対応する時間を与えることになるため、テンポを取り続けるプレイが求められます。

アドバンテージと似た概念ではありますが、アドバンテージは局所的に相手に対して有利な状況を作ることを目指すのに対し、テンポはより俯瞰した、試合の進行スピードやリズムを意識して、効率的にプレイすることを目指します。言い換えれば、継続してアドを取り続けることとも言えます。

バリュー

バリューとは、カードゲームにおいて、1枚のカードの効果や1つの行動の価値を表す概念です。
1枚のカードを消費して、相手の2枚のカードを破壊するといった行動は、バリューが高いと言えます。

アドバンテージとバリューは似ているようで異なる概念です。

  • 相手に対する相対的な有利さを表すのがアド
  • 1つのカードやアクションが持つ絶対的な価値がバリュー

エンジニアの判断の多くは、1つの行動によってどれだけの価値を生み出すか、という軸になることが多いので、バリューの概念はプログラミングによく登場します。

例: 〇〇アーキテクチャを採用することで、変更の影響範囲を狭めることができ、テストも書きやすくなるので、バリューが高い。

カードの「バリュー」とカード・アドバンテージ

細かい日本語の違い

カードゲームにおいては、細かい日本語の違いで裁定と呼ばれるカードの解釈が変わることがあります。
例えば、デュエルマスターズにおいては、「召喚する」という言葉と「出す」という言葉があります。

「召喚する」という行動は、マナを消費してクリーチャーを場に出すことを指します。
「バトルゾーンに出す」という行動は、クリーチャーを場に出すことを指し、召喚を含みますが、カード効果による場に出す行動も含みます。

ここで、効果の発動条件が「召喚する」という場合、他のカードの効果でバトルゾーンに出されたクリーチャーは対象になりません。

こういった細かい違いへの意識は、プログラミングにおいても重視されるかなと感じています。

また、裁定そのものが変わることもあり、プログラミングにおける仕様変更そのものです。

プロダクト開発におけるカードゲーム概念の適用

これはプログラミングというよりはプロダクト開発に近いかもしれませんが、カードゲームの概念をプロダクト開発に適用することもできるかなと思います。

例えば、プロダクトの立ち上げフェーズでは、強力で完璧な機能を1つリリースするよりも、軽いコストでリリースできる機能を多くリリースすることで、ユーザーをつけ、お金や開発メンバーといったリソースを増やすことができます。

プロダクトのフェーズが進むにつれて、CI/CDや自動テスト、品質向上などの仕組みを使い、リソースの効率を上げることができます。これは、リソースアドバンテージを取ることに近いかもしれません。

カードゲームは詰まるところ、自分のデッキの強みをどれだけ相手に押し付けることができるか、というものだと思います。

プロダクト開発も同じで、自分のプロダクトの強みをどれだけユーザーに伝え、使ってもらえるか、ということが重要だと考えています。

もちろん、プロダクト開発を他社との競争だと考えるとカードゲームと違って同時に始まるわけでもないし、そもそも1対1でもないし、勝者も1つではないので、完全には適用できないので、全然違います。どちらかというと市場だったり、環境といったものが相手になるのかもしれません。

個人的な技術へのスタンス

私は残念ながら、本当の意味でのものづくりをするクリエイターというタイプのエンジニアではないのかなと思っています。
ただ、他人の作った技術、仕組みを使って自分の作りたいものを作ることは好きです。
私にとって新しいフレームワークやライブラリは、カードゲームで言えば新弾のようなものです。

  • プログラミングにおいてビジネスという目的の上で技術を利用し、新しい技術を学び続けていること
  • 勝利するために現在の環境メタを勉強したり新弾のカードを知り、必要であればデッキに組み込むこと

似ていると思うんですよね。

あくまで技術ユーザーとしての立場なので、新しい技術の話題は「新弾の〇〇強すぎない?」みたいな話だし、漫画でいうと「今週のジャンプ見た?」くらいの感覚でいます。

まとめ

  • カードゲームとプログラミングは、共通点が多いと思う
  • アドバンテージ、テンポ、バリューなどの概念は、プログラミング、さらには人生そのものにも適用できる
  • プロダクト開発においても、テンポを意識して、適切なタイミングで適切なカードをプレイすることが重要
  • カードゲームは自分のデッキの強みをどれだけ相手に押し付けることができるか
  • プロダクト開発は自分のプロダクトの強みをどれだけユーザーに伝え、使ってもらえるか

所感

色々と書きましたが、一連のゲームにおける最初のアドを取れるポイントは手札であり、現実世界においてはカードゲームほど平等に手札が配られるものではないと思います。
行動量を増やしたり、学習量を増やしたり、すでに知識を持っている人に教えてもらったりといったアドバンテージを取る行動は他の要素にも響いてくるので、まずはここからアドバンテージを取ることが個人的には重要だと思います。

強い切り札を持っているか、というのは人によりますが、現実世界では1ターンに5回ドローするひともいれば、1ターンに1回もドローしない人もいます。
山札からドローできる回数は自分で増やしていくことができそうです。

フロントエンドの状態管理について調べてみる

はじめに

今回はフロントエンドの状態管理について調査してみます。

フロントエンドにおける状態管理とは

フロントエンドにおける状態管理とは、ユーザーインターフェース(UI)のデータやその変化を一貫性を持って管理する手法を指します。
ここで言う「状態(State)」とは、サーバーから取得したデータ、UIの現在の表示内容など、アプリケーションの動作に影響を与える全ての情報を意味します。
ユーザーからの入力も状態に含まれます。

今回は自分の考えにも近いこちらの記事を参考に状態管理周りを調査してみます。

2020年に立ち上げたWebフロントエンド構成の振り返り

状態が必要な理由

状態管理を行わない、もしくは不適切な状態管理を行なった場合のつらさを確認してみます。

データの不整合

主流となっている宣言的UI系のライブラリ、Vue.jsやReactでは、コンポーネントの再利用性を高めるために、コンポーネントごとにデータを管理することができます。

この管理をした場合、各コンポーネントが独自の状態を持つことになります。これにより、同じリソースを表示すべき箇所が複数ある場合はUIコンポーネント間でデータの不整合が発生する可能性があります。

コードの複雑化

データの受け渡しの流れを適切に整理せず、コンポーネント間でデータを受け渡すことが多くなると、コードの複雑化が進みます。
UIはどうしても親子関係が入れ子になるため、親から子へのデータの受け渡し、子から親へのデータの受け渡し、兄弟間でのデータの受け渡しなど、データの流れを把握するのが難しくなります。

後述するVuexの公式ドキュメントには下記の記載があります。

しかし、単純さは、共通の状態を共有する複数のコンポーネントを持ったときに、すぐに破綻します:
複数のビューが同じ状態に依存することがあります。
異なるビューからのアクションで、同じ状態を変更する必要があります。
一つ目は、プロパティ (props) として深く入れ子になったコンポーネントに渡すのは面倒で、兄弟コンポーネントでは単純に機能しません。二つ目は、親子のインスタンスを直接参照したり、イベントを介して複数の状態のコピーを変更、同期することを試みるソリューションに頼っていることがよくあります。これらのパターンは、いずれも脆く、すぐにメンテナンスが困難なコードに繋がります。
では、コンポーネントから共有している状態を抽出し、それをグローバルシングルトンで管理するのはどうでしょうか? これにより、コンポーネントツリーは大きな "ビュー" となり、どのコンポーネントもツリー内のどこにあっても状態にアクセスしたり、アクションをトリガーできます!
さらに、状態管理に関わる概念を定義、分離し、特定のルールを敷くことで、コードの構造と保守性を向上させることができます。
これが Vuex の背景にある基本的なアイディアであり、Flux、 Redux そして The Elm Architectureから影響を受けています。 他のパターンと異なるのは、Vuex は効率的な更新のために、Vue.js の粒度の細かいリアクティビティシステムを利用するよう特別に調整して実装されたライブラリだということです。

バグの増加と開発効率の低下

機能追加時や変更時に状態の管理が複雑になってしまうと、理解するのに時間がかかり、開発スピードが落ちます。
同じデータを扱う場合の同期の問題など、データのスコープを適切に設計しないと、バグが発生しやすくなります。

Vue.jsにおける状態管理

Vue.js 2.xでは、Vuexという状態管理ライブラリが提供されています。現在のVue.js 3.xでもVuexは引き続き利用可能ですが、Vue 3.xではComposition APIを利用することで、Vuexを使わずに状態管理を行うことも可能です。似たような状態管理ライブラリとして、Piniaが公式で推奨されています。

Vuex

Vuexはアプリケーション全体で共有される集中型ストアを持ちます。これはFluxというアーキテクチャにインスパイアされており、他のライブラリにも影響を与えています。

VUexは以下の要素で構成されています。

  • State:アプリケーションの状態を保持するオブジェクト。
  • Getters:状態を取得するための算出プロパティ。
  • Mutations:状態を変更するためのメソッド。同期的な処理。
  • Actions:ミューテーションをコミットするためのメソッド。非同期処理も可能。

Stateの定義の仕方

const store = new Vuex.Store({
  state: {
    count: 0
  }
})

Stateの取得、Gettersの定義の仕方

const store = new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  }
})

Mutationの定義の仕方

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

Actionでの非同期処理の定義の仕方

const store = new Vuex.Store({
  state: { count: 0 },
  mutations: { /* ... */ },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  }
});

Fluxとは

FluxはFacebookが提唱するアプリケーションのアーキテクチャです。
Fluxは3つの要素から構成されています。

要素 説明
Store アプリケーションの状態を保持するオブジェクト、および状態の更新処理
Action ユーザーの操作やAPIのレスポンスなど、アプリケーションの状態を変更するための処理
Dispatcher Storeに対してActionを発火させる

Fluxの特徴として、データの流れが一方向であることが挙げられます。

流れとしてはAction -> Dispatcher -> Store -> Viewとなります。

状態を更新する場合は

  1. ViewからActionを発火(ボタンを押す、文字を入力するなど)
  2. 更新したい内容をActionとしてDispatcherに送信
  3. DispatcherがStoreにActionを送信
  4. Storeの状態がActionの内容に応じて更新され、それを検知したViewが再描画される

という流れになります。

この方法によって、データの流れを逆流させたり、Dispatcherを経由せずにStoreを直接更新することができないため、データの一貫性を保つことができます。

ちなみに現在主流となっているPiniaやVuex5と呼ばれるものはFluxアーキテクチャをベースにしてはいますが、よりシンプルでFluxアーキテクチャを理解する必要がないように設計されています。

Reactにおける状態管理

ReactにおいてもFluxアーキテクチャをベースにした状態管理ライブラリがいくつか存在します。
Reduxが有名ですが、他にもMobXやRecoil、Zustandなどがあります。

ですが、今回はそれらのライブラリの解説ではなく、自分の考えている状態管理の最適解についてまとめてみます。

状態の分類

参考記事にもある通り、フロントエンドで扱う状態は下記のように分類できると考えます。

  • サーバーから取得したデータのキャッシュとしての状態
  • アプリケーション全体で持つグローバルな状態
  • コンポーネント内でのみ持つローカルな状態

サーバーから取得したデータのキャッシュとしての状態

SWR

SWR(Stale-While-Revalidate)は、Next.jsの開発元であるVercel社が提供するReact Hooksライブラリです。
主な責務はデータの取得とキャッシュを簡単に行うことです。

SWRは、HTTPのキャッシュ戦略であるStale-While-Revalidateを採用しています。(MDN web docs)

この戦略は、キャッシュが古い場合に古いデータを返しつつ、バックグラウンドで新しいデータを取得し、新しいデータが取得できたら古いデータを新しいデータに置き換えるというものです。

基本的な使い方は下記の通りです。

import useSWR from 'swr';

const fetcher = url => fetch(url).then(res => res.json());

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher);

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;

  return <div>Hello {data.name}!</div>;
}

特徴としては、下記のような点が挙げられます。

  • キャッシュの有効期限を設定できる
  • フォーカス時や再接続時にデータを再取得する
  • api/userのような部分はエンドポイントではなくキー。同じキーを指定することで、複数のコンポーネントでデータを共有できる

ただ、あくまでデータフェッチに特化しており、サーバーからのデータ取得に特化しているため、状態管理ライブラリとしては不十分です。

React Query

React Queryは、Reactアプリケーションでデータを取得、キャッシュ、更新、削除するためのライブラリです。
SWRと比較すると、より高度な機能を持っています。

もともと、React QueryやSWRを利用しない場合はuseStateとuseEffectを組み合わせてデータの取得とキャッシュを行っていましたが、React Queryを利用することで、データの取得やキャッシュを簡単に行うことができます。

初期設定

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

データの取得

// App.js
import React from 'react';
import { useQuery } from 'react-query';

function fetchUsers() {
  return fetch('https://api.example.com/users').then(res => res.json());
}

function App() {
  const { data, error, isLoading } = useQuery('users', fetchUsers);

  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラーが発生しました: {error.message}</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default App;

useQueryの第一引数にはキーを指定します。このキーはデータのキャッシュに利用されます。
useQueryの第二引数にはデータを取得する関数を指定します。この関数は非同期関数である必要があります。
その他状態としてisLoadingやerrorが返却されるため、それに応じて表示を変更することができます。

React Queryはデフォルトでデータをキャッシュし、一定時間が経過すると自動的に再フェッチします。これにより、データの新鮮性を保ちながら、不要なリクエストを削減できます。

キャッシュの有効期限を設定することも可能です。

useQuery('users', fetchUsers, {
  staleTime: 1000 * 60 * 5, // 5分間データを新鮮とみなす
});

取得だけでなく、データの更新や削除も簡単に行うことができます。

import { useMutation, useQueryClient } from 'react-query';

function AddUser() {
  const queryClient = useQueryClient();

  const mutation = useMutation(newUserData => {
    return fetch('https://api.example.com/users', {
      method: 'POST',
      body: JSON.stringify(newUserData),
    });
  }, {
    onSuccess: () => {
      // 'users'クエリを無効化して再フェッチ
      queryClient.invalidateQueries('users');
    },
  });

  const handleAddUser = () => {
    mutation.mutate({ name: '新しいユーザー' });
  };

  return (
    <button onClick={handleAddUser}>
      ユーザーを追加
    </button>
  );
}

ちなみにSWRにもuseSWRMutationというデータの更新を行うためのフックが用意されていますが、React Queryの方がより高度な機能を持っています。
シンプルさを求める場合はSWR、より高度な機能を求める場合はReact Queryを利用すると良いでしょう。

アプリケーション全体で持つグローバルな状態

これはSWRやReact Queryでは対応できないため、状態管理ライブラリを利用する必要があります。
とはいえコンポーネント内で保持できるものは後述するローカルな状態で十分であるため、アプリケーション全体で持つグローバルな状態を持つ必要がある場合は、VuexやRedux等ライブラリを利用して、状態管理を行うことが一般的です。

例えば、認証の情報やサイドバーの開閉状態、検索条件などページを跨いでも保持しておきたい情報がこれに該当します。

参考記事ではRecoilを利用していますが、こちらに関しては触ったことがないため、ここでは割愛します。

この記事も参考になりそうです。 Facebook製の新しいステート管理ライブラリ「Recoil」を最速で理解する

コンポーネント内でのみ持つローカルな状態

最後に、コンポーネント内で保持するローカルな状態についてです。

ページを跨いで保持する必要のない状態はReactの場合はuseStateやuseReducerを利用して管理することができます。
単純である代わりにテストがしづらいことが参考記事では挙げられていますが、storybookを利用したフロントエンドのテストは現状取り組めていないため、今後の課題としたいと思います。

まとめ

フロントエンドの状態管理について調査してみました。
状態管理の悩みの種であるサーバーからのデータの取得とキャッシュについてを中心に調査しましたが、React QueryやSWRを利用することで、簡単にデータの取得とキャッシュを行うことができることがわかりました。
コンポーネントのテストや、アプリケーション全体で共有する状態については、まだまだ課題が残っているため、今後も引き続き調査を行っていきたいと思います。

tRPCへの入門

はじめに

今回はtRPCという技術について調査してみます。
なにもわからない状態からのやってみた系記事になります。
概念の理解や個人的な感想を中心に書いていきます。
実際に導入する際には公式サイトを参照することをおすすめします。

tRPCとは

そもそもRPCとは?についてはこちらのQiita記事に詳しく書かれています。

tRPC公式サイトにもコンセプトページが用意されています。

tRPCを利用するメリットとしては、TypeScriptによる型推論を活用して、フルスタックアプリケーションを構築する場合において、型安全なAPIクライアントとサーバーを提供することができる点が挙げられます。公式サイトには以下のような特徴が記載されています。日本語訳なので不自然な表現があるかもしれませんが、ご了承ください。

  1. 自動的安全性: サーバー側で変更を加えた場合、ファイルを保存する前にクライアント上でエラーを警告します。
  2. SnappyDX: tRPCにはビルドやコンパイルのステップがなく、コード生成、ランタイムの膨張がありません。(Snappyは快活な、活発な、きびきびした、威勢の良い、てきぱきしたという意味があります。)
  3. フレームワークに依存しない: 全てのJavaScriptフレームワークおよびランタイムと互換性があります。既存のプロジェクトに簡単に追加できます
  4. 自動補完: tRPCを使用すると、APIのサーバーコードにSDKを使用するのと同じになり、エンドポイントに自信が持てるようになります。
  5. ライトバンドルサイズ: tRPCには依存関係がなく、クライアント側のフットプリントが小さいため軽量です。(フットプリントとは、稼働時に必要とする資源の大きさという意味があります。)
  6. 電池付属: React, Next.js, Express, Fastify, AWS Lambda、Solid、Svelteなどのアダプターを提供しています。

tRPCを導入する手順

1. tRPCのセットアップ

パッケージのインストール

npm install @trpc/server@next @trpc/client@next

今回はNext.jsにtRPCを導入してみます。まずはsrc配下にserverディレクトリを作成し、trpc.tsを作成します。 ここではバックエンドを初期化します。 いい慣例として、再利用可能なヘルパー関数としてエクスポートすることが推奨されています。

src/server/tprc.ts

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

次にルーターのセットアップをします。

公式サイトではserver/index.tsに記述されていますが、今回はsrc/server/routersフォルダを用意し、エンドポイントが増えた場合に対応しやすいように記述します。

src/server/routers/_app.ts

import { router } from '../trpc';
import { exampleRouter } from "./example";

const appRouter = router({
  // ここにルーターを追加
  example: exampleRouter
});

export type AppRouter = typeof appRouter;

2. エンドポイントの作成

それぞれのエンドポイントでは、下記のようにプロシージャを作成します。

src/server/routers/example.ts

import { publicProcedure, router} from "../trpc";
import { z } from "zod";

// エンドポイントの定義

export const exampleRouter = router({
  hello: publicProcedure
    .input(z.object({
      text: z.string().nullish()
    }).nullish())
      .query(({ input })=> {
        return {
          greeting: `Hello, ${input?.text ?? "world"}!`
        }
      }
    )
});

これはごく簡単な例ですが、この内容がエンドポイントの処理内容になるので、ドメイン駆動設計やクリーンアーキテクチャなどを取り入れる場合はさらにファイルを分割することになります。

このプロシージャーをAction層、Controller層として扱い、UseCaseやRepository、Service、ドメインオブジェクトなどに分割してそれぞれテストを書いていくことができます。

この例ではシンプルに書いてますが、実際にはprisma等を利用してDBアクセスしたり、外部APIを呼び出したりすることができます。

3. クライアントのセットアップ

クライアント側のコードに移り、バックエンドと同じ型を利用し型安全性の力を活用しつつ、バックエンドの呼び出しを行います。

tRPCのセットアップとして、公式サイトのクイックスタート同様にhttpBatchLinkを利用します。解説は後に回します。

src/client/trpc.ts

import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from 'src/server/routers/_app';
import { httpBatchLink} from "@trpc/client";

export const trpc = createTRPCClient<AppRouter>({
      links: [
        httpBatchLink({
          url: '/api/trpc'
        })
      ],
    }
);

4. クライアントからエンドポイントを呼び出す

クライアントの任意のファイルでエンドポイントを呼び出します。

    const example = await trpc.example.hello.query({
      text: "世界"
    });

httpBatchLinkについて

httpBatchLinkは、複数のリクエストを一度に送信するためのリンクです。

例えば、次のようなリクエストを送信することができます。

const somePosts = await Promise.all([
    trpc.post.byId.query({ id: 1 }),
    trpc.post.byId.query({ id: 2 }),
    trpc.post.byId.query({ id: 3 }),
])

このコードは1つのHTTPリクエストになります。

詳細は公式サイトを参照してください。

用語について

改めて公式サイトに記載されているコンセプトと用語を確認しておきましょう。

RPCとは

RPCはRemote Procedure Callの略です。あるコンピューター上の関数を別のコンピューターから呼び出すことができる方法のことを指します。(厳密にはより広義の意味合いもありますが、現代フロントエンドにおいてはこの意味合いが一般的です。)

tPRCは、TypeScriptのモノレポように設計されたRPCの実装の1つです。

平たく言えば、HTTP通信のラッパーであり、アプリケーションコードを書く際に実装の詳細について意識せずに、関数を呼び出すだけでtRPCが全てを処理します。

公式サイトより、以下の用語がエコシステムで頻繁に使用される用語です。

用語 意味
Procedure APIエンドポイント - querymutationsubscriptionのいずれか。
Query データを取得するための手続き。procedure
Mutation データを変更するための手続き。creates, updates, or deletes を行う手続き。procedure
Subscription 持続的な接続を作成し、変更を受け付けるための手続き。procedure
Router 共有している名前空間の下にあるprocedure(または他のルーター)の集まり。
Context 全てのprocedureがアクセスできるもの。セッション状態やデータベース接続などに使用される。
Middleware procedureの前後に実行できる関数。コンテキストを変更することができる。
Validation procedureの入力データを検証するためのしくみ。

まとめ

  • tRPCはTypeScriptの型推論を活用して、フルスタックアプリケーションを構築する場合において、型安全なAPIクライアントとサーバーを提供することができる。
  • サーバーサイドのエンドポイントは、プロシージャを作成し、アクション層、コントローラー層として扱い、UseCaseやRepository、Service、ドメインオブジェクトなどに分割してそれぞれテストを書いていくこともできる。
  • クライアントサイドからは関数を呼び出すだけでtRPCの処理を呼び出すことができる。
  • Next.jsと組み合わせて使う場合は、API Routeを利用することで、サーバーサイドのエンドポイントを作成することができる。

OpenAPIの理解とその活用方法を考える

はじめに

今回はWeb APIの仕様書として広く使われているOpenAPIについて調査してみます。

OpenAPIとは

OpenAPI(旧Swagger)はREST APUのAPI記述形式です。

公式ドキュメントによると、次の内容を記述できると書かれています。

  • 利用可能なエンドポイント(/users)と書くエンドポイントでの操作(GET /users, POST/users)
  • 操作パラメータ 各操作の入力と出力
  • 認証方法
  • 連絡先情報、ライセンス、利用規約、その他の情報。

参考: https://swagger.io/docs/specification/about/

なお、よく登場するSwaggerという用語は、OpenAPIの前身の名称のようです。
もともとReverb(現SmartBear)が開発したAPI設計ツールおよび仕様です。
APの設計やドキュメント化、テスト、モックサーバーなどシミュレーションのためのツールセットを提供しており、広く使われるようになりました。
OpenAPIはそのSwagger仕様をベースに進化したもので、Swagger 2.0の後継として誕生しました。現在はOpenAPI Initiativeという組織によって管理されているAPIの設計と仕様を標準化するための業界標準です。

現在Swaggerという名称は、OpenAPIのためのツールの名称として残っており、Swagger EditorやSwagger UIなどがOpenAPIのツールとして提供されています。
OpenAPIという名称は先述の通り、ツールではなく仕様の名称です。

OpenAPIの活用方法、事例

基本的な活用としては、OpenAPIでREST APIの仕様を記述することですが、ほかにも次のような活用方法があります。

  1. 自動テスト いわゆる契約テストとして、OpenAPI仕様書をもとに、APIが仕様通りに動作しているかを自動的にテストするツールがあります。
    例えば、GUIAPIリクエストを送信できるクライアントツールとしてPostmanがあります。
    このPostmanのリクエストを定義したCollectionをOpenAPI仕様書から自動生成するためのツールもあります。(OpenAPI 3.0, 3.1 and Swagger 2.0 to Postman Collection)
    PostmanはGUIなので、CI/CDパイプラインに組み込むことは難しいですが、Postmanのコレクションをコマンドラインで実行するNewmanというツールもあるようです。

  2. モックサーバーの作成 開発の初期段階で、バックエンドチームとフロントエンドチームが分かれている場合、OpenAPI仕様を先に作成することで、フロントエンドチームはモックサーバーを使って開発を進めることができます。 これにより、バックエンドの実装が完了する前に、APIとのやりとりを模倣しながら開発を進めることができます。ツールはPrismが一番有名なのかなと思います。 OpenAPI仕様書以外にも、PostmanのCollectionからモックサーバーを作成することもできます。Postman Collections Support

  3. クライアントコードの自動生成 OpenAPI仕様書から、各種プログラミング言語向けのクライアントSDKを自動生成できます。ツールは割と乱立していそうですが、有名どころは下記の通りです。

  4. Swagger Codegen

  5. OpenAPI Generator
  6. Kiota

一番ポピュラーなのはSwagger Codegenかなと思います。先に述べたSwaggerツール群の一つです。OpenAPI GenerattorはSwagger Codegenからのフォークで、Swagger Codegenの後継として開発されています。(https://github.com/OpenAPITools/openapi-generator/blob/e78aeb6bc7610a763e17e3d53614a1ef1990f311/docs/qna.md)

  1. API Gatewayの設定 OpenAPI仕様から、AWS API GatewayのCDKを利用してAPI Gatewayの設定を自動生成することができるようです。参考: https://zenn.dev/taroman_zenn/articles/91879cec40627c

スキーマ駆動開発

OpenAPIはREST APIの仕様を記述するためのもので、スキーマ駆動開発の一環として使われることが多いです。
スキーマ駆動開発とは、実装前にAPIの仕様をスキーマとして定義し、そのスキーマに従って実装を進める開発手法です。
これにより、先述したようなドキュメント、実装コード、テスト、モックサーバーなどの一部を自動生成することができます。
また、バックエンドチームの実装を待たずにフロントエンドチームが開発を進めることができるようになります。
また、スキーマを決めることによってデータ構造等の一貫性を保つことができるため、開発の品質を向上させることができます。

個人的に実現したいスキーマ駆動開発のポイントとして、次のようなものがあります。

  • OpenAPI仕様書からTypeScriptの型定義を自動生成する。
  • バックエンドでは実装と仕様の乖離を減らす。

一点目についてはさまざまなツールが存在しているので、実現は難しくないと思います。例えば、swagger-typescript-apiのようなツールがあります。

2点目については、バックエンドをtypescript以外で実装する場合は同じ言語で型定義を共有することができないため、仕様と実装の乖離が生じやすいです。 PHP、Laravelの場合はこちらの記事が参考になりそうでした。
参考: 実装と乖離させないスキーマ駆動開発フロー / OpenAPI Laravel編

スキーマとコードを同期させるためには、コードベースに埋め込むのがいい、という発想は以前phpDocumentorを利用した経験があるので、なるほどと思いました。

PHP8以降はAttributeが利用できるようになったので、phpDocよりもAttributeの方がスキーマとコードを同期させやすいかもしれません。

swagger-phpでは、Attributeでスキーマを記載できます。
IDEのサポートを受けることができる点もAttributeの利点です。

ただ、参考記事のようにAttributeから全てのコードを生成するのは、すでに動いているプロジェクトに導入するのは難しいかなと感じました。

あくまで補助的に導入し、IDE等で警告を出すなどの形でスキーマとコードの乖離を防ぐのが良いのかなと思いました。

バックエンドの実装を待たなくてもいいことがスキーマ駆動開発のメリットでもあるため、各プロジェクトで、先にOpenAPI仕様書を作成するのか、コードベースから生成するのかを検討する必要がありそうです。

ドキュメントの重要性は叫ばれますが、実際にはドキュメントはコードベースと乖離してしまい、信頼できなくなった時点で無価値になってしまいます。
そのため、スキーマ駆動開発はドキュメントの信頼性を高めるためにも有効な手法だと感じました。テスト駆動開発に近い感覚で開発を進めることができると思います。

実装の乖離のチェックとして、契約テストを行うことも有効だと思います。Pact、Prism、Postmanなどのツールを使って、APIの仕様と実装が一致しているかを自動的にチェックする仕組みの用意、CI/CDパイプラインでの実行が現実的に導入しやすいと感じました。

まとめ

  • OpenAPIはREST APIの仕様を記述するための仕様書です。
  • OpenAPIを使うことで、自動テスト、モックサーバーの作成、クライアントコードの自動生成、API Gatewayの設定などができます。
  • スキーマ駆動開発のポイントとして、OpenAPI仕様書からTypeScriptの型定義を自動生成することが挙げられます。
  • バックエンドの実装を待たなくてもいいことがスキーマ駆動開発のメリットでもあるため、各プロジェクトで、先にOpenAPI仕様書を作成するのか、コードベースから生成するのかを検討する必要がありそうです。
  • スキーマ駆動開発はドキュメントの信頼性を高めるためにも有効な手法だと感じました。テスト駆動開発に近い感覚で開発を進めることができると思います。
  • Pact、Prism、Postmanなどのツールを使って、APIの仕様と実装が一致しているかを自動的にチェックするCI/CDパイプラインの実行が現実的に導入しやすいと感じました。