😱

Next13のappでサーバーとクライアントのコード混入時にビルドを止める

2022/10/26に公開約3,600字

Next 13のappディレクトリは、React 18のサーバー/クライアントコンポーネントの概念を前提として実装されており、開発者にもその理解が要求されます。そのため、まずは以下のBetaドキュメントを熟読しましょう。

https://beta.nextjs.org/docs/rendering/server-and-client-components

特に注意してほしいのは、イベントリスナーや状態変化を前提としたコンポーネントは、use clientでクライアントコンポーネントだと明示する必要がある点です。

また、明確にgetXXXPropsに分離できていた通信関連の関数を、必ずサーバーコンポーネントから呼ぶ必要が生じます。

import サーバーコンポーネント(デフォルト) クライアントコンポーネント(use client)
サーバー想定コード
(A: CORSエラーや意図しない漏洩の発生)
クライアント想定コード
(B: windowを使ってしまった等の場合)

この区別を蔑ろにすると、意図しないコードの混入が発生したが、ビルドが通ってしまって気づかない 場合がこれまで以上に増えるでしょう。

この記事ではAとBのパターンで確実にビルドを止める方法を解説していきます。

サーバー限定コードが誤ってクライアント側に混入する場合

通信が含まれていれば、大抵はビルド時にCORSエラーで引っかかり気づくのですが、そうならない場合、エラーなくビルドを終えて、page-XXX.jsにサーバー側のコードをバンドルしてしまいます。

get-dog.ts
export async function getDog() {
  // 実際はここで通信などが行われると想定
  return `${process.env.SECRET_DOG_BREED}大好き!`;
}

例えば上記の関数をgetServerSidePropsからコンポーネントに移動したとしましょう。

NEXT_PUBLIC無しの環境変数が使われていることから、これを「サーバーサイドで動く前提で書かれたが、説明不足でごっちゃになってしまった」ということにしましょう。

ClientComponent.tsx
"use client";

import { use } from "react";
import { getDog } from "../lib/get-dog";

const ClientComponent = () => {
  const dog = use(getDog());
  return <div>{dog}</div>;
};
export default ClientComponent;

次に、use clientを明示したクライアントコンポーネントで上記の関数を使ってみます。

コード混入の結果

NEXT_PUBLIC接頭辞がないため環境変数の流出は免れますが、本来バンドルに含まれないはずのコードが丸見えになります。

ビルドを止めよう

とにかく想定していない漏洩を発生させないために、そもそもビルド時に例外を投げる方向でいきましょう。

https://beta.nextjs.org/docs/rendering/server-and-client-components#keeping-server-only-code-out-of-client-components-poisoning

こんなときのためにserver-onlyというパッケージが用意されています。

npm i server-only
get-dog.ts
+ import "server-only";

export async function getDog() {
  return `${process.env.SECRET_DOG_BREED}大好き!`;
}

サーバーを想定したコードのいずれかの場所で、import "server-only"するだけです。

You're importing a component that needs server-only. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.
(サーバー限定コンポーネント(というか関数)をインポートしてますよ。それはサーバーコンポーネントだけで動きますが、use clientなのでそれクライアントコンポーネントです)

ビルドが止まるようになりました。ローカルで動かしている際もエラーになります。

クライアント限定コードが誤ってサーバー側に混入する場合

client-only.ts
export function getWindowSize() {
  return [window.innerWidth, window.innerHeight];
}
ServerComponent.tsx
import { getWindowSize } from "../lib/client-only";

/**
 * 動くわけがない
 */
const ServerComponent = () => {
  const [width, height] = getWindowSize();
  return (
    <div>
      ウィンドウサイズ: {width}x{height}
    </div>
  );
};
export default ServerComponent;

app内のデフォルトはサーバーコンポーネントですから、これは動くわけ無いですね。

npm i client-only
client-only.ts
+ import "client-only";

export function getWindowSize() {
  return [window.innerWidth, window.innerHeight];
}

server-onlyと同様に、該当ファイルにimportするだけです。

You're importing a component that imports client-only. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
(クライアント限定コンポーネント(というか関数)をインポートしてますよ。それはクライアントコンポーネントだけで動きますが、親のどれもuse clientを使ってないので、それらはデフォルトでサーバーコンポーネントです)

ビルドが適切なエラーメッセージとともに止まるようになりました。


以上、ビルドを確実に止める方法でした。

既存のgetXXXPropsを前提とした知識のアップデートが必要なため、ようやくNextの採用が増え始めた日本のフロントエンド界隈で、この変化に対応できるチームはかなり限られるんじゃないかなと思います。

まあそもそもベータ版ですから、今のところは、新規に趣味のプロジェクトを作った際に使うぐらいですかね。

Discussion

ログインするとコメントできます