フィーチャーフラグ(Feature Flags)は、コードをデプロイせずに機能の有効/無効を切り替える手法です。LaunchDarkly、Unleash、Flagsmith などのサードパーティサービスが有名ですが、シンプルな用途であれば自作も可能です。
今回、Vite 7 + React 19 + TanStack Router でカスタムフィーチャーフラグを実装するデモプロジェクトを作成しました。
- GitHub: vite-feature-flag-demo
フィーチャーフラグのメリット
| メリット | 説明 |
|---|---|
| 継続的インテグレーション | 未完成の機能を安全に main ブランチへマージ |
| 段階的リリース | 一部のユーザーにのみ機能を公開 |
| A/B テスト | 異なる実装を実際のユーザーでテスト |
| 即時ロールバック | 問題のある機能を再デプロイなしで無効化 |
| トランクベース開発 | 長期間のフィーチャーブランチを排除 |
トランクベース開発のフロー
従来の開発フローと、フィーチャーフラグを使ったトランクベース開発の比較です。
| 項目 | 従来のフロー | トランクベース開発 |
|---|---|---|
| ブランチ戦略 | 長期間のフィーチャーブランチ | main ブランチに直接マージ |
| PR サイズ | 大きな PR(数週間分の変更) | 小さな PR(こまめにマージ) |
| マージ頻度 | 機能完成時に 1 回 | 毎日〜数日ごと |
| リスク | マージ時のコンフリクト大 | コンフリクト小、常にデプロイ可能 |
| 機能の公開 | マージ = リリース | フラグ ON でリリース |
フィーチャーフラグを使うことで、未完成の機能でも main ブランチにマージできます。フラグを OFF にしておけばユーザーには見えず、完成したらフラグを ON にするだけでリリースできます。
なぜカスタム実装なのか
| 観点 | カスタム実装 | サードパーティサービス |
|---|---|---|
| コスト | 無料 | $50-500+/月 |
| 複雑さ | シンプル | SDK 統合が複雑 |
| 制御 | 完全に自社管理 | ベンダー依存 |
| 機能 | 基本的(拡張可能) | 高度(ターゲティング、分析) |
| レイテンシ | なし(クライアントサイド) | ネットワークリクエスト必要 |
| データ所在地 | 自社インフラ | 日本リージョンがない場合も |
シンプルな用途であればカスタム実装で十分です。また、企業によっては外部 SaaS の利用に制約があったり、データの所在地(日本リージョンの有無)が問題になるケースもあります。そうした制約がある場合、カスタム実装は有力な選択肢です。
高度なターゲティングや分析が必要な場合は、サードパーティサービスの検討をお勧めします。
技術スタック
今回のデモプロジェクトで使用した技術スタックです。
| 技術 | バージョン |
|---|---|
| Vite | 7.3.1 |
| React | 19.2.4 |
| TypeScript | 5.9.3 |
| TanStack Router | 1.158.0 |
| Turborepo | 2.8.3 |
| Biome | 2.3.14 |
| pnpm | 10.9.0 |
| Node.js | 24.13.0 |
プロジェクト構造
pnpm workspaces と Turborepo を使ったモノレポ構成です。
apps/web - メインの React アプリ
| ファイル | 説明 |
|---|---|
src/routes/__root.tsx | ルートレイアウト(FeatureFlagProvider を配置) |
src/routes/index.tsx | ホームページ(デモ UI) |
src/main.tsx | エントリーポイント |
vite.config.ts | Vite 設定 |
packages/feature-flags - フィーチャーフラグライブラリ
| ファイル | 説明 |
|---|---|
src/index.ts | パブリック API エクスポート |
src/types.ts | TypeScript 型定義 |
src/FeatureFlagContext.ts | React Context 定義 |
src/FeatureFlagProvider.tsx | Provider コンポーネント |
src/useFeatureFlag.ts | カスタム React Hooks |
ルート設定ファイル
| ファイル | 説明 |
|---|---|
turbo.json | Turborepo パイプライン設定 |
pnpm-workspace.yaml | pnpm ワークスペース定義 |
2つのパターン
このプロジェクトでは、2つの異なるパターンでフィーチャーフラグを実装しています。
| パターン | 解決タイミング | 用途 |
|---|---|---|
| React Context(ランタイム) | 実行時 | A/B テスト、ユーザーターゲティング、動的トグル |
| 環境変数(ビルドタイム) | ビルド時 | 環境別設定、CI/CD 統合 |
環境変数パターン
Vite の環境変数を使って、ビルド時にフラグを埋め込むパターンです。VITE_FF_ プレフィックスを使用します。
# .env
VITE_FF_EXPERIMENTAL_CHECKOUT=true
VITE_FF_ANALYTICS_DASHBOARD=true
VITE_FF_MAINTENANCE_MODE=falseimport { isEnvFlagEnabled } from "@demo/feature-flags";
if (isEnvFlagEnabled("maintenanceMode")) {
return <MaintenancePage />;
}環境変数名は SCREAMING_SNAKE_CASE から camelCase に自動変換されます。
| 環境変数 | 変換後のフラグ名 |
|---|---|
VITE_FF_NEW_DASHBOARD | newDashboard |
VITE_FF_DARK_MODE | darkMode |
パターンの使い分け
| 観点 | React Context | 環境変数 |
|---|---|---|
| 再ビルドなしで変更 | 可能 | 不可 |
| ユーザー別ターゲティング | 可能 | 不可 |
| 環境別設定 | 複雑 | ネイティブサポート |
| バンドルサイズ | 最小限 | なし(デッドコード除去) |
両方のパターンを組み合わせることも可能です。
import { useFeatureFlag, isEnvFlagEnabled } from "@demo/feature-flags";
function MyComponent() {
// ビルドタイム: この環境で機能が利用可能か?
const isFeatureAvailable = isEnvFlagEnabled("newFeature");
// ランタイム: このユーザーは機能にロールアウトされているか?
const isUserEnrolled = useFeatureFlag("newFeatureRollout");
if (isFeatureAvailable && isUserEnrolled) {
return <NewFeature />;
}
return <LegacyFeature />;
}実装の詳細
フィーチャーフラグライブラリの構成
React の Context API を使った軽量な実装です。
型定義
// フィーチャーフラグはシンプルな key-value マップ
type FeatureFlags = Record<string, boolean>;
// Context で提供される値
type FeatureFlagContextValue = {
flags: FeatureFlags;
setFlag: (key: string, value: boolean) => void;
isEnabled: (key: string) => boolean;
};Provider コンポーネント
import { FeatureFlagProvider } from "@demo/feature-flags";
const defaultFlags = {
newDashboard: true, // デフォルトで有効
darkMode: false, // デフォルトで無効
betaFeature: false, // デフォルトで無効
};
function App() {
return (
<FeatureFlagProvider flags={defaultFlags}>
<YourApp />
</FeatureFlagProvider>
);
}カスタム Hooks
import { useFeatureFlag } from "@demo/feature-flags";
function Dashboard() {
const isNewDashboardEnabled = useFeatureFlag("newDashboard");
if (isNewDashboardEnabled) {
return <NewDashboard />;
}
return <LegacyDashboard />;
}よくあるパターン
パターン 1: コンポーネントの切り替え
function MyFeature() {
const useNewImplementation = useFeatureFlag("newImplementation");
return useNewImplementation ? <NewComponent /> : <OldComponent />;
}パターン 2: 条件付きレンダリング
function Header() {
const showBetaBadge = useFeatureFlag("betaFeature");
return (
<header>
<h1>My App</h1>
{showBetaBadge && <span className="badge">BETA</span>}
</header>
);
}パターン 3: 動作の変更
function SubmitButton() {
const useAsyncSubmit = useFeatureFlag("asyncSubmit");
const handleClick = () => {
if (useAsyncSubmit) {
submitAsync();
} else {
submitSync();
}
};
return <button onClick={handleClick}>Submit</button>;
}セキュリティの考慮事項
クライアントサイドフィーチャーフラグの限界
この実装はクライアントサイドのフィーチャーフラグです。重要なセキュリティ上の注意点があります。
| 注意点 | 説明 |
|---|---|
| すべてのコードがバンドルに含まれる | フラグが OFF でも、保護されたコンポーネントのコードは JavaScript バンドルに含まれます |
| フラグはバイパス可能 | ユーザーはブラウザの DevTools で React の state を変更し、任意のフラグを有効化できます |
| ソースコードは閲覧可能 | ソースマップやバンドル解析で「隠された」機能のコードが見えます |
つまり、フラグが OFF でも /admin や /beta のコンポーネント、フィーチャーフラグのロジックなど、ビルドされた JavaScript バンドル内のすべてのコードはユーザーから閲覧可能です。
保護レベルの比較
| レベル | 方法 | 防げる対象 | ユースケース |
|---|---|---|---|
| 1. UI トグル(本デモ) | 条件付きレンダリング | カジュアルユーザー | UX 実験、段階的リリース |
| 2. 遅延読み込み | React.lazy() + 動的 import | 軽度のコード検査 | やや良いが URL はアクセス可能 |
| 3. サーバーサイド認証 | API 認可チェック | 不正なデータアクセス | 機密データには必須 |
| 4. ビルド時除外 | 環境変数でビルド | コード露出 | 環境別の別ビルド |
推奨アーキテクチャ
| レイヤー | 役割 | 備考 |
|---|---|---|
| クライアントサイド | UI の表示制御のみ(UX 目的) | セキュリティ用途には使用不可。クライアントのコードはすべて公開情報と考える |
| サーバーサイド | 認証・認可・データフィルタリング | ユーザーの身元確認、リクエストごとの権限チェック、フラグのサーバーサイド評価、許可されたデータのみ返す |
クライアントサイドフラグが適切な場面
- A/B テスト(UI バリエーション)
- UI 変更の段階的リリース
- ベータ機能プレビュー(非機密)
- ダークモード、テーマ切り替え
- UI レイアウト実験
サーバーサイド保護が必要な場面
- 機密データを扱う管理画面
- プレミアム/有料機能
- ユーザーデータのアクセス制御
- セキュリティに関わるすべての機能
本番環境での考慮事項
このデモは意図的にシンプルに作られています。本番環境では以下を検討してください。
フラグの永続化
const [flags, setFlags] = useState(() => {
const saved = localStorage.getItem("featureFlags");
return saved ? JSON.parse(saved) : defaultFlags;
});
useEffect(() => {
localStorage.setItem("featureFlags", JSON.stringify(flags));
}, [flags]);リモートからのフラグ取得
useEffect(() => {
fetch("/api/feature-flags")
.then((res) => res.json())
.then((remoteFlags) => setFlags(remoteFlags));
}, []);ユーザーベースのターゲティング
const flags = {
betaFeature: user.isBetaTester,
adminPanel: user.role === "admin",
};環境ベースのフラグ
const flags = {
debugMode: import.meta.env.DEV,
newFeature: import.meta.env.VITE_ENABLE_NEW_FEATURE === "true",
};まとめ
React Context を使ったシンプルなフィーチャーフラグシステムを実装しました。
- 軽量: 外部依存なし、React のみ
- 型安全: TypeScript で完全に型付け
- トランクベース開発: 未完成機能を安全にマージ可能
ただし、クライアントサイドフィーチャーフラグはセキュリティ目的には使えません。機密性の高い機能には、必ずサーバーサイドでの認証・認可を実装してください。
デモプロジェクトを clone して試してみてください。
git clone https://github.com/codenote-net/vite-feature-flag-demo.git
cd vite-feature-flag-demo
pnpm install
pnpm dev以上、Vite + React でカスタムフィーチャーフラグを実装してトランクベース開発を試してみた、現場からお送りしました。