Open8

Next.jsのプロジェクトをBunで作成して、ざっくりとシステムを構成したときの作業ログと感想文

raruraru

Next.jsをApp routerしながらBunで試してみる。storybookもしたい
OSはUbuntu24.04

オンデマンドISRでAPIはGolangでやりたい。=> 別にSSRでよくね?ってなり始めてる
小さく始めるならSSRで、その後ISR + Cloudflare workersとかでも良いんじゃないのかと思ったしそっちのほうがいいのではないかという気はした。

Bunのインストール

# 参照: https://bun.sh/docs/installation
$ curl -fsSL https://bun.sh/install | bash 

Next.jsのプロダクトををBunから作成する

https://bun.sh/guides/ecosystem/nextjs を参考に作成する
リンク先には、以下の記載がある。

The Next.js App Router currently relies on Node.js APIs that Bun does not yet implement. The guide below uses Bun to initialize a project and install dependencies, but it uses Node.js to run the dev server.

2024/05/01現在ではApp routerにはNodeにあってBunにないAPIが使われてるらしく、サーバ自体はNodeで起動されるらしい。
まぁ、BunをRuntimeとして考えずに開発環境として考えれば別に気にするほどでもないか。
むしろNodeで稼働するのでProductionはNodeで構築して、開発はBunの恩恵に預かりつつ動作を本番に合わせられると考えると好都合...?

$ cd ~/workspace
$ bun create next-app
✔ What is your project named? … my-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /home/raru/workspace/hoge/my-app.

Using bun.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss

bun install v1.1.6 (e58d67b4)

 + @types/node@20.12.7
 + @types/react@18.3.1
 + @types/react-dom@18.3.0
 + postcss@8.4.38
 + tailwindcss@3.4.3
 + typescript@5.4.5
 + next@14.2.3
 + react@18.3.1
 + react-dom@18.3.1

 131 packages installed [3.72s]
Initialized a git repository.

Success! Created my-app at /home/raru/workspace/hoge/my-app

pacages installed 3.72s
速い、気がする。
試しにNodeで試したところ 11sでした。
だいぶ速い!

開発サーバの起動も試してみた

# $ bun run dev はNodeとして実行
$ bun --bun run dev # こちらはbunで実行。ただしApp routeを利用する場合は結局Nodeで実行される
$ next dev
  ▲ Next.js 14.2.3
  - Local:        http://localhost:3000

 ✓ Starting...
 ✓ Ready in 1782ms

Node側のプロジェクトは1691ms
サイト記載の通りサーバはどちらもNodeのようだった。

raruraru

Next.jsにVite?

Next.jsのモジュールバンドラにViteを利用してみたくて調べてみた。
結論Viteを適用しようとしないほうが良さそう。というか、viteをnextに適用するという発想自体がイビツのようだった。

Next.jsのバンドラは?

まずNext.jsはzero-configを一つの思想として持っていました。
そのためあまり設定を利用者にさせないように工夫されています。

Next.jsは元々Webpackを内部的に利用しており、それを隠蔽して外部から拡張できるようにnext.config.jsという形に切り出しているっぽいです。そしてnext自体はwebpackの設定をゴリゴリにチューニングしている様子。
現在はWebpackのチューニングでは限界があるため、パフォーマンスを改善するために新規にTurbopackという後継のバンドラ作成してBeta版として取り込み始めてます。

Next.js自体にバンドラが組み込まれて設定されているため、別のバンドラを外側から利用する必要も意義もなさそうです。

Turbopack?

Turbopackはwebpack作成者がVercelに入社しつつwebpackの後継として開発しているものです。
2024/05/01現在も開発は継続中で、まだwebpackの全てを置き換えられている状態ではないみたい。
Next13から早期アクセスとして試すことができる状態となり、14現在は開発環境では利用可能。

next buildには利用されておらず、その部分はまだ未対応。
おそらく現在はnextでのみ利用可能で、今後はSvelteにも統合していく計画があるらしい。

viteを使う場合

Next.jsはSSRなどバックエンド側の機能も含まれているがviteがそこには対応していない様子。
そのためNext.jsのバンドラとして自力で適用すると利用できる機能に制限が出てしまうらしい。
zero-configの思想とも相反するため、Nextを利用するときはviteのことを考えないほうが良さそう。

そもそもViteのProject作成テンプレートにNextは含まれていない

raruraru

Projectのセットアップをコツコツ勧めてみる

Biomeを入れてみる

せっかくなのでBiomeを入れてみます。

# biomeのインストール
$ bun add --dev --exact @biomejs/biome

