🦔

Remix入門: フロントエンドもバックエンドも爆速開発を実現する次世代Webフレームワーク

2023/06/11に公開

こんにちは!Acompanyのマッケイです!

この記事は Acompany5周年アドベントカレンダー 11日目 の記事です。

https://recruit.acompany.tech/b6d945cfebca4be0876128af68dd5d8b

今回はAcompanyのプロダクト開発でも活用しているRemixを開発環境で使ってみた所感を書いていこうと思います。

Hello,Remix

Remixは、Reactをベースとしたフルスタックフレームワークです。
https://remix.run/

Reactを魔改造して色々できるようにしようぜ、という昨今のモダンフレームワークに習うように、RemixもReactに厚化粧をした"React"フレームワークです。

書き心地はそのままReactですが、気づいたらサーバーサイドのコードを書いており、気づいたらデータベースをいじっているというなんとも不思議な経験ができるフレームワークです。

フルスタックフレームワークを使っているというよりは、Reactで開発しながら、サーバーサイドの処理も同時に書けるのがRemixです。

モダンフロントエンドという荒波の中で存在感を放つRemixの、実際の開発現場で使ってみた所感をRemixの入門を織り交ぜながら説明します。

Remix導入の参考にしていただければと思います。

Remixの哲学

Remixの哲学を一言で表すと、「Web標準に忠実に」です。

データを取得したいですか?
→ Web Fetch APIを使います。

データのミューテーションを行いたいですか?
→ Formが全て行ってくれます。

サーバーに状態を持たせたいですか?
→ CookieとSessionの出番です。

Remixの取り扱いが上手くなると、Webの取り扱いが上手くなります。

Remixで取り扱う概念のほとんどは、25年前のインターネットバブルの頃から脈々と受け継がれてきた技術インフラです。

Remixの革新は、このカラカラに枯れ切った技術インフラの上に、Reactを初めとする多くのモダンパッケージと開発体験を融合させたことにあります。

しかし、心配することはありません。

SPAに飼い慣らされた私のようなフロントエンドエンジニアであっても、難なくバックエンドの開発を始めることができます。

フロントエンドを主戦場とするエンジニアでも、今日からフルスタックエンジニアの仲間入りができる。

それがRemixなのです。

MDNは友達」が合言葉です。

https://remix.run/docs/en/main/pages/philosophy

Remixのアーキテクチャ

Reactの革新の一つは、「単方向データフロー」をWebの世界に取り入れ、View,Action,Stateの見通しを良くしたことです。

https://ja.react.dev/learn/thinking-in-react

階層の一番上のコンポーネント (FilterableProductTable) が、データモデルを props として受け取っています。データがトップレベルのコンポーネントからツリーの下の方にあるコンポーネントに流れていくため、この構造を 単方向データフロー (one-way data flow) と呼びます。

データは常に、アプリケーションを上から下に向けて流れるため、コードの記述と理解が容易になります。


Remix Data Flow

この考え方はReactだけに限らず、昨今のモダンフレームワークでも取り入れている概念であり、"State"によって"UI"が決定されると言う意味で、下記のようなシンプルな式で表現されます。


Start thinking declaratively:Flutter

何らかの方法で(例えばActionを用いて)Stateを変更することによって、Viewが再レンダリングされます

このメンタルモデルを使用して、Reactでも数多くの状態管理ソリューション (Context API, Redux, Recoil)が作成されてきました。

アプリケーションが、本当の意味でReactの中で閉じている間は、このメンタルモデルで問題ありませんでした。

しかし、アプリケーションにはデータベースや外部APIとの接続などが存在し、常にデータはReactの外に置かれます。


Remix Data Flow

Reactでは、外部に永続化されているStateを毎回自身のState管理システムにコピーしてから利用する必要があります。

もちろん、コピーしたStateをクライアントで変更する場合は、クライアントのState変更と外部Stateの変更を、あんなことこんなことをして二重管理する必要があります。(ああなんと嘆かわしい😇)

