RailsエンジニアのためのNext.js入門

というタイトルで先日 Kaigi on Rails 2021 で話してきました。

プレゼンで話せなかった内容なども含めてブログ記事にも書いておきます。

Intro

Railsのことはけっこう知ってるけどNext.jsについて何も知らないという人をターゲットにしてNext.jsとは一体何なのか、いつどこで使えばいいのか、具体的にNext.jsのどういうところがいいのか、どういう機能があるのかという話をします。

最終的には普段Railsを書いているエンジニアが、Next.jsよさそうなんで使ってみようかな?と思ってもらえるといいかなと思っています。

Next.jsとは何か

Next.jsのトップページを見てみましょう。

f:id:hokaccha:20211022225231p:plain

The React Framework for Production

と書いてあります。これは読んで字のごとくですが、Next.jsというのはReactをベースにしたフレームワークです。

では具体的にNext.jsの何がいいのか、というのはその下に書いてあります。

Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed.

後半のほうは具体的な機能の説明が書かれていますが、個人的には"best developer experience"の部分、最高の開発体験を提供すると書いてあるところがけっこう重要で、Next.jsはめちゃくちゃ開発体験がよいのがいいところの一つだと思っています。

これはRailsと共通するところがあると思っていて、僕がRailsを好きな理由の一つは開発体験の良さなので、RailsエンジニアもきっとNext.jsの開発体験は好きになれるんんじゃないかと思います。

Reactをベースにしたフレームワークということで、Next.jsはフロントエンド方面の開発体験がよさそうなWebアプリケーションフレームワークぽい、というのはなんとなくわかったと思いますけど、じゃあどういうときに使うのがいいんでしょうか。Next.jsはRailsを置き換えて次世代のRailsになるようなフレームワークなんでしょうか?答えは明確にNoです。

RailsとNext.jsを比較するために、まずはRailsの構成要素を見てみましょう。

f:id:hokaccha:20211022225346j:plain

DBがあってActive RecordがあってAction PackがあってAction Viewがあって、必要に応じて使うActive JobとかAction Mailerみたいなものある、と。まあこれはRailsエンジニアに対しては特に説明しなくてもいいと思います。

それからフロントエンドに各種もろもろのやつがありますね。今回はNext.jsとの比較ということでフロントエンドのコンポーネントについては解像度高く細かく書いていますが、まあ色々ありますね。ではNext.jsはRailsでいうどの領域をサポートするものでしょうか。

こんな感じです。

f:id:hokaccha:20211022225405j:plain

Next.jsがカバーする領域はAction Packより下のレイヤーです。フロントエンドのフレームワークなので、Viewより下を見ることについては特に疑問はないと思いますけど、Action Packのレイヤーもカバーしているところに注目してください。

Action Packというのはリクエストを受け取って、ルーティングしてコントローラーにリクエスト渡してレスポンスをクライアントに返すような責務の層です。つまりNext.jsはHTTPのサーバーを起動してリクエストをハンドリングするという機能も持っているということです。当然ですけどサーバーサイドのプロセスはNode.jsで動きます。

それから、Active Recordのレイヤーがないというのも重要なポイントですね。Next.js自信はDBの操作レイヤーを持ちません。もちろんサーバーサイドでNode.jsのプロセスが動くのでDBに接続してデータを読み書きすることは可能ですが、そこは自前で書く必要があります。

なんでNext.jsを使うのか、Railsではダメなのか

Next.jsがRailsでいうとどのあたりの機能を提供するフレームワークかはわかったと思いますけど、Railsの一部のレイヤーしかサポートしないのであれば全部入りのRailsのほうがよくない?という話になりそうなので、そのあたりについて説明していきます。

具体的な理由は、Railsのフロントエンド開発がつらい、というところですね。これらは僕自身RailsでViewをずっと書いてきていてペインに感じるところです。

