Vercelが提供するFeature flagsをNext.jsと合わせて利用する

## はじめに
Vercelでは[Feature Flags](https://vercel.com/docs/workflow-collaboration/feature-flags)と呼ばれる特定の機能のOn・Offを切り替える機能が提供されています。

Feature Flagsを使えば、特定のタイミングでのみ表示させる機能や、アクセス時にランダムで異なるパスへ振り分ける機能を簡単に作れます。

この記事ではNext.jsで`Feature Flags`を使う方法と、Vercelへデプロイしたときに使える便利な機能を紹介します。

この記事で利用したコードはGitHub上に上げています。

https://github.com/k35o/vercel-feature-flag

フラグの動きはVercel上にアップロードしたサイトで確認出来ます

https://vercel-feature-flag.vercel.app

:::note
`Feature Flags`は執筆現在、`beta`版として公開されています。製品版になるまでにいくつか変更が加わる可能性があるのでご了承ください。
:::

## Next.js
Feature Flags機能は`@vercel/flags`パッケージを用いて利用します。
```command
pnpm i @vercel/flag
```

パッケージをインストールしたら、環境変数を`Flags_SECRET`を登録します。公式では次のコマンドで生成することを勧めています。
```command
node -e "console.log(crypto.randomBytes(32).toString('base64url'))"
```
Vercelのツールバーと連携してフラグを上書したときにその値を読み取るために使用するようです。

フラグを管理するファイル`flag.ts`を用意します。

```ts:flag.ts
import { unstable_flag as flag } from '@vercel/flags/next';

export const showFeatureA = flag({
  key: 'feature-a',
  decide: () => false,
});
```
`unstable_flag`関数を用いて利用するフラグを定義しています(今後は単に`flag`関数と呼びます。)。`showFeatureA`は機能Aを表示するかしないかを判定する非同期関数です。`await showFeatureA()`でフラグを取り出します。

引数の`key`はこのフラグを識別するための一意な文字列です。

もう1つの引数である`decide`はフラグの値を決めるための関数です。この場合は常に`false`を返すので、Offになります。`decide`には非同期な関数も渡せます(`showFeatureA`が非同期関数なのはおそらくこのためです)。

`decide`を利用すれば環境変数をベースにフラグを切り替えることもできます。
```ts
import { unstable_flag as flag } from '@vercel/flags/next';

export const showFeatureA = flag({
  key: 'feature-a',
  decide: () => process.env.FEATURE_A_ENABLED === '1',
});
```

引数はそのほかにも、`decide`が`undefined`を返した時のフラグ値となる`defaultValue`やフラグの説明を記述する`description`、フラグが管理されるURLを指定する`origin`、フラグの値によってどのようにVercelのツールバーに表示するかを決める`options`があります。

定義した`showFeatureA`は非同期コンポーネントで以下のように呼び出します。
```tsx:app/page.tsx
import { showFeatureA } from "./../flag";

export default async function Home() {
  const featureA = await showFeatureA();
  return (
    <main className="flex flex-col items-center justify-center h-screen gap-8">
      {featureA ? <div>機能1</div> : <div>デフォルト</div>}
    </main>
  );
}
```

`decide`が`false`を返せば「デフォルト」が表示され、`decide`が`ture`を返すば「機能1」が表示されます。

`middleware`を使えばA・Bテストのようなこともできます。
まず、50%の確率で`showTypeA`関数のresolvedされる値が`true`になるフラグを定義します。
```ts:flat.ts
import { unstable_flag as flag } from '@vercel/flags/next';

...

export const showTypeA = flag({
  key: 'type-a',
  decide: () => Math.random() > 0.5,
});
```

それを元に、`middleware`でレスポンスを書き換えます。
```ts:middleware
import { NextResponse, type NextRequest } from 'next/server';
import { showTypeA } from './flag';

export const config = { matcher: ['/dashboard', '/dashboard/type-a'] };

export async function middleware(request: NextRequest) {
  const typeA = await showTypeA();

  const url = typeA ? '/dashboard/type-a' : '/dashboard';

  if (request.url.endsWith('type-a')) {
    request.nextUrl.pathname = '/dashboard';
    return NextResponse.redirect(request.nextUrl);
  }

  const nextUrl = new URL(url, request.url);
  return NextResponse.rewrite(nextUrl);
}
```
50%の確率で`/dashboard`へのアクセスを`/dashboard/type-a`の内容で表示するようにしました。

`middleware`で実行することで、`/dashboard`の内容も`/dashboard/type-a`の内容も他のページと同じように構築できるので、`middleware`の分岐処理を除いてパフォーマンス上の欠点がないのが良いところです。。

## Vercelのツールバーで書き換える
この状態でVercelにアプリケーションを公開して、Vercelのツールバーを見てみましょう。(本番でツールバーを見るにはいくつかの準備が必要です。[add-to-production](https://vercel.com/docs/workflow-collaboration/vercel-toolbar/in-production-and-localhost/add-to-production)を読んでください)

[私が作成したこれまで紹介したフラグを実装したサイト](https://vercel-feature-flag.vercel.app/)で確認できます(ツールバーを利用するにはVercelにログインが必要です。ツールバーが見えなければコメント等で教えてください)。

https://vercel-feature-flag.vercel.app/

ツールバーにあるトグルボタンのような見た目をした`Flags Explorer`をクリックするとフラグについて管理するUIが出てきます。
![スクリーンショット 2024-12-23 17.49.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/bacf1ee2-1dbd-77d9-faa2-901fa8f6faa5.png)
その内容は`.well-known/vercel/flags`にフラグの内容を記述してくれというものです。![スクリーンショット 2024-12-23 20.16.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/b3a1e687-db91-a197-61fb-813d25e961c6.png)
Route Handlerで記述しましょう。伝える内容は`@vercel/flags`で`ApiData`として提供されているのでそれを元に作成します。
```ts:app/.well-known/vercel/flags/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { verifyAccess, type ApiData } from '@vercel/flags';

export async function GET(request: NextRequest) {
  const access = await verifyAccess(request.headers.get('Authorization'));
  if (!access) return NextResponse.json(null, { status: 401 });

  return NextResponse.json<ApiData>({
    definitions: {
      ['feature-a']: {
        options: [
          { value: false, label: 'Off' },
          { value: true, label: 'On' },
        ],
      },
      ['type-a']: {
        options: [
          { value: false, label: 'Off' },
          { value: true, label: 'On' },
        ],
      },
    },
  });
}
```
`verifyAccess`でログインしていないユーザーを弾くようにして、`flag.ts`で作ったフラグを再定義しています。

このように情報をツールバーから取得できるようにすると、ツールバー上でフラグを切り替えるUIに切り替わります。
![スクリーンショット 2024-12-23 20.30.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/b9260e60-9d91-74be-6f5d-c8493bf8fd54.png)

`feature-a`のトグルボタンを切り替えて、Applyを押すとそのフラグが画面に反映されます。

`dashboard`を開いてから`type-a`のトグルボタンをonにしてApplyを押すと「通常のダッシュボード」が必ず表示されます。offにすると「type-aのダッシュボード」が表示されます。
A・Bテストを実行しつつも、開発側で表示する画面を決められるのはかなり嬉しいです。

## おわりに
`Feature Flags`でできることを簡単に紹介しました。

この記事で紹介した機能の他にも、LCPや画面のチラつきを抑制するフラグの事前読み込みや、Vercelが提供する他のパッケージと組み合わせて利用する機能、ツールバーで上書きした値についてCookieをもとに検証する`decrypt`機能などさまざまな機能が提供されています。

Vercelを利用している人にとってはかなり便利なので、使ってみてはいかがでしょうか。