Reactのあらゆる状態管理システムは、クライアント上の状態の管理には役立ちますが、クライアントとサーバー上の状態を効果的に管理することには、無力です。

ここで、Remixの登場です。

One of the primary features of Remix is simplifying interactions with the server to get data into components.
(Remixの主な特徴の1つは、データをコンポーネントに取り込むためのサーバーとのやり取りを簡略化することです。)

Remixは、データフローをネットワーク全体に拡張して、サーバー(State)からクライアント(View)へ、そしてクライアント→サーバー(Action)を介してサーバー(State)に戻るという、真に単方向データフローを実現しています。


Remix Data Flow

リモートの状態をクライアントにロードしてきて、状態を変更する場合は、あくまでリモートの状態を変更し、その変更を再フェッチすることでクライアントのビューを再レンダリングします。

煩わしいState管理を行う必要はなく、ビューが参照するStateは、常にリモートのサーバーで管理しているStateと同期が取れた状態で参照するができ、バグが入る隙間を圧倒的に少なくします。

サーバーに保存するまでもないクライアントのローカルなStateは、Reactの状態管理ライブラリを用いて管理することが可能です。


Remix Data Flow

もちろん、これら全てのアークテクチャを開発者が実装する必要はありません。

Remixの丁寧な抽象化のおかげで、開発者はわずかなコードを記述するのみで、サーバーのStateをクライアントに渡し、クライアントからのアクションをサーバーで処理を行うことができます。

それでは、次章で具体的なRemixの開発体験について記述します。

Remixでの開発

Remixでの開発フローは大きく分けて3つです。

  1. Viewの定義
  2. Loader(またはAction)の定義
  3. データベースへの保存

Viewの定義

RemixのViewとは、つまりそのままReactのことです。

jsx/tsxを用いてコンポーネントを構築することはReactと変わりません。

Remixでは、ファイルベースのルーティングシステムがビルドインされているため、Reactの時のようにルーティングライブラリをインストール、整備する必要はありません。

app/routeディレクトリ以下に作成したディレクトリやファイルが、そのままアプリケーションのURLとして構築されます。

これは、Next.jsとほとんど変わりません。

以下のコードで、ログインページのViewを/loginURLパスで表示することができます。

app/route/login.tsx
export default function Index(){
  return(
    <Form method="post">
      <Input name="email" />
      <Input name="password" type="password" />
      <Button type="submit" />
    </Form>
  )
}

Loader(またはAction)の定義

Remixには、サーバー処理を記載するためのloader/action関数が用意されています。

この関数は、クライアント↔︎サーバーのやり取りを行うための関数です。

loader関数では、サーバーのデータ(State)をViewに渡すためのデータ(State)を定義し、loader関数の返り値をViewで受け取ることが可能です。