1つ目は独自路線な技術スタックです。一昔前のsprockets、Webpackerとか最近だとHotwireやImportmapなどですね。それらがダメ、React最高というつもりはイチミリもないですし、流行ってるからReactがいいというつもりも全くないんですが、既存のエコシステムに乗っかれるというメリットはやっぱり大きいですね。採用とか開発のモチベーションにも関わってくるので独自路線の技術選定だとそのあたりが厳しくなるのかなと思ってます。JavaScriptのbundler界隈が盛り上がるまえにsprocketsを発明したDHHはすごいと思っているし、僕自身sprocketsは好きだったんですけど、最近はメインストリームとの乖離がやはり気になりますね。

それと、やはり単純にReact、TypeScriptの体験がよすぎるのでそこを最大化する技術スタックでやるのが筋がいいんじゃないかというのもあります。

それから、JavaScriptRailsのViewが混ざるとどこにViewが書いてあるかわからなくなる問題、あると思います。ただこのあたりはRails7でだいぶ改善するのかなと思っているので期待したいところではあります。

独自路線が嫌でReact使いたいなら、がわだけRailsでbodyの中だけReactにすればいいのでは、という話もあったりするんだけど、それはそれでPre Renderingできないという問題があってつらかったりします。airbnbhypernovaという別プロセスでNode.js実行してサーバーサイドでJSをレンダリングするという手法もあったりしますが、これはこれで運用がきついです。Pre Renderingできないことによる問題については後半で詳しく説明します。

これらのペインをNext.jsだと解決できるのでしょうか。答えはYesです。もはやデファクトスタンダードになったと言ってもよいReactを中心に据えて開発できて、言語はJavaScript、もしくはTypeScriptに統一できます。最近だとよほど特別な理由がない限りはTypeScriptを使うべきだと思います。

こういった理由でNext.jsを使うといい感じになるよ、という話ですが、逆にNext.jsだけ使えばRailsいらなないの?という話はどうでしょう?これはNoです。

Next.jsだけだと、やはりActive Recordのレイヤーがないのがきついですね。これを独自にやろうとするとかなり厳しいことになると思います。Node.jsのORMであるPrismaはわりとよさそうですし、フルスタックスレームワークのBlitz.jsなども出てきましたが、個人的にはRailsの生産性には及ばないかなという印象です。

RailsとNext.jsを使ったアーキテクチャ

ですので、現状だとRailsなどのバックエンドの得意なフレームワークと組合わてNext.jsを使うのがベターな選択肢だと考えています。具体的にどういう感じのアーキテクチャになるかを見ていきましょう。

f:id:hokaccha:20211022230158p:plain

まずはこんな感じのシンプルな3層アーキテクチャです。プレゼンテーション層にNext.js、アプリケーション層にAPIモードのRailsをおきます。もちろんこれが唯一の最適解ということではなくて、アプリケーション層とプレゼンテーション層のプロセスを分けるのは実装や運用面で複雑化するデメリットはあるのでチームメンバーのスキルセットやプロダクトの特性によって最適解を選ぶ必要があります。

例えば僕は両方それなりに得意なので、たぶん今自分が一人で何かプロダクトを作り始めるなら、0->1のスピード優先フェーズであってもこの構成にしますね。それぐらいNext.jsの開発体験がよくてトータルだと速度がでると思います。逆にチームメンバーがReact何もわからなくてRailsでViewを書くことに抵抗のないケースではRailsだけで済ませるのがいいですね。まあこれは当たり前の話ですね。

3層アーキテクチャにしておくメリットは、クライアントがWeb以外にもネイティブアプリなどがあるパターンに対応しやすいというのもありますね。

f:id:hokaccha:20211022230206p:plain

いつ増えるかわからないクライアントに対応するために無理にこの構成にするのはやめたほうがいいけど、最初からWebの他にもクライアントがあることがわかっていればWebアプリケーションのためのプレゼンテーション層としてNext.jsを挟むのは自然なアーキテクチャと言えると思います。

それからシステムが大きくなってきて、マイクロサービス的にバックエンドのアプリケーションを分けていくパターンがあると思います。

f:id:hokaccha:20211022225542p:plain

