server-only と client-only の使い方
モジュールを間違った側から使わせないための印が server-only と client-only です。
#共有したモジュールは、どちらからでも呼べてしまう
Server Component と Client Component は、同じ lib/ の中のモジュールを使えます。便利ですが、危険でもあります。
// lib/env.ts
export function getAdminKey() {
return process.env.ADMIN_KEY
}
このファイルには「サーバー専用です」とはどこにも書いてありません。誰かが Client Component から呼んでも、エディタは何も言いません。
#「NEXT_PUBLIC_ が無ければ安全」は間違い
いちばん多い誤解がこれです。実際に確かめました。
上の getAdminKey を Client Component から呼び、ADMIN_KEY=s3cr3t... を与えて本番相当のビルドを行い、成果物を検査した結果です。
| 成果物 | 秘密の値 |
|---|---|
| 配信される JavaScript | 含まれない |
| プリレンダーされた HTML | 含まれる |
NEXT_PUBLIC_ が決めているのは「その環境変数をブラウザ向けの JavaScript に埋め込むかどうか」だけです。
一方 Client Component は、初回表示のためにサーバーでも一度描画されます。そのときサーバーには本物の値があります。だから描画結果に混ざり、HTML として配信されます。
つまり、
- 仕組みが守っているのはバンドル
- 守っていないのは描画結果
です。画面に出していなくても、属性に入れただけでも、HTML には残ります。
#server-only を付けると、ビルドで止まる
// lib/env.ts
import 'server-only'
export function getAdminKey() {
return process.env.ADMIN_KEY
}
この1行を足すと、Client Component から呼んだときにビルドが失敗します。
./lib/env.ts:1:1
Error: 'server-only' cannot be imported from a Client Component module
It should only be used from a Server Component.
止まる場所が「公開したあと」から「ビルド」まで上がります。これが server-only の価値です。
判定は import の依存グラフで行われます。直接呼んでいなくても、Client Component からたどり着ける位置にあれば止まります。
#client-only は、その逆
window や localStorage に触るモジュールには client-only を付けます。
// lib/viewport.ts
import 'client-only'
export function widthOf() {
return window.innerWidth
}
Server Component から呼ぶと、こうなります。
./lib/viewport.ts:1:1
Error: 'client-only' cannot be imported from a Server Component module
It should only be used from a Client Component.
付けていない場合は「window is not defined」という実行時エラーになります。実行時のエラーは、条件によっては本番でだけ、画面のある部分でだけ起きます。ビルドで止められるなら、そのほうが確実です。
詳しくはwindow is not definedを読んでください。
#npm から入れなくても動く
Next.js 16.3.3 は server-only と client-only の解決を本体側で行います。インストールしなくても動きます。
公式ドキュメントも、インストールは任意だと明記しています。依存関係の記述を厳しく検査する設定を使っている場合だけ、入れておくとよい、という位置づけです。
pnpm add server-only
#どこに付けるか
| モジュール | 付ける印 |
|---|---|
| データベースに触る | server-only |
| API キー・トークンを読む | server-only |
| 認証・権限の判定 | server-only |
| window・localStorage に触る | client-only |
| DOM の寸法を測る | client-only |
| 純粋な計算・整形 | 付けない(どちらからも使ってよい) |
迷ったら「これがブラウザに届いて困るか」で決めてください。困るなら server-only です。
props としてブラウザへ渡ってしまう値の話はブラウザに何が届くかに、渡せる値の制限はServer から Client へ渡せる値にまとめてあります。