# 設定ファイルを作成
$ bunx @biomejs/biome init

入れるだけであれば、これだけで十分みたいです。

あとはこれをエディタから利用できるように、エディタ側の設定を追加する必要がありますね。
僕はNVIMユーザでCocを利用してる人なのでcoc-biomeを利用すれば良さそうです。
(話は変わりますがMasonにしておいたほうが良いんでしょうかね, nvim-lspconfigの方がいいのかな)

エディタを設定する

~/.config/nvim/coc-settings.json に以下を追加

{
  "coc.preferences.formatOnSave": true
}

:CocInstall coc-biome でbiomeのサポート機能をインストール or let g:coc_global_extensions = [ ..., 'coc-biome'] で自動インストールする対象に追加

これでエディタ上にLinterの警告表示とファイル保存時の自動フォーマットが適用されるようになりました。

設定ファイルを調整する

一旦以下のように設定。
今後使ってみて微妙に感じたら調整してみます。

vcs周りでgitignoreを参照してignoreなやつは無視されるようにしているつもり
ほかはほとんどbiomeに内部的にセットアップされてるオススメ設定を使ってみます。

{
  "$schema": "https://biomejs.dev/schemas/1.7.2/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "style": {
        "noDefaultExport": "off"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentSize": 2,
    "indentStyle": "space"
  }
}

小ネタ

Biomeは近くのbiome.jsonを参照して適用しようとするため、例えばフロントとバックエンドをモノレポで開発してる場合 {root}/front/biome.json, {root}/api/biome.jsonみたいに別々に管理できる

また設定の共通部分を切り出して、それを継承する形で枝分かれする設定も書けるらしい。
その場合はjsonにextendsプロパティで共通部分になるbiome.jsonのpathを指定すれば良いようだ。

個人的にはそんな複雑なことせずに一律揃えちゃったほうがシンプルでいいんじゃないかなと思う

raruraru

Storybookを導入して色々やってみる

storybookをプロジェクトに追加

$ bunx storybook@latest init

プロジェクトに追加後に自動起動した際に以下のような警告

⚠ Found lockfile missing swc dependencies, patching...
⚠ Lockfile was successfully patched, please run "npm install" to ensure @next/swc dependencies are downloaded

next/swcがpackage.jsonに追加されたみたいですね。
以下を実行して依存を取り込みましょう

$ bun install # 手元では特に差分はでなかった。警告は出ていたがインストールまで行われていたのかもしれない
$ bun run storybook # 実行後に警告がなければ問題なし

TODO

プロジェクトの雛形が出来たので、実際に開発をしながら調整をしてみる。

raruraru

認証認可

認証認可を実装しようとして悩んだり考えたことのメモ。書かれてる内容に正しい保証はありません。

Nextの認証系のライブラリにはNextAuth.jsというものがある。次のバージョンからAuth.jsという名前になりNext以外にも対応するらしい。
NextAuthではOAuth対応のいろんなサービスを利用してログイン機能が作れるよ!と謳っている

ここで疑問

OAuthで認証?

OAuthは認可の仕組みであり、認証の仕組みではないはず。
にもかかわらず、NextAuthは認証が行えると謳っている。なぜOAuthを利用して認証ができると言っているのだろうか?

OAuthを認証に使うのが危険な理由

まず技術的な理由の前に、そもそもの理由。
OAuthは認可の仕組みとして策定されているため、認証に対する仕様は明記されていない。
そのためOAuthの仕様を実装しても安全な認証を届けるための実装は行われない。
なので、危険。

では技術的には?
まずOAuthと一口に言っても、それにはいくつかのフローがある

  1. Authorization Code Grant
  2. Implicit Grant
  3. Resource Owner Password Credentials Grant
  4. Client Credentials Grant
  5. Refreshing an Access Token

5のリフレッシュトークンはリフレッシュトークンなので一旦無視する。
3,4も個人的には何故あえてこれを選択する必要があるのかよくわからないフローなので無視する。

1,2のフローがあるが、一般的に危ないと言われているのは2のimplicit flowの方(だと思う)
1,2の大きな違いはclient_secretをフローの中で利用するかどうか。

2のimplict flowではclient_secretを利用しない。
そのため偽サイトなどを利用して本サイトでも利用可能なトークンの発行が行えてしまい、危険。
またブラウザやアプリは情報を秘匿できないので、トークンの流出経路が多く危険。

そして上記の特性があるため、プロバイダに対して認証を行った人 = トークンを使ってユーザ情報取得を行った人、という状況が作れないため認証に使えないということだと思う。