ほとんどのサービスにマイクロサービスなんていらんのやという論調もありますが、まあそれは一旦置いておいて、ここではそういうシステムがあるとします。そういう場合にNext.jsをWebフロントエンドのためのプレゼンテーション層として立てて各マイクロサービスのアグリゲーションもここでやってしまうという方法もあります。

ただこういうケースではアグリゲーションレイヤーを一枚噛ませるのがベターだと思います。

f:id:hokaccha:20211022225550p:plain

現状だとGraphQLがこのアグリゲーション層の候補にあがりそうなのでここではGraphQLとしていますが、別にGraphQLである必要はありません。

このアーキテクチャは前職のクックパッドで採用したアーキテクチャなんですけど、偶然にも現職のUbieでも同一のアーキテクチャになっています。GraphQLのサーバーはクックパッドだとNode.js、UbieではKotlinだったりしますが、アーキテクチャとしては同じです。それとUbieでは裏のマイクロサービス群はほとんどがKotlinですね。

GraphQLの話をしだすと脱線してしまうのでまた別の機会にするとして、システムが大きくなってきたときに取り得るアーキテクチャとしては現状ではそこそこ現実的な解の一つではないかと思っています。

Next.jsの機能

次にもう少し具体的にここがすごいよNext.js、というのを見ていきましょう。

Zero Config

Next.jsの開発環境をつくるのは、まずNext.jsやReactを含むいくつかのモジュールをインストールして、Reactコンポーネントのファイルを一個おいて、next devコマンドを実行する、以上です。

$ yarn add next react react-dom
$ yarn add --dev typescript @types/react
$ mkdir pages
$ cat > pages/index.tsx <<EOL
const TopPage = () => {
  return <h1>Hello Next!</h1>;
};
export default TopPage;
EOL
$ yarn run next dev

WebpackとかTypeScriptなどのめんどくさい設定は一個もありません。正確にはtsxコンポーネントを作ったときにtsconfig.jsonが自動生成されますが、それぐらいです。

create-next-appみたいな雛形を一発で生成してくれるrails newみたいなものもありますが、個人的には雛形生成系は何が必要で何が必要じゃないかが全然わからないので、必要なものだけを足していきたいので、実際にアプリケーションを書くときもこのような手順でセットアップしています。

とはいえ同じ雛形生成系のrails newはそう思わないのはなんででしょうかね。全部必要だからかな。

File-system Routing

  • GET /
  • GET /posts
  • GET /posts/:id

このようなルーティングをつくるのにRailsだとroutes.rbにこんな感じで書きますよね。

Rails.application.routes.draw do
  root "root#index"
  resources :posts, only: [:index, :show]
end

まあこれは特に説明の必要はないですね。Next.jsだと、設定を書く必要がなくて、ディレクトリ構成を以下のようにします。

pages/
├── index.tsx
└── posts/
    ├── [id].tsx
    └── index.tsx

ファイル名、ディレクトリ名から自動的にルーティングを生成します。pagesというディレクトリ名だけが特殊で、Next.jsはpagesディレクトリ以下のファイルを特別に扱います。また、postsの下に[id].tsxというのがありますが、これで任意のパラメータをURLから受け取ることができます。この大かっこの記法は初見だとだいぶ気持ち悪いしシェルの入力とかとも相性悪いんですけど、まあこれは慣れてください。

pages以下のファイルの中身はこんな感じになっています。

Reactコンポーネントをexport defaultする、というのが規約です。さっきの[id]のidを受け取るにはnext/routerを使って受け取ることができます。ここで受け取ったidをAPIに投げてデータを取得する、などが一般的なフローですね。

import { useRouter } from 'next/router'

const PostPage = () => {
  const router = useRouter();
  const id = router.query.id;
  return <h1>Post ID: {id}</h1>;
};

export default PostPage;

Performance Optimization

次はパフォーマンス最適化の話ですね。パフォーマンスといっても色々なパフォーマンスがありますけど、ここでいうパフォーマンスはCore Web Vitals的なパフォーマンスです。LCP, CLS, FID という指標があります。

f:id:hokaccha:20211022225617p:plain