app/route/login.tsx
import type { ActionArgs } from "@remix-run/node";
import { redirect, json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export const loader = async ({ request }: LoaderArgs) => {
  const isAuth = await isAuthenticated(request);
  // 既にログイン済みであれば、/auth/callbackにリダイレクト
  if (isAuth.logged) throw redirect("/auth/callback");
  
  return json({
    logged: isAuth.logged,
    error: isAuth.error
  });
};

export default function Index() {
  // loader()の返り値を取得するHooks
  const { error } = useLoaderData<typeof loader>();
  
  return (
    <Form method="post">
      <RootError error={error} />
      <Input name="email" />
      <Input name="password" type="password" />
      <Button type="submit" />
    </Form>
  );
}

loader関数は、RESTfullでいうGETに相当する関数であり、app/route/*のファイルでexport const loaderをすることで、そのURLパスにGETAPIエンドポイントを定義することができます。

loader関数は、Remixがうまく抽象化を行なっているおかげで、まるでWeb Fetch APIを扱うかのごとく、サーバーサイド処理を記述することができます。(return jsonはまんまfetch().((res)=>res.json())の書きぶりです)

もちろん、loader()内で、fetch関数を用いたデータフェッチやResponseインスタンスでの返却、URLパラメータの処理など、一般的なサーバーの処理にWeb標準な処理を共存させることも可能です。

export const loader = ({ request }: LoaderArgs)=>{
  // クッキーの読み込み
  const cookie = request.headers.get("Cookie");
  
  // fetch data
  const res = await fetch("https://fetch.com")

  // クエリパラメータの取得(?q=)
  const url = new URL(request.url);
  const query = url.searchParams.get("q");
  
  // fetch("https://any/path").then((res)=>res.json()) と同じ
  // json()の返り値が、クライアントに送信される
  return json({ any: "thing" });

  // const res:Response = fetch("https://any/path") と同じ
  // Responseの値が、クライアントに送信される
  return new Response(JSON.stringify({ any: "thing" }), {
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
  });
}

action関数では、クライアントからのActionを受け取り、Stateの変更などを行うサーバーサイドの関数です。

クライアントのPOSTリクエストを受け取り、Bodyのデータを読み取ることが可能です。

app/route/login.tsx
import type { ActionArgs, LoaderArgs } from "@remix-run/node";
import { redirect, json } from "@remix-run/node";
import { useActionData, useFetcher, useLoaderData } from "@remix-run/react";
import { authenticate, isAuthenticated } from "~/servers/auth.server";

export const loader = async ({ request }: LoaderArgs) =>{
  // loader処理
}

export const action = async ({ request }: ActionArgs) => {
  // Formのデータを取得
  const formData = await request.formData();
  const email = String(formData.get("email"));
  const password = String(formData.get("email"));

  const { error } = await authenticate(
    request,
    { email, password },
    // 認証に成功したら、/auth/callbackにリダイレクト
    "/auth/callback"
  );

  return json({ error });
};


export default function Index() {
  // loader()の返り値を取得するHooks
  const { error: loaderError } = useLoaderData<typeof loader>();
  
  // action()の返り値を取得するHooks
  const { error: actionError } = useActionData<typeof action>();
  
  return (
    <Form method="post">
      <RootError error={loaderError} />
      <Input name="email" />
      <Input name="password" type="password" />
      <ErrorField error={actionError} />
      <Button type="submit" />
    </Form>
  );
}

Remixでサーバーサイドにリクエストを行うのに、fetchaxiosも必要ありません。

<form /><input />などのフィールドを用意して、<Button type="submit" />を行うだけで、クライアントのデータをサーバーに送ることができます。

サーバー側でも、受け取ったデータを簡単にパースして値を取り出すことができます。

何とも古き良きクライアント/サーバー方式の処理フローをしています。

残念ながら筆者はエンジニア人生をReactから始めた人種なため、懐かしさに浸ることはできませんが...

もちろん、POSTだけでなく、PUTDELETEメソッドも使えるようので、RESTfullっぽい処理も記述可能です。

export const action = ({ request }: ActionArgs)=>{
  switch (request.method) {
    case "POST":
      return postFn(request);
    case "PUT":
      return updateFn(request);
    case "DELETE":
      return deleteFn(request);
    default:
      return null;
  }
}

クライアント側では、<form method={"POST" | "PUT" | "DELETE"} />で制御します。

データベースの保存

View, loader/actinができたら最後はDBへの保存です。

MVCに精通する方々は、RemixがVCを提供するフレームワークであることにお気づきになるかもしれません。

Remixは、ViewをReact、Controllerをloader/action関数を用いて構築ができますが、Model部分をどのように構築するかは開発者に委ねられます。

生SQL書くも、ORマッパーを使うも、外部APIを使うも自由に選択が可能です。

ただ、サーバーサイドでコードを動かすことができるという一点において、Reactよりも柔軟で堅牢なState管理を行うことができます。

とはいえ、何もとっかかりが無いのもそれはそれで、開発に困るのも事実であり、

Remixは、Remix Stacksで、数百万人のユーザーにサービスを提供する大規模かつ高速なプロダクショングレードのアプリケーションを想定したテンプレートプロジェクトを用意してくれています。

例えば、Blues Stackでは、PostgreSQL + Prismaを使用しています。

私も実際の開発環境では、このBlues Stackの構成を継承して開発を進めています。

Prismaは非常に使いやすいNode.jsのO/Rマッパーであり、普段はSQLをいじらない筆者であってもすんなりとDBの操作をすることが可能です。

Prismaを使うと、下記の3ステップでDBのスキーマ構築と操作が可能になります。

  1. schema.prismaの記述
  2. マイグレーション
  3. クライアントコードの記載

schema.prismaファイルは、TypeScriptでtypeinterfaceを書くかのようにDBのスキーマを定義できます。

schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(uuid())
  name      String
  email     String   @unique
  createdAt DateTime @default(now()) @map("created_at")
  post      Post[]  //

  @@map("user_account") // Schemaで利用する名前とDBのカラム名を変更する
}

model Post {
  id        String   @id @default(uuid())
  name      String
  content   String
  ownerId   User     @map("owner_id") @relation(fields: [ownerId], references: [id])
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@map("post")
}

スキーマを変更したらマイグレーションを行い、DBに変更を適用します。

npx prisma migrate dev

スキーマの内容をPrismaのクライアントライブラリにも変更を適用します。

npx prisma generate

これで、DBのスキーマの構築とアプリケーションコードでのDBの操作が可能です

つまり、loader/action関数でDBへの保存が可能になります。

epxort const loader = async ({ params }: LoaderArgs) => {
  const userId = params.userId // URLの動的パスの値を取得
  
  const data = await prisma.user.findUnique({
    select: {
      id: true,
      name: true,
      email: true,
    },
    where: { id: userId },
  });
  
  return json(data)
}

export const action = async ({ request }: ActionArgs) => {
  const user = currentUser(request)
  const formData = await request.formData()
  const name = formData.get("name")
  const content = formData.get("content")
  const data = await prisma.post.create({
    name,
    content,
    ownerId: user.id
  })
  
  return json(data)
}

この一連の開発を通して、Remixで下記のアーキテクチャを実現します。


Remix Data Flow

Remixの恩恵

State管理からの解放

Remixのアプリケーションで、State管理のための追加のライブラリは必要がありません。

Remixで取り扱うStateのほとんどは、サーバーサイドから供給されます。

Reactは受け取ったStateをコンポーネントに流す込むだけでよく、本当の意味で、インターフェース(コンポーネント)の管理を行うライブラリに集中することができます。

また、Remixでは、階層構造になったRoute内で、親Routeから子RouteへのStateの共有が可能です。

例えば、/app/というURLのStateを、/app/homeからアクセスできます。

組み込みで用意されたuseRouteLoaderData関数を使うことで、簡単に任意の親RouteのStateにアクセス可能で、実質的にグローバルなState管理を代替することができるのです。

アプリケーション全体で使うデータをあらかじめ親Routeのloaderで読み込んでおくだけで、全ての子Routeからアクセス可能です。

TypeSafeなクライアント/サーバー処理

Remixは、デフォルトでTypeScriptに対応しています。

loader/actionから返される全ての値は、クライアント側と型定義を共有することができます。

クライアントでは、全ての値に型がついた状態で取り扱うことができます。

Remixではこれを、tRPCGraphQLといったものに頼ることなく、シンプルな構成で実現しています。

サーバーサイドの開発体験の向上

Reactに慣れたエンジニアは、全てのコードをクライアント上に置こうとします。

しかし、現実のアプリケーションを開発しようとすると、サーバーサイドのコードを避けて通ることはできません。

認証/認可、DBへの保存、セッションの管理、上げだすとキリがありません。

Remixのコアコードはサーバーサイドです。

そして、Remixはサーバーサイドのコードを、まるでReact(つまりクライアントサイド)で書いているかのように記述することが可能です。

開発者は、Remixによってうまく抽象化されたエコシステムの中で、サーバーサイドとクライアントサイドを相互に行き来しながらコードを記述することができます。

コアコードはサーバーサイドにあるのに、開発体験はReactで開発しているような気分になる、それがRemixというフレームワークです。

パフォーマンス向上

昨今のWebパフォーマンスの向上とは、いかにしてクライアントに送信するデータの量を減らせるかに神経をすり減らしています。

Reactの莫大な量のコードをロードする仕組みを解決するために、SSRSSGといったものも開発されました。

Remixでも、クライアント側のロード時間を削減するためにあらゆるチューニングがされています。

RemixはSSRをサポート(むしろSSRしかサポートしていません!)しているため、サーバー上で実行可能なJSコードをクライアントに送信することはありません。

「Web標準に忠実に」という言葉の通り、HTMLやCSS,WebAPIで実現できるものにわざわざJSコードを使うこともありません。

もちろん、コンテンツのキャッシュにも気を使っており、毎回SSRを行なってしまうという特性にも、エッジコンピューティングというアプローチで解決を図っています。

Remixの辛いポイント

統一的なAPIエンドポイントを作るのに苦労する。

今時点で、最も頭を悩ませているのが、何度も同じ処理を行うコードをうまく共通化する方法が見出せていないことです。

Remixは、コードのコロケーションを非常に意識したコードを書くことになります。

前述した通り、loader/actionはそのエンドポイントを使いたいコンポーネントと同じファイルからエクスポートされます。

その場所でしか使わない処理であればこの方法でも問題ないのですが、アプリケーションを開発していく中では、いくつかの場所で同じような処理を呼び出したいケースは結構な頻度で発生します。

その場合、どのエンドポイントを真のマスターエンドポイントにするのか、という問題に悩まされます。

もちろん、app/route下であれば、自由にURLを生成できるので、Viewを持たないAPI
のみのエンドポイントを生やすことも可能です。

いずれにしても、どのエンドポイントからどのようなCRUDができるのかを管理する方法がないため、開発者が一生懸命に整合性を保ちながらコードを書く必要があります。

このコードを共通化すると言う点に関しては、Remixのコンポーネントと処理は別々で管理しなければならないと言うルールが、裏目に出ているなと感じます。

これに関しては、私の環境ではまだコード量が少ないため、同じようなコードをいくつかの箇所で転載する方法で対応しています。

今後、コードが肥大化していく中で、良い解決方法を模索する必要がありそうです。

まとめ

今回は、Remixの紹介と、プロダクト開発の環境で使ってみた所感をまとめてみました。

Remix自体は非常に洗練されたフレームワークであり、アプリケーションを開発する上で十分な機能を持っている感じます。

個人的には、Next.js一強時代に、突如として全く異なるアプローチでNext.jsとガチンコを始めていたRemixを応援したい気持ちではあります。

SPAの時代から、プログレッシブリー・エンハンスド・シングルページアプリ(PESPAs)の時代に突入を始めているWeb界隈において、新たなメンタルモデルを実現するフレームワークで遊べることはエンジニアとして非常に興奮を覚えます。

Remixは、PESPAsをリードするフレームワークとして、今後もどんどん成長を続けていくフレームワークになると思います。

ぜひ皆さんも、Remixによる快適フロント開発を体験してみてください。

最後になりますが、ここまで読んで頂けた方への些細なプレセントとして、Remixの開発をすぐに始められるようなブートストラップテンプレートを共有して終わりにしようと思います。

Remix Directory

技術スタックで検索すると、その技術スタックを用いたRemixアプリケーションを構築するためのテンプレートをゲットすることができます。

それでは、混沌としたモダンフロントエンドの海原に漂うエンジニアの皆さんが快適なフロントエンドライフを過ごせることを祈りながら、締めさせてもらいます。

Happy Hacking😎

Acompany

Discussion