一方Authorization Code Grantではclient_secretを利用してトークンの発行依頼元が担保できるのと、認可処理とその後のトークンを利用したAPI callがサーバの一連の流れでフロントに露出せずに行えるので、ユーザ情報取得APIを叩いている人 = プロバイダで認可取得のために認証処理した人 という状態が作れるため認証として利用可能なのだと思う。

NextAuthではどうなってる?

ドキュメントを読んでいる感じclient_secretを使うことを前提にしてる = 認可コードフローを利用する前提になっている、というもののよう。

しかしNext.jsにはSSGやISRなどの随時実行されるバックエンドの処理を持たない利用方法があります。
その場合にはどうなっているんだろう? と疑問が湧く。

これはどうもこうもなくて、ログインやユーザ登録ページに関してはSSRにする感じになりそうです。
NextはページごとにSSR, SSGを切り替えて利用できるようなので、NextAuthのサーバ側処理が必要なところはSSRとするしかなさそう。そりゃそうだ。

しかしNextとは別のAPIサーバを用意する場合に、ややこしくなりそう。
サブドメインにもクッキーを許可する感じにすれば良さそうではあるけど。
このあたりどうなるんでしょうか。NextAuthを使うならNextでバックエンドを実装するのが一般的なのかなぁ。
もしくはユーザ登録・ログインはNextをBFF的な感じで中継地点にして後ろに流す感じ? それはちょっと面倒くさいし無駄に複雑な感じがする。

Next以外に例えばスマホアプリがあるシステムでは?

NextAuthはもちろんNextでしか利用できない。
Nextでスマホアプリ用のAPIを実装しないのであれば、WEBサイトだけNextAuthを使っていても意味がない。
別言語のOAuthやOIDC系ライブラリを使って、それをWEBからもアプリからも呼ぶべき。

でもNextAuthのhookとかクライアントサイドでの取り回し部分だけ拝借したりってできないのかなぁ?

余談

OAuthは認可の仕組みだが、OAuthを実装したAPIが認証に絶対に使えないのかというと、そういうわけではない。
Facebook, Googleなど一部のプロバイダはOAuthを拡張して認証を行える仕組みを自社のOAuthの仕組みに拡張している。

OIDCはこの「独自拡張」が広まるのと、独自拡張されたものが結局セキュアじゃないじゃんってのを避けるために仕様策定されたんだろうな、と勝手に思っている。(あと普通に需要がある)

個人的な方針

NextAuthは使わない。
NextをISR、APIをGolangで書きたいから。そしてチャンスがあればFlutterとかしてみたいから。
認証はGolang側に実装する。

Google OAuth

OAuthといってるけど、どうもOIDCっぽい。
そのため例えばreact-oauthはImplicit Flowをデフォルトに実装されていそうだが、OAuthのImplicit FlowではなくOIDCのImplicit Flowであるためユーザ登録・ログインに使う程度の用途であればセキュアに利用できそう。

ただフロントとサーバにOIDCの一連の流れの処理が分散するのが嫌だし、とはいえImplicitじゃない方のフローのほうがセキュアだし、やっぱ自前でボタン作ってやるほうが楽だなぁ。
他のOIDCプロバイダに対してもボタンだけフロントで作れば処理共有できるだろうし。

raruraru

Next/Reactで遭遇した知らなかったこと・困ったこと

ClientComponentはサーバサイドでプリレンダリングされている

use clientをしてもサーバ側でプリレンダリングされているので、普通にHydration Errorは発生する。
普通にガッツリ勘違いしてた。

Google OAuth Buttonを公式のHTML API形式で使ったらHydration Error

しっかり解決するまで調査していないがonload時にdomが生成されまくるため、サーバ側で生成されるHTMLと差分が出てしまう様子。
JavaScript APIを利用してゴリゴリ書けば解決できると思われるが、そんなに頑張りたくない。

react-oauthというものがあるので、それが利用できそうではある。
onSuccessが必須なので、Implicit Flow前提で実装されてそう?
ux_mode=redirectだとcallbackいらないはずなので。

raruraru

ライセンス表記をどこにするのか

MIT LICENSEなどはある程度自由に利用できるが、第三者にわかるようにライセンス表記を行う必要がある。
この「ライセンスを表記すれば自由に使える」の「ライセンス表記」をどこにどうやって行うのか謎だったので改めて調べた。完璧に調べきってはいない。

非コピーレフト型、特にMITに限定して調べている

OSSでコードを公開する場合

リポジトリにThirdPartyLicenses.txtとか、誰かにわかるような感じでまとめておけば良い。

サーバサイドで製品として利用する

