React開発者Dan Abramov氏のRSCの解説がとてもわかりやすいぞ!
React開発者Dan氏のRSCに関する記事 「How Imports Work in RSC」 を読みました。
これからReact,Next.jsを触る方、あるいはRSCの基本概念を再確認したい方に向けて、仕組みを分かりやすく解説します。
はじめに
近年、Next.jsなどのフレームワークで標準化されつつある React Server Components (RSC) は、フロントエンド開発のパラダイムを大きく変えました。RSCを正しく理解するためには、実はReactそのものの知識よりも、「JavaScriptのモジュール(import/export)がどのように動作するか」を深く理解する必要があります。
Dan氏の記事をもとに 「モジュールシステムの基礎の部分からRSCがどのようにサーバーとクライアントの境界を管理しているか」 を解説します

1. モジュールシステムの基本原理
なぜモジュールが必要なのか
コンピュータがプログラムを実行する際、本来「モジュール」という概念は必要ありません。コンピュータはすべてのコードとデータがメモリ上に展開されていればそれで良いのです。モジュールは、人間が複雑なプログラムを分割して理解しやすくするために存在します。
モジュールシステムの役割は、人間が分割して書いたコードを、コンピュータが実行できる形(メモリ上のプログラム)に「展開」することです。
import は「コピぺ」ではない
直感的には、import は他のファイルからコードをコピーしてくるように思えるかもしれません。しかし、JavaScriptのモジュールシステムは、C言語の #include(文字通りファイルを埋め込む挙動)とは決定的に異なります。
JavaScriptのモジュールは「シングルトン(単一の存在)」です。
あるモジュールが複数の場所から import されたとしても、そのモジュール内のコードは最大で1回しか実行されません。
以下のコードを見てください。
/* c.js */
export function c() { return 2; }
/* a.js */
import { c } from './c.js';
export function a() { return c() * 2; }
/* b.js */
import { c } from './c.js';
export function b() { return c() * 3; }
/* index.js */
import { a } from './a.js';
import { b } from './b.js';
const result = a() + b();
console.log(result);
このプログラムが実行される際、JSエンジンは以下のような単一のプログラムと同等な挙動になります。c() 関数が2回定義されたり実行されたりすることはありません。
// 実行時のイメージ
function c() { return 2; } // c は1回だけ定義・実行される
function a() { return c() * 2; }
function b() { return c() * 3; }
const result = a() + b(); // 10
この「シングルトン」という性質により、状態の保持が可能になり、プログラムサイズの爆発的な増加を防いでいます。
2. サーバーとクライアント:2つのコンピュータ、2つのプログラム
従来のSPA(Single Page Application)開発やRSC以前の構成では、フロントエンド(ブラウザ)とバックエンド(サーバー)は完全に別のコンピュータで動く、別のプログラムでした。
共有コードの落とし穴
フロントエンドとバックエンドの両方をJavaScriptで書く場合、便利なユーティリティ関数などを共有したくなります。しかし、ここで重要な原則があります。
ある環境からコードを
importすると、そのコードはその環境へ「持ち込まれる(bring into)」。
つまり、utils.js をサーバーとクライアントの両方からインポートした場合、それらは共有されているように見えて、実際にはそれぞれの環境に独立してコピーが存在することになります(各環境ごとにシングルトンになります)。
「ビルドエラー」は味方である
コードを共有する際、片方の環境でしか動かないコード(例:サーバー側の fs モジュール)を誤ってフロントエンドに持ち込んでしまった場合どうなるでしょうか?
/* c.js - 共有モジュールのつもり */
import { readFileSync } from 'fs'; // サーバー専用API
export function c() {
return Number(readFileSync('./number.txt', 'utf8'));
}
これをフロントエンドから import すると、ビルドエラー("Cannot import 'fs'")が発生します。これは良いことです。間違いにすぐ気づけるからです。
一番怖いのはエラーが吐き出されないこと
最も恐ろしいのは、ビルドエラーにならずに、本来公開すべきでない情報(秘密鍵など)がフロントエンドに含まれてしまうケースです。
/* secrets.js */
export const secret = 12345; // サーバーの秘密鍵
/* c.js */
import { secret } from './secrets.js';
export function c() { return secret; }
もしフロントエンドが c.js をインポートしてしまうと、secret は静かにブラウザ向けバンドルに含まれてしまい、セキュリティ事故につながります。
3. 「Poison Pills(毒薬)」による境界防御
誤ってサーバー専用コードをクライアントに送らないようにするにはどうすればよいでしょうか?
ここで登場するのが server-only というパッケージ(通称:Poison Pill / 毒薬)です。
server-only の仕組み
このパッケージは、クライアント(ブラウザ)向けのビルドに含まれると意図的にビルドを失敗させるように作られています。
/* secrets.js */
import 'server-only'; // 👈 これを追加!
export const secret = 12345;
このように宣言しておけば、誰かが誤って secrets.js をフロントエンドからインポートした瞬間、import の連鎖を辿って server-only が読み込まれ、ビルドが失敗します。これにより、「うっかりミス」をシステム的に防ぐことができます。
同様に、client-only を使えば、window や prompt などのブラウザ専用APIを含むコードがサーバーで実行されるのを防ぐことができます。
4. RSCにおけるモジュールシステム
これまでの説明で、「サーバーとクライアントは独立したモジュールシステムを持つ」「混ぜるな危険」 ということがわかりました。React Server Components (RSC) は、この2つの世界を統合するアーキテクチャですが、魔法を使っているわけではありません。
RSCは以下の2つの仕組みで成り立っています。
-
Poison Pills (
server-only/client-only): 特定の環境に持ち込まれてはいけないコードを防御する。 -
ディレクティブ (
"use client"/"use server"): 2つの世界をつなぐ「ドア」を作る。
"use client" とは何か?
サーバー(バックエンド)のコードを書いていて、インタラクティブなUI(ボタンなど)を使いたいとします。しかし、単にフロントエンドのコンポーネントを import してしまうと、そのコードはサーバー側に「持ち込まれて」しまい、サーバー上で実行されようとしてしまいます(そして window がないなどでエラーになります)。
サーバーからフロントエンドのコードを「参照」したいが、「実行(取り込み)」したくない。
この時に使うのが "use client" です。
/* button.js */
'use client'; // 👈 私はクライアント側のモジュールです!という宣言
export default function Button({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
サーバー側がこのファイルをインポートしても、コードの中身は実行されません。代わりに、「これはクライアント側で処理されるべきコードへの参照(ドア)である」 として扱われます。結果として、ブラウザには <script> タグのような形で届けられ、クライアント側で復元・実行されます。
"use server" とは何か?
逆に、クライアント(フロントエンド)からサーバーの関数を呼び出したい場合、単に import するとその関数のコード自体がブラウザに「持ち込まれて」しまいます(データベース接続コードなどがブラウザに含まれてしまう)。
"use server" は、クライアント側からサーバー側の関数を参照できるようにしますが、コード自体は持ち込みません。これは実質的に、自動的にAPIエンドポイントを作成し、そこへリクエストを送るような挙動になります。
5. まとめ
再度この画像を見てみましょう。かなりスッキリするのではないでしょうか?

RSCのメンタルモデルは以下の通りです。
-
2つの独立したモジュールシステムがある
RSCアプリは、サーバーとクライアントという2つの環境にまたがる単一のプログラムのように見えますが、内部的には独立したモジュールシステムを持っています。 -
importはコードを「持ち込む」
通常、何かをインポートすると、そのコードは実行環境に取り込まれます。 -
Poison Pills (
server-only/client-only) で守る
重要なコードが誤った環境に「持ち込まれる」のをビルド時に防ぎます。 -
ディレクティブ (
"use client"/"use server") は「ドア」である
これらはコードを実行場所を指定するものではなく、一方の世界からもう一方の世界へ情報を渡すための「参照(ドア)」を作るためのものです。
この構造を理解すれば、一見複雑に見えるNext.jsやRSCのエラー(「なぜこのモジュールが見つからないのか?」「なぜこの変数がundefinedなのか?」)の原因を、モジュールシステムの原理から論理的に推測できるようになります。
Discussion