Next.jsのパフォーマンス最適化は色々あって、めちゃくちゃパフォーマンスの最適化に力を入れているフレームワークといえます。Webパフォーマンスの最適化は色々なプラクティスがあるのでこの時間で説明はできませんが、Next.jsデフォルトで様々な最適化をおこなってくれるので、何も考えずに使うだけである程度は速くなる、と雑に思ってもらえればいいと思います。もちろん遅いコードを書けば遅くなるので過信は禁物ですが。

next/imageというコンポーネントを使うと画像の最適化をしてくれたり、next/scriptで外部スクリプト読み込みの最適化を行ってくれます。Web fontの最適化などもありますね。

それから、next/linkを使って画面遷移をするとクライアントサイドで画面遷移します。Railsエンジニアの人にわかりやすいように説明するとturbolinksということです。いい感じのturbolinksです。ようするに画面遷移するときにHTMLを丸ごと読み直すのではなく、遷移するページのコンテンツをJSで取得してきてURLをJSで書き換えることでページ遷移したように見せる、というやつですね。これによって高速な画面遷移を実現できます。turbolinksはRailsを普段使いしている人の間でも悪名高い存在ですけど、next/linkはよくできているので安心安全なturbolinksと思ってもらうといいですね。

Pre Rendering

Pre Renderingの説明の前にPre Renderingじゃないケースの説明をします。Client Side Rendering(以下CSR)です。

f:id:hokaccha:20211022225637p:plain

CSRはその名の通りクライアントサイドのJavaScriptでViewを描画するというやつです。CSRでは、ブラウザはまず特定のページのURLにHTTPリクエストしてHTMLを受け取ります。このときHTMLはCSSとJSを参照するタグだけ書いてあって中身は空です。

ブラウザはJSの参照先にリクエストしてJSファイルを受け取ったらJSを実行します。JSにはAPI呼び出しのコードが書いてあるのでAPIにリクエストしてデータを受け取ったら、そのデータを元にViewを構築します。これでやっとユーザーにはコンテンツが表示されるわけですね。