サーバサイドのコードは配布されないので、特にライセンスを表記する必要はない(はず)

スマホアプリに組み込む

アプリがバイナリとして配布されるので、ライセンス一覧ページなどを作成して明記する必要がある

WEBのフロント技術

これが一番よくわからなかったが、基本的には以下の感じのはず。

  • JavaScript, HTML, CSSはフロントに配布されるためコピーライトの記載が必要。
  • webpack4以下ではバンドルする際にコピーライトは削除されずそのまま残るようなので、バンドル後のソースの中に残っているから「基本」気にしなくていい
  • webpack5以降ではmain.jsにmain.js.LICENSES.txtへのリンクが貼ってあり、そのテキストの中にライセンスが一覧になっているため気にしなくていい。ただしちゃんとホスティングされて参照できるようにする必要はある。
  • viteでもビルド時にコピーライトはそのまま残る

上記のようになっているため、システムのリリース前に確認することは必要そうだがあまり気にしなくてもバンドラがよしなにやってくれているようだ
(よしなにやる機能がなかったら、おそらくMITライセンス違反だらけの世界だったに違いない)

なんでコピーライトが残るのか

JSDocで @license タグで記載されたコメントはコンパイル時に削除されないというルールがある。
トランスパイラ、バンドらもおそらくそのルールに則って処理を行っているので残っていると思われる。

また慣習として /*! で始まるコメントは残すような文化があるらしく、それで記載している場合も残るっぽい。ただしこれは慣習なので、JSDoc形式を利用するほうが良さそう。

npmなどで管理されてない野良のコードだけどMITなやつら

これはコードを流用させていただきつつ、流用したコードにJSDoc形式でコメントを残せば良いだろう
※ まだ試してみてないので、試してみる必要あり。

疑問

利用されるOSS側でJSDoc形式でライセンスコメントを残してなくLICENSE.txtにだけライセンスが分離され記載されている場合にもバンドラは良しなにやってくれるんだろうか?
やってくれないと、事故るよね。

またJSDoc形式で書いてなくて /*! で書いてなくて、でもソース上にMITライセンス記載がされているOSSだとどうなるのだろうか?
これも事故りそうで怖いですね。

package-lock.jsonには依存してるライブラリのライセンスプロパティがあるため、npmなどで管理されているものに関しては自分でそこから抽出する必要があるのかもしれない?

感想

ライセンスややこしすぎるし、完璧に対応されているのか確認するのとても労力が掛かりそう。

raruraru

Intercepting Routes

soft navigationのページ遷移をインターセプトして、別のページを呼び出したり出来る機能
Parallels Routesと組み合わせてモーダルに使われているサンプルがよく出てくる

Intercepting Routesを設定しても正常に動作しない場合、プロジェクトルートにある .next ファイルを削除してコンパイルし直すことで動作することがある。
インクリメンタルに処理してるとルーティング周りで適切に再コンパイルが入らなくて上手くインターセプトルートの設定が生きてこないっぽい

Route Groupとの組み合わせ

app/(hoge)/fuga/page.tsx をinterceptしたい場合
app/@modal/(.)fuga/page.tsxを配置すれば良い
Interceptはディレクトリに対しての指定ではなくてURLに対しての指定なのでRoute Groupは気にせずURLを意識して設定すれば動く

[...catchAll]のルーティングでモーダルを閉じる

モーダルをこれで作る場合 router.backやLink Componentでモーダルを閉じることが出来る 参照

例えばモーダルの中で簡単な登録・更新系の処理を作った場合、処理の結果に応じてrouter.pushしたくなる
このときモーダルがちゃんと閉じてくれない、ということになる。

これをパラレルルートしたいものとして定義されているもの以外は[...catchAll]で吸い込むようにして return null してあげるとモーダルが閉じられる

ちょっと正直煩雑だと思う
けどまぁ仕方ないのかなぁという気もする

あとそんな複雑なもの作るなよって言われそうだけど、モーダルの中で更に画面遷移するようなものとかって作れるのかな? 作れるんだろうけど、試しにやる気にはならないので必要になったら試してみたい。
でも必要にならないようにUIを考えたい

所感

  • モーダルにURLを付与できるので、リンク共有が出来ると便利なページには嬉しい
  • モーダルの管理にContextを使わなくても良いので、その点は煩雑にならなくて楽
  • モーダルはアクション後に閉じるだけでよいが、ページは必要に応じて画面遷移などをさせる導線になるためモーダルで呼ばれたときとページで呼ばれたときで処理に微妙な差が出るケースが有る
    • そしてその管理は煩雑になると思われる