use server が宣言していたのは、実行場所ではありませんでした

転職用のポートフォリオとして、個人でWebアプリ「Hubpin」を作っています。
制作の経緯は「Hubpin 開発記録」に時系列でまとめています。
設計と判断は自分で行い、実装も原則自分で書いています。AIには手順書・レビュー・調査を任せています。

自作アプリ「Hubpin」(Next.js 16 + Supabase)でログイン機能を作っていたとき、
手が止まった場所があります。

App Router では、何も書かなければコードはサーバーで動きます。
なのに Server Action には 'use server' と書く。

既定と同じことを、どうしてわざわざ宣言するのか。

目次

公式の説明を読んでも解けませんでした

まずドキュメントを見にいきました。書いてあったのはこれです。

The use server directive designates a function or file to be executed on the server side.

サーバー側で実行される関数として指定する、と読めます。
けれど既定でサーバーなので、これだと意味が通りません。

しばらく「自分の理解が足りないのだろう」と思っていたのですが、
既定でサーバーなのに宣言が要る、というその事実のほうが答えでした。

実行場所を決める記述ではないから、書く意味がある。

宣言していたのは公開範囲でした

同じドキュメント群の中に、Server Action の仕組みを説明した箇所があります。

'use server' を書くと、ビルド時にコンパイラがその関数の中身をクライアント側から抜き取って、
アクションIDとディスパッチャの組に置き換えます。
ブラウザに配られるのは本体ではなく、そのIDへ POST する代理のほうでした。

呼び出しの形は普通の関数と同じです。中身はサーバーに残っています。
ただし、同じ POST を送れる人なら誰でもその経路に届きます。

公式はこう書いています。

Treat every action as an untrusted entry point.

すべてのアクションを、信頼できない入口として扱うこと。

実行場所の宣言だと読んでいた記述が、実際には入口を開ける宣言でした。

Network タブに POST が飛んでいました

理屈だけだと半信半疑だったので、開いて確かめました。

DevTools の Network タブを開いた状態でログインフォームを送信すると、
/login への POST が1本飛びます。

関数を呼んでいるように書いたコードが、HTTPリクエストになって出ていく。
ここで「代理を渡している」が実物として見えました。

読んで分かった気になっていたところが、1本のリクエストで腑に落ちています。

だから認可をUIに置けません

ここまで来ると、次の記述の意味が変わります。

Render-time gating (only rendering a form on an authenticated page) is not a security boundary,
because requests can be sent without going through the UI.

ログイン済みのページにしかフォームを出さない、はセキュリティの境界にならない。
UIを通さずにリクエストを送れるからです。

ボタンを出さなければ押されない、という発想が通用しません。
押す代わりに、直接 POST を送れば同じ経路に届きます。

だから認可は、画面ではなく関数の中で持つ必要がありました。

ちなみに、どこからも呼ばれていない Server Function はクライアントのバンドルから除去されるので、
公開エンドポイントを持ちません。
危ないのは、自分で呼べるようにしたもののほうです。

忘れた頃に返ってきました

同じ日の夕方、Server Component の中でインライン関数を <form action> に渡したら怒られました。

Functions cannot be passed directly to Client Components

IDを持たない関数は渡せない、という話です。

午前に読んだ「IDを振って代理を渡す」が、そのままエラーメッセージとして戻ってきました。
理解した順番と、それが効いてくる順番は違うのだと思いました。

おわりに

ディレクティブの名前と、それが実際に決めていることがずれていました。
use server は場所を指しているように読めますが、決めているのは公開範囲のほうです。

名前から意味を推測して止まっていたら、認可を画面側に置く実装を書いていたと思います。
分からないまま進めず、公式まで見にいったのが結果的に効きました。

次回は、同じ「ビルドが何をしているか」の話で、
"use client" を書いたページが静的に焼けていた件を書きます。

目次