Turborepo で Zustand スライスをパッケージ化:Monorepo 運用の初期設定

複数のアプリケーションで状態管理のロジックを共有したいとき、どのようにコードを整理していますか?コピー&ペーストで同じコードを複数箇所に配置すると、メンテナンスが大変になりますよね。
Turborepo と Zustand を組み合わせることで、状態管理のロジックをパッケージとして整理し、複数のプロジェクト間で効率的に共有できます。この記事では、Zustand のスライスパターンを Turborepo で管理する初期設定方法を、実際のコード例とともに段階的に解説していきます。
モノレポ環境での状態管理に悩んでいる方、チーム開発でコードの再利用性を高めたい方にとって、きっと役立つ内容になるはずです。それでは、一緒に見ていきましょう。
背景
Monorepo における状態管理の課題
現代のフロントエンド開発では、複数のアプリケーションやサービスを 1 つのリポジトリで管理する Monorepo が広く採用されています。Web アプリケーション、管理画面、モバイルアプリなど、複数のプロジェクトが同じビジネスロジックを共有するケースが増えてきました。
しかし、状態管理のコードをどのように共有するかは悩ましい問題です。各プロジェクトで個別に実装すると、同じようなコードが重複してしまいます。また、仕様変更があった際には、すべてのプロジェクトを修正しなければなりません。
以下の図は、Monorepo における典型的な状態管理の課題を示しています。
mermaidflowchart TB
repo["Monorepo<br/>リポジトリ"]
subgraph apps["アプリケーション層"]
web["Web アプリ"]
admin["管理画面"]
mobile["モバイルアプリ"]
end
subgraph states["状態管理コード(重複)"]
state1["ユーザー状態<br/>(重複1)"]
state2["ユーザー状態<br/>(重複2)"]
state3["ユーザー状態<br/>(重複3)"]
end
repo --> apps
web -.-> state1
admin -.-> state2
mobile -.-> state3
style state1 fill:#ffcccc
style state2 fill:#ffcccc
style state3 fill:#ffcccc
図で理解できる要点:
- 各アプリケーションが独自の状態管理コードを持つと重複が発生する
- 仕様変更時には複数箇所の修正が必要になる
- コードの一貫性を保つことが困難になる
Zustand スライスパターンとは
Zustand は軽量でシンプルな状態管理ライブラリです。その中でも「スライスパターン」は、状態を機能ごとに分割して管理する設計手法として注目されています。
スライスパターンでは、大きな Store を機能単位の小さなスライスに分割します。たとえば、ユーザー情報、商品情報、カート情報など、それぞれを独立したスライスとして定義するのです。
typescript// ユーザースライスの例
interface UserSlice {
user: User | null;
setUser: (user: User) => void;
clearUser: () => void;
}
typescript// スライスを作成する関数
const createUserSlice: StateCreator<UserSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
});
このパターンの利点は、各スライスが独立しているため、再利用しやすく、テストもしやすくなることです。さらに、複数のスライスを組み合わせて 1 つの Store を構築できます。
typescript// 複数のスライスを組み合わせた Store
const useStore = create<UserSlice & ProductSlice>(
(...args) => ({
...createUserSlice(...args),
...createProductSlice(...args),
})
);
以下の図は、スライスパターンの構造を視覚化したものです。
mermaidflowchart LR
subgraph store["Zustand Store"]
direction TB
userSlice["User Slice<br/>(ユーザー状態)"]
productSlice["Product Slice<br/>(商品状態)"]
cartSlice["Cart Slice<br/>(カート状態)"]
end
subgraph components["コンポーネント"]
userComp["ユーザー<br/>プロフィール"]
productComp["商品一覧"]
cartComp["カート"]
end
userSlice --> userComp
productSlice --> productComp
cartSlice --> cartComp
style userSlice fill:#e1f5ff
style productSlice fill:#fff4e1
style cartSlice fill:#ffe1f5
図で理解できる要点:
- 各スライスは独立した機能単位で定義される
- コンポーネントは必要なスライスのみを参照できる
- スライス単位での再利用が容易になる
Turborepo の役割
Turborepo は、Monorepo を効率的に管理するためのビルドツールです。Vercel が開発しており、高速なビルドとキャッシュ機能が特徴となっています。
Turborepo の主な役割は以下の通りです。
# | 役割 | 説明 |
---|---|---|
1 | パッケージ管理 | ワークスペース内の複数パッケージを一元管理 |
2 | タスク実行最適化 | 依存関係に基づいた並列実行とキャッシュ |
3 | ビルドパイプライン | 効率的なビルドフローの構築 |
4 | 開発体験向上 | 高速なフィードバックループの実現 |
Turborepo では、turbo.json
というファイルでビルドパイプラインを定義します。これにより、どのパッケージをどの順序でビルドするか、キャッシュをどう活用するかを細かく制御できるのです。
json{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}
Zustand のスライスを Turborepo で管理することで、各スライスを独立したパッケージとして扱えます。変更があったスライスだけを再ビルドし、他のスライスはキャッシュを利用するため、ビルド時間を大幅に短縮できるでしょう。
課題
従来の状態管理パッケージ化における問題点
状態管理のコードをパッケージ化しようとすると、いくつかの問題に直面します。まず、すべての状態を 1 つの巨大なパッケージにまとめてしまうケースが多く見られました。
typescript// すべてを含む巨大なパッケージ(アンチパターン)
export const useAppStore = create((set) => ({
// ユーザー関連
user: null,
setUser: (user) => set({ user }),
// 商品関連
products: [],
setProducts: (products) => set({ products }),
// カート関連
cart: [],
addToCart: (item) =>
set((state) => ({ cart: [...state.cart, item] })),
// 通知関連
notifications: [],
addNotification: (notification) =>
set((state) => ({
notifications: [...state.notifications, notification],
})),
// ... さらに多くの状態が続く
}));
このアプローチには以下のような問題があります。
# | 問題点 | 影響 |
---|---|---|
1 | パッケージサイズの肥大化 | 必要のない機能まで含まれてしまう |
2 | 変更の影響範囲が大きい | 小さな修正でも全体を再ビルド |
3 | テストの複雑化 | 1 つの機能をテストするために多くの依存関係が必要 |
4 | チーム開発での競合 | 同じファイルを複数人が編集しやすい |
さらに、型定義の管理も課題となります。TypeScript を使用している場合、各状態の型を適切に定義し、共有する必要がありますが、巨大なパッケージでは型定義も複雑になってしまいます。
typescript// 複雑な型定義(メンテナンスが困難)
interface AppStore {
user: User | null;
setUser: (user: User) => void;
products: Product[];
setProducts: (products: Product[]) => void;
cart: CartItem[];
addToCart: (item: CartItem) => void;
notifications: Notification[];
addNotification: (notification: Notification) => void;
// ... 型定義が続く
}
スライスの依存関係管理の難しさ
スライスを分割してパッケージ化する場合、依存関係の管理が新たな課題となります。あるスライスが別のスライスに依存しているとき、適切に管理しないと循環依存や不要な結合が発生してしまうのです。
以下の図は、不適切な依存関係の例を示しています。
mermaidflowchart TD
userPkg["@repo/user-slice<br/>パッケージ"]
cartPkg["@repo/cart-slice<br/>パッケージ"]
productPkg["@repo/product-slice<br/>パッケージ"]
userPkg -->|依存| cartPkg
cartPkg -->|依存| productPkg
productPkg -->|依存| userPkg
style userPkg fill:#ffcccc
style cartPkg fill:#ffcccc
style productPkg fill:#ffcccc
note["循環依存が発生<br/>ビルドエラーの原因に"]
productPkg -.-> note
図で理解できる要点:
- スライス間で循環依存が発生すると、ビルドが失敗する可能性がある
- 依存関係が複雑になると、変更の影響範囲が予測しづらくなる
- パッケージの独立性が損なわれる
具体的には、以下のような問題が発生します。
typescript// cart-slice.ts(カートスライス)
import { User } from '@repo/user-slice'; // ユーザースライスに依存
export interface CartSlice {
items: CartItem[];
user: User; // ユーザー情報を直接保持(結合度が高い)
addItem: (item: CartItem) => void;
}
typescript// user-slice.ts(ユーザースライス)
import { CartItem } from '@repo/cart-slice'; // カートスライスに依存
export interface UserSlice {
user: User | null;
recentCartItems: CartItem[]; // カート情報を直接保持(循環依存)
setUser: (user: User) => void;
}
このような循環依存を避けるためには、依存関係を一方向にする設計が重要です。しかし、実際のアプリケーション開発では、機能同士が密接に関わることが多く、適切な境界を引くのが難しいのです。
ビルド・キャッシュの最適化課題
Monorepo 環境では、ビルド時間の最適化が重要な課題となります。すべてのパッケージを毎回ビルドすると、開発のフィードバックループが遅くなり、生産性が低下してしまいます。
従来のビルドシステムでは、以下のような非効率が発生していました。
# | 非効率なパターン | 問題 |
---|---|---|
1 | 全パッケージの再ビルド | 変更のないパッケージまでビルドしてしまう |
2 | キャッシュの不適切な管理 | キャッシュが効かず、同じ処理を繰り返す |
3 | 依存関係の不明確さ | 不要なパッケージまでビルド対象になる |
4 | 並列実行の不足 | 順次実行により時間がかかる |
bash# 従来のビルドコマンド(全パッケージを順次ビルド)
yarn workspace @repo/user-slice build
yarn workspace @repo/product-slice build
yarn workspace @repo/cart-slice build
yarn workspace @repo/notification-slice build
# ... すべてのスライスを順番にビルド
特に、スライスの数が増えるほど、この問題は深刻になります。10 個、20 個とスライスが増えていくと、ビルド時間が数分から数十分にまで膨れ上がることもあるのです。
さらに、CI/CD 環境では、毎回すべてをビルドすることになるため、デプロイまでの時間も長くなってしまいます。開発チームが大きくなり、複数の機能を並行して開発する場合、この問題はボトルネックとなるでしょう。
解決策
Turborepo でのパッケージ構成設計
Turborepo を活用することで、スライスを適切にパッケージ化し、効率的に管理できます。まず、推奨されるディレクトリ構成を見ていきましょう。
bashmy-monorepo/
├── apps/
│ ├── web/ # Web アプリケーション
│ ├── admin/ # 管理画面
│ └── mobile/ # モバイルアプリ
├── packages/
│ ├── store-user/ # ユーザースライス
│ ├── store-product/ # 商品スライス
│ ├── store-cart/ # カートスライス
│ └── ui/ # 共有 UI コンポーネント
├── package.json
├── turbo.json
└── yarn.lock
この構成では、各スライスを独立したパッケージとして packages/
ディレクトリに配置しています。アプリケーションは apps/
ディレクトリに格納し、必要なスライスパッケージを依存関係として指定します。
以下の図は、パッケージ間の依存関係を示しています。
mermaidflowchart TB
subgraph apps["アプリケーション層"]
web["Web アプリ"]
admin["管理画面"]
end
subgraph packages["共有パッケージ層"]
userSlice["@repo/store-user<br/>ユーザースライス"]
productSlice["@repo/store-product<br/>商品スライス"]
cartSlice["@repo/store-cart<br/>カートスライス"]
end
web --> userSlice
web --> productSlice
web --> cartSlice
admin --> userSlice
admin --> productSlice
style web fill:#e1f5ff
style admin fill:#e1f5ff
style userSlice fill:#fff4e1
style productSlice fill:#fff4e1
style cartSlice fill:#fff4e1
図で理解できる要点:
- アプリケーションは必要なスライスパッケージのみを依存関係に含める
- スライスパッケージは独立しており、相互依存を避ける
- 依存関係が一方向(アプリ → スライス)になっている
ルートの package.json
では、ワークスペースを定義します。
json{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev"
}
}
このように設定することで、Yarn Workspaces と Turborepo が連携し、効率的なパッケージ管理が可能になります。
Zustand スライスの分割戦略
スライスを適切に分割することで、再利用性と保守性が向上します。ここでは、効果的なスライス分割の戦略を紹介しましょう。
1. ドメイン単位での分割
ビジネスドメインごとにスライスを分けることで、責務が明確になります。
typescript// packages/store-user/src/index.ts
import { StateCreator } from 'zustand';
// ユーザー情報の型定義
export interface User {
id: string;
name: string;
email: string;
}
typescript// ユーザースライスの型定義
export interface UserSlice {
user: User | null;
isLoading: boolean;
setUser: (user: User) => void;
clearUser: () => void;
fetchUser: (id: string) => Promise<void>;
}
typescript// ユーザースライスの実装
export const createUserSlice: StateCreator<UserSlice> = (
set
) => ({
user: null,
isLoading: false,
setUser: (user) => set({ user, isLoading: false }),
clearUser: () => set({ user: null }),
fetchUser: async (id) => {
set({ isLoading: true });
try {
// API 呼び出しの例
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, isLoading: false });
} catch (error) {
set({ isLoading: false });
throw error;
}
},
});
2. 状態とアクションのカプセル化
各スライスは、自身の状態と、その状態を変更するアクションをセットで提供します。
typescript// packages/store-cart/src/index.ts
import { StateCreator } from 'zustand';
// カートアイテムの型定義
export interface CartItem {
productId: string;
quantity: number;
price: number;
}
typescript// カートスライスの型定義
export interface CartSlice {
items: CartItem[];
totalPrice: number;
addItem: (item: CartItem) => void;
removeItem: (productId: string) => void;
clearCart: () => void;
}
typescript// カートスライスの実装
export const createCartSlice: StateCreator<CartSlice> = (
set,
get
) => ({
items: [],
totalPrice: 0,
addItem: (item) =>
set((state) => {
const existingItem = state.items.find(
(i) => i.productId === item.productId
);
if (existingItem) {
// 既存アイテムの数量を更新
const updatedItems = state.items.map((i) =>
i.productId === item.productId
? { ...i, quantity: i.quantity + item.quantity }
: i
);
return {
items: updatedItems,
totalPrice: calculateTotal(updatedItems),
};
} else {
// 新規アイテムを追加
const updatedItems = [...state.items, item];
return {
items: updatedItems,
totalPrice: calculateTotal(updatedItems),
};
}
}),
removeItem: (productId) =>
set((state) => {
const updatedItems = state.items.filter(
(i) => i.productId !== productId
);
return {
items: updatedItems,
totalPrice: calculateTotal(updatedItems),
};
}),
clearCart: () => set({ items: [], totalPrice: 0 }),
});
typescript// ヘルパー関数
function calculateTotal(items: CartItem[]): number {
return items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
3. スライスの粒度の決定
スライスは小さすぎても大きすぎても管理が難しくなります。以下の基準で粒度を決めるとよいでしょう。
# | 基準 | 説明 |
---|---|---|
1 | 単一責任の原則 | 1 つのスライスは 1 つのドメイン概念を扱う |
2 | 再利用可能性 | 複数のアプリケーションで使える単位 |
3 | 変更頻度 | 同じタイミングで変更される機能をまとめる |
4 | チーム構成 | チームの担当領域と一致させる |
TypeScript 型定義の共有方法
スライスパッケージで定義した型を、他のパッケージやアプリケーションから利用できるようにする必要があります。ここでは、型定義の共有方法を説明します。
各スライスパッケージの package.json
で、型定義ファイルのエントリーポイントを指定しましょう。
json{
"name": "@repo/store-user",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"zustand": "^4.0.0"
}
}
TypeScript の設定ファイル tsconfig.json
では、型定義の出力先を指定します。
json{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
これにより、ビルド時に型定義ファイル(.d.ts
)が自動生成され、他のパッケージから型情報を参照できるようになります。
アプリケーション側では、スライスパッケージをインポートするだけで、型情報も一緒に取得できます。
typescript// apps/web/src/store/index.ts
import { create } from 'zustand';
import {
createUserSlice,
UserSlice,
} from '@repo/store-user';
import {
createCartSlice,
CartSlice,
} from '@repo/store-cart';
typescript// 複数のスライスを組み合わせた Store
type StoreState = UserSlice & CartSlice;
export const useStore = create<StoreState>()((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}));
型定義が適切に共有されているため、IDE での自動補完やエラーチェックが効きます。これにより、開発体験が向上し、バグを未然に防げるでしょう。
具体例
プロジェクト初期設定
それでは、実際に Turborepo プロジェクトを作成していきましょう。まずは、新しいディレクトリを作成し、初期化します。
bash# プロジェクトディレクトリを作成
mkdir my-monorepo
cd my-monorepo
bash# package.json を作成
yarn init -y
package.json
を編集して、Workspaces を設定します。
json{
"name": "my-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "^1.10.0",
"typescript": "^5.0.0"
}
}
次に、Turborepo の設定ファイル turbo.json
を作成します。
json{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
}
}
}
この設定により、以下のような最適化が実現されます。
# | 設定項目 | 効果 |
---|---|---|
1 | dependsOn: ["^build"] | 依存パッケージを先にビルドする |
2 | outputs | ビルド成果物をキャッシュする |
3 | cache: false (dev) | 開発時はキャッシュを使わない |
4 | persistent: true | 開発サーバーを継続的に実行 |
ディレクトリ構造を作成します。
bash# アプリケーションディレクトリを作成
mkdir -p apps/web
# パッケージディレクトリを作成
mkdir -p packages/store-user
mkdir -p packages/store-cart
これで、基本的なプロジェクト構造が整いました。次に、具体的なスライスパッケージを実装していきましょう。
基本的なスライスパッケージの作成
ユーザースライスパッケージを作成します。まず、パッケージディレクトリに移動して初期化しましょう。
bash# ユーザースライスディレクトリに移動
cd packages/store-user
bash# package.json を作成
yarn init -y
packages/store-user/package.json
を以下のように編集します。
json{
"name": "@repo/store-user",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"zustand": "^4.4.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}
TypeScript の設定ファイルを作成します。
json{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
次に、実際のスライスコードを実装します。src
ディレクトリを作成しましょう。
bashmkdir src
packages/store-user/src/types.ts
に型定義を記述します。
typescript// ユーザー情報の型定義
export interface User {
id: string;
name: string;
email: string;
avatarUrl?: string;
}
typescript// ユーザースライスの型定義
export interface UserSlice {
user: User | null;
isLoading: boolean;
error: Error | null;
setUser: (user: User) => void;
clearUser: () => void;
fetchUser: (id: string) => Promise<void>;
}
packages/store-user/src/slice.ts
にスライスの実装を記述します。
typescriptimport { StateCreator } from 'zustand';
import { User, UserSlice } from './types';
typescript// ユーザースライスを作成する関数
export const createUserSlice: StateCreator<UserSlice> = (
set
) => ({
user: null,
isLoading: false,
error: null,
// ユーザー情報を設定
setUser: (user) =>
set({
user,
isLoading: false,
error: null,
}),
// ユーザー情報をクリア
clearUser: () =>
set({
user: null,
error: null,
}),
// API からユーザー情報を取得
fetchUser: async (id) => {
set({ isLoading: true, error: null });
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(
`Failed to fetch user: ${response.statusText}`
);
}
const user: User = await response.json();
set({ user, isLoading: false });
} catch (error) {
set({
isLoading: false,
error:
error instanceof Error
? error
: new Error('Unknown error'),
});
}
},
});
packages/store-user/src/index.ts
にエントリーポイントを作成します。
typescript// 型定義をエクスポート
export type { User, UserSlice } from './types';
// スライス作成関数をエクスポート
export { createUserSlice } from './slice';
これで、ユーザースライスパッケージの基本的な実装が完了しました。ビルドして動作を確認しましょう。
bash# パッケージをビルド
yarn build
ビルドが成功すると、dist/
ディレクトリに JavaScript ファイルと型定義ファイルが生成されます。
複数アプリケーションからのスライス利用
作成したスライスパッケージを、複数のアプリケーションから利用してみましょう。まず、Web アプリケーションを作成します。
bash# Web アプリディレクトリに移動
cd ../../apps/web
bash# package.json を作成
yarn init -y
apps/web/package.json
を編集して、スライスパッケージを依存関係に追加します。
json{
"name": "web",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zustand": "^4.4.0",
"@repo/store-user": "*",
"@repo/store-cart": "*"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/react": "^18.2.0",
"@types/node": "^20.0.0"
}
}
アプリケーション内で Store を作成します。apps/web/src/store/index.ts
に以下のコードを記述しましょう。
typescriptimport { create } from 'zustand';
import {
createUserSlice,
UserSlice,
} from '@repo/store-user';
import {
createCartSlice,
CartSlice,
} from '@repo/store-cart';
typescript// 複数のスライスを組み合わせた Store の型定義
type StoreState = UserSlice & CartSlice;
typescript// Store を作成
export const useStore = create<StoreState>()((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}));
React コンポーネントから Store を利用します。apps/web/src/components/UserProfile.tsx
を作成しましょう。
typescriptimport React, { useEffect } from 'react';
import { useStore } from '../store';
typescript// ユーザープロフィールコンポーネント
export const UserProfile: React.FC = () => {
const { user, isLoading, error, fetchUser } = useStore();
useEffect(() => {
// コンポーネントマウント時にユーザー情報を取得
fetchUser('user-123');
}, [fetchUser]);
if (isLoading) {
return <div>読み込み中...</div>;
}
if (error) {
return <div>エラー: {error.message}</div>;
}
if (!user) {
return <div>ユーザーが見つかりません</div>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
{user.avatarUrl && (
<img src={user.avatarUrl} alt={user.name} />
)}
</div>
);
};
同様に、管理画面アプリケーションでも同じスライスパッケージを利用できます。apps/admin/src/store/index.ts
に以下のコードを記述します。
typescriptimport { create } from 'zustand';
import {
createUserSlice,
UserSlice,
} from '@repo/store-user';
typescript// 管理画面では User スライスのみ使用
type StoreState = UserSlice;
export const useStore = create<StoreState>()((...args) => ({
...createUserSlice(...args),
}));
このように、各アプリケーションは必要なスライスのみを選択して利用できます。スライスの実装は共通化されているため、ロジックの重複がなく、メンテナンスもしやすくなりました。
以下の図は、複数アプリケーションでのスライス利用を示しています。
mermaidflowchart TB
subgraph apps["アプリケーション"]
web["Web アプリ<br/>(全スライス利用)"]
admin["管理画面<br/>(User のみ利用)"]
end
subgraph packages["スライスパッケージ"]
userSlice["@repo/store-user"]
cartSlice["@repo/store-cart"]
end
web --> userSlice
web --> cartSlice
admin --> userSlice
style web fill:#e1f5ff
style admin fill:#ffe1e1
style userSlice fill:#fff4e1
style cartSlice fill:#fff4e1
図で理解できる要点:
- 各アプリケーションは必要なスライスのみを依存関係に含める
- スライスパッケージは複数のアプリケーションで再利用される
- ロジックの重複が排除され、一貫性が保たれる
ビルドパイプラインの構築
最後に、Turborepo を活用した効率的なビルドパイプラインを構築しましょう。プロジェクトルートの turbo.json
を詳細に設定します。
json{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"env": ["NODE_ENV"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}
この設定により、以下のような動作が実現されます。
# | タスク | 動作 |
---|---|---|
1 | build | 依存パッケージを先にビルド、成果物をキャッシュ |
2 | dev | キャッシュなしで開発サーバーを起動 |
3 | lint | リント結果をキャッシュしない |
4 | test | ビルド後にテストを実行、カバレッジをキャッシュ |
ルートの package.json
にスクリプトを追加します。
json{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules"
}
}
すべてのパッケージをビルドする際は、以下のコマンドを実行します。
bash# 全パッケージをビルド(並列実行 + キャッシュ活用)
yarn build
Turborepo は依存関係を解析し、最適な順序でビルドを実行します。変更のないパッケージはキャッシュから復元されるため、ビルド時間が大幅に短縮されるでしょう。
bash# 初回ビルド
• Packages in scope: @repo/store-user, @repo/store-cart, web, admin
• Running build in 4 packages
• Remote caching disabled
@repo/store-user:build: cache miss, executing
@repo/store-cart:build: cache miss, executing
web:build: cache miss, executing
admin:build: cache miss, executing
Tasks: 4 successful, 4 total
Time: 15.2s
bash# 2回目のビルド(変更なし)
• Packages in scope: @repo/store-user, @repo/store-cart, web, admin
• Running build in 4 packages
@repo/store-user:build: cache hit, replaying output
@repo/store-cart:build: cache hit, replaying output
web:build: cache hit, replaying output
admin:build: cache hit, replaying output
Tasks: 4 successful, 4 total
Cached: 4 cached, 4 total
Time: 0.8s >>> FULL TURBO
キャッシュが効いているため、2 回目のビルドは 1 秒以下で完了します。特定のパッケージのみを変更した場合も、そのパッケージと依存するアプリケーションのみが再ビルドされます。
開発時には、以下のコマンドですべての開発サーバーを同時に起動できます。
bash# 全アプリケーションの開発サーバーを起動
yarn dev
これにより、複数のアプリケーションを並行して開発でき、効率的な開発フローが実現されるのです。
まとめ
Turborepo と Zustand を組み合わせることで、Monorepo 環境における状態管理を効率的に運用できます。この記事では、スライスパターンを活用したパッケージ化の手法を、実際のコード例とともに解説しました。
重要なポイントをおさらいしましょう。
# | ポイント | 内容 |
---|---|---|
1 | スライス分割 | ドメイン単位で状態管理を分割し、再利用性を高める |
2 | パッケージ構成 | 各スライスを独立したパッケージとして管理する |
3 | 型定義共有 | TypeScript の型定義を適切にエクスポートする |
4 | ビルド最適化 | Turborepo のキャッシュ機能で高速化を実現する |
この手法を導入することで、複数のアプリケーション間でロジックを共有しながら、保守性の高いコードベースを維持できるでしょう。チーム開発においても、各スライスを独立して開発できるため、並行作業がスムーズになります。
さらに、ビルド時間の短縮により、開発のフィードバックループが高速化され、生産性の向上も期待できますね。まずは小さなスライスから始めて、徐々に拡張していくアプローチがおすすめです。
Monorepo での状態管理に悩んでいる方は、ぜひこの手法を試してみてください。きっと開発体験が向上するはずです。
関連リンク
- article
Turborepo で Zustand スライスをパッケージ化:Monorepo 運用の初期設定
- article
Zustand × TanStack Query × SWR:キャッシュ・再検証・型安全の実運用比較
- article
Zustand × Next.js の Hydration Mismatch を根絶する:原因別チェックリスト
- article
Zustand を React なしで使う:subscribe と Store API だけで組む最小構成
- article
Zustand の状態管理を使ったカスタムフック作成術
- article
Zustand × URL クエリパラメータ連動:状態同期で UX を高める
- article
【2025 年 10 月版】 Claude Sonnet 4.5 登場! Claude Pro でも使える!Claude Code のアップデート手順まで紹介
- article
Turborepo で Zustand スライスをパッケージ化:Monorepo 運用の初期設定
- article
Nuxt を macOS + yarn で最短構築:ESLint/Prettier/TS 設定まで一気通貫
- article
キャッシュ比較:WordPress で WP Rocket/LiteSpeed/W3TC を検証
- article
Nginx を macOS で本番級に構築:launchd/ログローテーション/権限・署名のベストプラクティス
- article
WebSocket を NGINX/HAProxy で終端する設定例:アップグレードヘッダーとタイムアウト完全ガイド
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来