CSRにはいくつか問題があります。

  • 初期描画のパフォーマンスがでない
  • JavaScriptを実行しないクライアントに弱い(OGP, SEO

初期描画のパフォーマンスがでないということですね。上記の図のようにユーザーにコンテンツが表示されるまでブラウザは何度もリクエストを発行する必要があります。

もう一つがJavaScriptを実行できないクライアントに対してコンテンツを提供できないということです。SEOについては最近はGoogleのボットがJavaScriptを実行してくれるので問題になることは少なくなってきましたが、OGPなどは依然として問題になります。例えば商品詳細ページのURLをSNSに貼ったときに全部JavaScriptで実行されるページだとその商品の情報がSNSに表示されなくなるわけですね。いい加減OGP問題どうにかならんかと思ってますけどどうにかならないですかね。

画面の一部をCSRするような場合でも問題になるようなケースがあります。例えばメインのコンテンツはサーバーサイドでレンダリングするけど、特定のユーザーだけにバナーを出す、みたいなのをクライアント側でやる場合ですね。これは初期描画やOGPなどは問題になりませんが、CLSを悪化させてしまう可能性があります。このようにCSRはいくつかの問題点を抱えています。

ではどうするかというと、クライアントサイドでレンダリングするのではなく、あらかじめサーバーサイドでHTMLを作って返せばいいということになります。Railsエンジニアにとっては、え、何いってんの、普通じゃない?と思うかもしれないですけど、まあそうです。ただJavaScriptでViewを作る場合、基本的にはクライアントサイドでレンダリングするのが普通なので、それをいかにしてサーバーサイドでレンダリングするか、という発想になるわけですね。

方法は2つあって、1つ目がリクエストを受けたときにサーバーでHTMLを返します。Server Side Rendering(以下SSR)と呼ばれるやつですが、Railsが普段やっているのと同じと思ってもらって大丈夫です。

もう一つがStatic Site Generation(以下SSG)で、予め静的にHTMLを作っておいてそれを返すという手法です。これはJekyllとかHugoみたいな静的サイトジェネレーターを想像してもらうとわかりやすとい思います。

CSRSSR、SSGがそれぞれがどういうケースで有効なのかについても説明しておきます。

まずCSRですが、これはさっきダメな点をいくつかあげたんですけど、いいところもあって、実装や運用がシンプルになるということで。実装はクライアントサイドのことだけ考えればいいし、運用は基本的には静的アセットを配信するだけで済みます。場合によってはnginxとかでルーティングだけ必要になる可能性はありますが、その程度です。なので先程あげたCSRの欠点が受け入れられるのであればCSRを選択するのは良い選択といえます。例えばGmailみたいなアプリケーション系とか、管理画面とかそういうところですね。

次にSSGですが、これはビルド時にコンテンツが決まるようなサービスで選択します。基本的には静的なHTMLを配信することになるのでパフォーマンスは最強なので静的にビルドできる場合はSSGを選ぶべきですね。ブログとかメディア系のサイトが典型的なユースケースです。

最後に静的にコンテンツは決まらないけどパフォーマンスもOGPも妥協したくないというパターンでは泣く泣くSSRを選択します。ただ社会というのは得てして厳しく、要件的にSSRじゃないと無理、というケースはそれなりにあります。例えばクックパッドのレシピページでは300万超えのレシピを静的に生成はできないし、OGPもパフォーマンスも重要な要件でした。

それからNext.js独自の機能してIncremental Static RegenerationというSSRとSSGの間の子のような機能もあります。これは基本的にはSSGで静的コンテンツを作りつつ、コンテンツがない場合はオンデマンドでファイルを生成したり、生成したファイルの生存期間を超えるとサーバー側で再生成してくれるというものです。SSR+ファイルキャッシュのようなものと捉えるとわかりやすいかもしれません。

Next.jsにおいてはCSR,SSR,SSGのどれか一つを選択しないといけないというわけではなく、一つのアプリケーションの中で併用することができます。

例えば動的にデータが変わる商品ページはSSR、事前にデータがわかるヘルプページはSSG、パフォーマンスやOGPを気しない管理画面はCSR、のような感じですね。

最後にNext.jsの具体的なコードを出しておきます。Next.jsでSSRにするには、pages以下においたコンポーネントgetSeverSidePropsという関数を定義して exportします。この名前はNext.js の規約です。

const PostPage = ({ post }) => {
  return (
    <div>
      {post ? <Post post={post} /> : <Loading />}
    </div>
  );
};

export async function getServerSideProps({ params }) {
  const post = await fetchPost(params.id);
  return { props: { post } };
}

この関数はリクエストを受け取ったときに実行されて、データをコンポーネントに渡します。コンポーネントは受け取ったデータを元にHTMLを作ってクライアントに返します。これでクライアントはコンテンツがすでに入っている状態のHTMLを受け取ることができます。

SSGも基本的には同じで、getStaticPathsgetStaticPropsという関数をexportします。

const PostPage = ({ post }) => {
  //...
};

export async function getStaticPaths() {
  const posts = await fetchPosts();
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }));
  return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
  const post = await fetchPost(params.id);
  return { props: { post } };
}

SSRと違ってビルド時に対象のコンテンツがわかっている必要があるので、まずはビルド対象のコンテンツ、ここでは post id のリストを getStaticPaths で生成して、それをもとにgetStaticPropsがidの数だけ呼ばれてHTMLが事前に生成されます。

まとめ

最初にNext.jsの概要についてRailsと比較しつつ説明しました。とにかくNext.jsは開発体験がよくてRailsエンジニアであれば好きになると思います。たぶん。好みというのは人それぞれなので予防線は貼っておきます。

もしこの発表を聞いてちょっとNext.js使ってみようかな、と思ってくれたら嬉しいです。

最後に

私が今所属しているUbie Discoveryでは、Next.jsもRailsも使っているしサーバーサイドはKotlinがメインだったりします。技術的でも事業的でも興味あると思った方は、ぜひカジュアルにご連絡ください。