Reactアプリケーションが成長するにつれて、状態管理の複雑さが開発者を悩ませる課題となります。Zustandは、シンプルかつパフォーマンスに優れた状態管理ライブラリとして注目されています。本記事では、Zustandを活用して状態を分割し、スケーラブルで効率的な設計を行う方法について解説します。Zustandの基本から始め、スケーラブルなデザインパターン、具体的な実装例、そして応用例までを網羅し、状態管理の課題を解決するための実践的な知識を提供します。
Zustandの概要と基本概念
Zustandは、軽量で直感的なReact向け状態管理ライブラリです。ReduxやContext APIのような従来のツールと比較して、設定が簡単で、ボイラープレートコードが少なく、柔軟性に優れている点が特徴です。
Zustandの特長
- シンプルなAPI: Zustandは、直感的でシンプルなAPIを提供し、状態管理の煩雑さを軽減します。
- パフォーマンス重視: 必要なコンポーネントだけが再レンダリングされるため、高いパフォーマンスを維持できます。
- 依存関係の柔軟性: Zustandは依存関係に縛られず、既存のライブラリやツールと併用できます。
Zustandと他の状態管理ライブラリの違い
- Redux: Reduxは強力ですが、設定やミドルウェアが複雑である一方、Zustandはシンプルで迅速に導入できます。
- Context API: Contextは小規模な状態管理には適していますが、Zustandはコンポーネントの再レンダリングを最小化し、大規模なアプリケーションに最適です。
Zustandのシンプルさと拡張性は、開発者が状態管理に必要なコストを削減し、プロジェクトのスケーラビリティを向上させるのに役立ちます。
Zustandのインストールと初期設定
Zustandをプロジェクトで利用するには、まずインストールから始めます。Zustandは非常に軽量なライブラリで、数分でセットアップを完了できます。
1. インストール手順
以下のコマンドを使用してZustandをインストールします。
npm install zustand
または、Yarnを利用してインストールする場合:
yarn add zustand
2. 基本的な状態管理のセットアップ
Zustandでは、状態を管理するストアを簡単に作成できます。以下に基本的なストアの例を示します:
import { create } from 'zustand';
// ストアの作成
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
export default useStore;
この例では、count
という状態と、それを増減させるアクションincrement
とdecrement
を定義しています。
3. Reactコンポーネントでの利用
作成したストアをReactコンポーネント内で使用するには、以下のようにuseStore
フックを呼び出します:
import React from 'react';
import useStore from './store';
const Counter = () => {
const { count, increment, decrement } = useStore();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default Counter;
4. Zustandのストアが選ばれる理由
- コードの簡潔さ: ボイラープレートがほとんどなく、迅速に状態管理を実装可能。
- Reactフックとの統合:
useStore
フックを直接使用するだけで状態にアクセス可能。
これで、Zustandの基本的なセットアップが完了しました。このストアを基盤に、スケーラブルな状態管理設計を構築していきます。
状態の分割とそのメリット
状態管理が複雑になると、すべての状態を一つのストアにまとめるのは非効率であり、スケーラビリティが損なわれる可能性があります。Zustandでは、状態を分割して管理することで、効率的で柔軟な設計が可能になります。
状態を分割する理由
- 責務の分離: 各ストアが特定の機能に集中できるため、コードの見通しが良くなります。
- パフォーマンスの向上: 不必要な再レンダリングを防ぐことができ、アプリのパフォーマンスが向上します。
- テストの容易さ: 状態が分割されていると、各ストアを個別にテストしやすくなります。
状態分割のアプローチ
Zustandでは、複数のストアを作成して必要に応じて組み合わせることができます。以下は、Todoアプリの例です:
- Task Store: タスクのリストや状態を管理。
- User Store: ユーザー情報や認証ステータスを管理。
- Settings Store: ユーザー設定やUIの状態を管理。
Task Storeの例
const useTaskStore = create((set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
removeTask: (taskId) =>
set((state) => ({
tasks: state.tasks.filter((task) => task.id !== taskId),
})),
}));
User Storeの例
const useUserStore = create((set) => ({
user: null,
login: (userInfo) => set({ user: userInfo }),
logout: () => set({ user: null }),
}));
状態を分割するメリット
- 再利用性の向上: 状態を特定のコンポーネントやモジュールに依存させずに再利用可能。
- スケーラビリティ: アプリが成長しても、状態の複雑さを抑えられる。
- デバッグが容易: 分割された状態は、問題が発生した場合の原因特定を容易にします。
まとめ
状態の分割は、Reactアプリケーションの成長に伴う複雑さを軽減する効果的な手法です。Zustandはその柔軟性により、分割されたストアを管理するのに最適なツールです。この基礎をもとに、スケーラブルなデザインを構築していきます。
スケーラブルな状態管理のためのデザインパターン
Zustandを使用してスケーラブルな状態管理を構築するには、適切なデザインパターンを採用することが重要です。これにより、コードの再利用性が高まり、アプリケーションの拡張性を確保できます。
1. スライスパターン
スライスパターンでは、状態を機能単位(スライス)に分割します。それぞれのスライスは特定の責務に対応し、分割管理が容易になります。
スライスの例
以下の例は、タスク管理アプリのスライスを実装したものです:
const createTaskSlice = (set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
removeTask: (id) => set((state) => ({ tasks: state.tasks.filter((t) => t.id !== id) })),
});
const createUserSlice = (set) => ({
user: null,
login: (userInfo) => set({ user: userInfo }),
logout: () => set({ user: null }),
});
const useStore = create((set) => ({
...createTaskSlice(set),
...createUserSlice(set),
}));
このように、スライスを組み合わせて一つのストアに統合します。
2. モジュール化されたストア
各ストアを完全に分離し、必要に応じて組み合わせて使用します。これにより、各ストアの依存関係が独立し、テストやデバッグが容易になります。
実装例
// taskStore.js
export const useTaskStore = create((set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
}));
// userStore.js
export const useUserStore = create((set) => ({
user: null,
login: (userInfo) => set({ user: userInfo }),
}));
これらを必要な場所でインポートして使用します。
3. ミドルウェアの活用
Zustandはミドルウェアを利用して、ロギングや永続化などの機能を簡単に追加できます。
永続化の例
import { persist } from 'zustand/middleware';
const usePersistentStore = create(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}),
{
name: 'theme-storage', // localStorageのキー
}
)
);
4. セレクタによる最適化
セレクタを使用して必要な状態だけを取得し、再レンダリングを最小限に抑えることができます。
例
const useTaskCount = () => useStore((state) => state.tasks.length);
これにより、タスク数に変更があった場合のみコンポーネントが再レンダリングされます。
まとめ
スケーラブルな状態管理を構築するためには、スライスパターンやモジュール化、ミドルウェアの活用が有効です。Zustandの柔軟性を活かして、機能に応じた最適なデザインパターンを選択し、効率的なアプリケーション開発を進めましょう。
実例:Todoアプリでの状態分割の実装
ここでは、Zustandを活用した状態分割の実装例として、シンプルなTodoアプリを構築します。状態分割の実際の効果と実装方法を確認していきましょう。
1. 状態の分割計画
Todoアプリでは、以下のように状態を分割します:
- タスクリストの管理: Todoリストの状態と操作。
- フィルタ設定の管理: 表示するタスクのフィルタリング条件。
2. タスクリストの状態管理
まず、タスクリストを管理するストアを作成します。
import { create } from 'zustand';
const useTaskStore = create((set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
removeTask: (id) =>
set((state) => ({ tasks: state.tasks.filter((task) => task.id !== id) })),
toggleComplete: (id) =>
set((state) => ({
tasks: state.tasks.map((task) =>
task.id === id ? { ...task, completed: !task.completed } : task
),
})),
}));
export default useTaskStore;
このストアでは、タスクの追加、削除、完了状態の切り替えを管理します。
3. フィルタ設定の状態管理
次に、フィルタ条件を管理するストアを作成します。
const useFilterStore = create((set) => ({
filter: 'all',
setFilter: (filter) => set({ filter }),
}));
export default useFilterStore;
ここでは、all
、completed
、incomplete
といったフィルタ状態を管理します。
4. Todoアプリのコンポーネント構築
状態分割したストアを使って、Reactコンポーネントを構築します。
import React from 'react';
import useTaskStore from './useTaskStore';
import useFilterStore from './useFilterStore';
const TodoApp = () => {
const { tasks, addTask, removeTask, toggleComplete } = useTaskStore();
const { filter, setFilter } = useFilterStore();
const filteredTasks = tasks.filter((task) => {
if (filter === 'completed') return task.completed;
if (filter === 'incomplete') return !task.completed;
return true;
});
const handleAddTask = () => {
const newTask = {
id: Date.now(),
text: 'New Task',
completed: false,
};
addTask(newTask);
};
return (
<div>
<h1>Todo App</h1>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('completed')}>Completed</button>
<button onClick={() => setFilter('incomplete')}>Incomplete</button>
</div>
<ul>
{filteredTasks.map((task) => (
<li key={task.id}>
<span
style={{
textDecoration: task.completed ? 'line-through' : 'none',
}}
onClick={() => toggleComplete(task.id)}
>
{task.text}
</span>
<button onClick={() => removeTask(task.id)}>Delete</button>
</li>
))}
</ul>
<button onClick={handleAddTask}>Add Task</button>
</div>
);
};
export default TodoApp;
5. 分割のメリットを確認
- 柔軟性: 状態ごとに責務が分離されており、各ストアを独立して変更可能。
- 拡張性: 新しい機能(例: タスクの期限設定)を簡単に追加可能。
- パフォーマンス: フィルタ状態の変更はタスクリストに影響を与えず、必要な部分だけ再レンダリングされます。
まとめ
この例では、タスクリストとフィルタ状態を分離して管理することで、コードの見通しを良くし、スケーラブルな設計を実現しました。ZustandのシンプルなAPIを活用することで、複雑さを抑えながら効率的に状態管理を行うことができます。
Zustandでの依存性管理とパフォーマンス最適化
Reactアプリケーションにおける状態管理では、依存性を適切に制御し、パフォーマンスを最適化することが重要です。Zustandを使えば、シンプルなAPIで依存性を明示的に管理し、効率的なアプリケーションを構築できます。
1. Zustandの依存性管理
Zustandでは、状態やアクションをセレクタとして選択することで、必要な依存性だけをReactコンポーネントに渡すことが可能です。これにより、不要な再レンダリングを防ぐことができます。
セレクタの使用例
以下は、特定の状態だけを取得するためのセレクタを使用した例です:
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
// セレクタで状態を選択
const Counter = () => {
const count = useStore((state) => state.count); // countのみ取得
const increment = useStore((state) => state.increment);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
</div>
);
};
この例では、count
とincrement
を個別に取得しているため、他の状態が変更されてもコンポーネントは再レンダリングされません。
2. Zustandのパフォーマンス最適化
Zustandでは、以下の手法を用いてパフォーマンスを向上させることができます。
シャロウ比較の利用
Zustandのセレクタには、シャロウ比較(浅い比較)が組み込まれており、必要な場合にのみ再レンダリングが発生します。たとえば、複数の値をセレクトする際に利用します:
const { count, increment } = useStore(
(state) => ({ count: state.count, increment: state.increment }),
shallow // シャロウ比較を適用
);
shallow
を利用すると、セレクトした値のいずれかが変更された場合のみレンダリングが発生します。
アクションを分割して使用
アクションと状態を明確に分離することで、不要な状態の取得を避けられます。
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const IncrementButton = () => {
const increment = useCountStore((state) => state.increment);
return <button onClick={increment}>Increment</button>;
};
3. Zustandのミドルウェアでさらに最適化
Zustandはミドルウェアを活用することで、状態の永続化やロギングを簡単に追加できます。
永続化ミドルウェア
以下は、zustand/middleware
を使用して状態をローカルストレージに永続化する例です:
import { persist } from 'zustand/middleware';
const usePersistentStore = create(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'counter-storage' } // ローカルストレージキー
)
);
これにより、状態が永続化され、ページリロード後も保持されます。
4. 再レンダリングのトラブルシューティング
再レンダリングが予期しないタイミングで発生する場合、以下を確認してください:
- セレクタの設計: 必要な状態だけを選択しているか。
- 深い状態の更新: 深いネストの状態を更新する場合、シャロウ比較を利用して効率化する。
- ミドルウェアの使用: 不要なロギングや永続化がパフォーマンスに影響を与えていないか。
まとめ
Zustandのセレクタやミドルウェアを活用することで、Reactアプリケーションの状態管理における依存性を適切に管理し、パフォーマンスを最適化できます。これにより、スケーラブルかつ効率的な状態管理が可能になります。
状態管理におけるベストプラクティス
Zustandを活用してReactアプリケーションの状態管理を行う際、効率性と保守性を高めるためのベストプラクティスを採用することが重要です。ここでは、実用的なガイドラインを紹介します。
1. 状態を最小限に保つ
状態は必要最小限に管理するべきです。
状態の管理範囲が広すぎると複雑になり、再レンダリングの頻度が増える可能性があります。必要な状態だけをZustandのストアに保持し、それ以外はローカル状態や派生データで処理することを推奨します。
例: ドライブ状態
以下は、必要最小限のデータのみを保持する例です:
const useDriveStore = create((set) => ({
selectedFile: null,
setSelectedFile: (file) => set({ selectedFile: file }),
}));
ここでは、選択されたファイルのみを管理し、ファイルリストなどの派生データは他の場所で処理します。
2. ストアを機能ごとに分割する
単一の巨大なストアではなく、機能単位で状態を分割することで、管理しやすくします。これにより、特定の機能に焦点を当てた状態管理が可能になります。
例: ユーザーとタスクの分離
const useUserStore = create((set) => ({
user: null,
login: (userInfo) => set({ user: userInfo }),
}));
const useTaskStore = create((set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
}));
このように分割することで、ユーザーデータやタスクデータの変更が相互に影響しなくなります。
3. セレクタを活用して依存性を制御
必要な状態だけを選択して取得することで、依存性を最小限に抑えます。セレクタを活用することで、不要な再レンダリングを防止できます。
例: タスク数の取得
const useTaskCount = () => useTaskStore((state) => state.tasks.length);
このアプローチにより、タスクリストが変更されてもタスク数だけが必要なコンポーネントのみ更新されます。
4. 状態の永続化を計画的に実施
状態をローカルストレージやセッションストレージに永続化することで、ユーザー体験を向上させることができます。ただし、必要のないデータを永続化しないよう注意が必要です。
例: テーマの永続化
import { persist } from 'zustand/middleware';
const useThemeStore = create(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}),
{ name: 'theme-storage' }
)
);
5. ミドルウェアで再利用可能なロジックを追加
ミドルウェアを活用して、状態管理のロジックを再利用可能にします。ロギングやエラーハンドリング、永続化を追加するのに便利です。
例: ロギングミドルウェア
const log = (config) => (set, get, api) =>
config((args) => {
console.log('State updated:', args);
set(args);
}, get, api);
const useStoreWithLogging = create(log((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
})));
6. 適切なデバッグツールを利用
Zustandはシンプルであるため、デバッグが比較的容易ですが、コンソールログや状態の可視化ツールを活用するとさらに効果的です。
7. コードのドキュメント化
状態の分割と責務の明確化を行った後は、コードに十分なコメントを付けることを忘れないでください。これにより、チーム内での理解が深まります。
まとめ
Zustandでの状態管理はシンプルでパワフルですが、適切なベストプラクティスを守ることで、その効果を最大限に引き出すことができます。状態を最小限に保ち、機能単位で分割し、セレクタやミドルウェアを活用することで、スケーラブルで効率的なアプリケーションを構築しましょう。
トラブルシューティング:よくある課題と解決策
Zustandを使用する際、予期しない動作や問題が発生する場合があります。ここでは、よくある課題とその解決策を具体的に解説します。
1. 状態変更後のコンポーネントが再レンダリングされない
原因: 状態を適切にセレクトしていない、またはセレクタが変更を検知できていない可能性があります。
解決策:
セレクタを明示的に指定し、必要な状態のみを取得してください。また、必要であればshallow
を利用して深いオブジェクトの比較を回避します。
import { shallow } from 'zustand/shallow';
const count = useStore(
(state) => ({ count: state.count, increment: state.increment }),
shallow
);
補足: 状態全体を取得するのではなく、変更が必要な部分だけをセレクトすることを推奨します。
2. 状態が予期せず上書きされる
原因: 同じストア内の複数のアクションが競合している可能性があります。
解決策:
アクション内での状態更新が意図したものであるか確認し、更新処理が他のアクションの影響を受けないようにします。
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set(() => ({ count: 0 })),
}));
上記のように、reset
アクションが他の状態を影響しない形で定義されていることを確認してください。
3. 初期状態が正しく設定されていない
原因: Zustandのストアが未初期化の状態で呼び出されている可能性があります。
解決策:
ストアの初期値を明示的に設定し、依存するコンポーネントが正しく初期化されるまで描画を遅延させます。
const useStore = create((set) => ({
data: null,
setData: (newData) => set({ data: newData }),
}));
const MyComponent = () => {
const data = useStore((state) => state.data);
if (!data) return <div>Loading...</div>;
return <div>Data Loaded</div>;
};
4. 再レンダリングが多発する
原因: 不必要に多くの状態をセレクトしている可能性があります。
解決策:
セレクタを利用して必要な状態だけを取得するように最適化します。
const useCount = () => useStore((state) => state.count);
const Counter = () => {
const count = useCount(); // countのみ取得
return <h1>Count: {count}</h1>;
};
5. 永続化されたデータが期待通りに動作しない
原因: Zustandのpersist
ミドルウェアで設定されたストレージキーが競合している、またはストレージデータが破損している可能性があります。
解決策:
ストレージキーを確認し、永続化データを初期化するロジックを追加します。
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'counter-storage' } // キーを一意に設定
)
);
永続化されたデータが破損している場合は、ストレージをクリアする処理を加えることも検討してください。
6. Zustandが非同期操作でエラーを投げる
原因: 非同期操作が状態更新と同期していない、または適切に処理されていない場合があります。
解決策:
非同期処理を安全に実行するために、アクション内でasync/await
を使用します。
const useStore = create((set) => ({
data: null,
fetchData: async () => {
const response = await fetch('/api/data');
const data = await response.json();
set({ data });
},
}));
非同期操作が完了するまで、コンポーネントの描画を遅延させるようにしましょう。
まとめ
Zustandを使用する際に直面する問題は、適切なセレクタの使用や状態管理の設計、非同期処理の適用などで解決可能です。問題が発生した場合は、ここで紹介した方法を参考にトラブルシューティングを行い、効率的な状態管理を目指してください。
応用例:大規模アプリケーションへの適用
Zustandは、シンプルさと柔軟性を兼ね備えた状態管理ライブラリとして、大規模なReactアプリケーションにも適用可能です。本章では、複雑な要件を持つアプリケーションでZustandを利用する際の応用例を解説します。
1. 状態のモジュール化
大規模なアプリケーションでは、機能ごとに状態を分割し、モジュール化するのが重要です。以下は、認証、タスクリスト、UI設定をモジュール化した例です。
// authStore.js
export const useAuthStore = create((set) => ({
user: null,
login: (userInfo) => set({ user: userInfo }),
logout: () => set({ user: null }),
}));
// taskStore.js
export const useTaskStore = create((set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
removeTask: (id) =>
set((state) => ({ tasks: state.tasks.filter((task) => task.id !== id) })),
}));
// uiStore.js
export const useUIStore = create((set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}));
各モジュールを独立して管理することで、責務の分離とスケーラビリティを実現します。
2. グローバルなイベント管理
グローバルイベントや通知システムなど、アプリ全体で共有する状態を管理する場合もZustandが有効です。
export const useNotificationStore = create((set) => ({
notifications: [],
addNotification: (message) =>
set((state) => ({
notifications: [...state.notifications, { id: Date.now(), message }],
})),
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
}));
これにより、通知の管理を効率化し、どのコンポーネントからでも状態を更新可能にします。
3. 非同期データのキャッシング
大規模アプリケーションでは、APIから取得したデータをキャッシュして再利用することが求められます。Zustandを使って非同期データをキャッシュする方法を以下に示します。
export const useDataStore = create((set) => ({
data: null,
loading: false,
fetchData: async (url) => {
set({ loading: true });
const response = await fetch(url);
const data = await response.json();
set({ data, loading: false });
},
}));
これにより、データ取得の効率化と再利用性の向上を実現します。
4. Middlewareを活用したログ管理
大規模アプリでは、デバッグやモニタリングのために状態の変更を記録する仕組みが重要です。
const logMiddleware = (config) => (set, get, api) =>
config(
(args) => {
console.log('State updated:', args);
set(args);
},
get,
api
);
export const useLoggedStore = create(
logMiddleware((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
ログ管理を追加することで、状態変更の追跡が容易になります。
5. 大規模な状態を分離するためのスライス構造
スライス構造を採用して状態を分離し、管理を効率化します。
const createAuthSlice = (set) => ({
user: null,
login: (userInfo) => set({ user: userInfo }),
logout: () => set({ user: null }),
});
const createTaskSlice = (set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
});
export const useStore = create((set) => ({
...createAuthSlice(set),
...createTaskSlice(set),
}));
これにより、状態がスライスごとに分離され、保守性が向上します。
まとめ
大規模アプリケーションにおいても、Zustandのシンプルさと柔軟性を活かして効率的な状態管理が可能です。状態のモジュール化、グローバルイベント管理、非同期キャッシュ、ログ管理、スライス構造などの応用技術を駆使することで、スケーラブルで堅牢な設計を実現できます。
まとめ
本記事では、ReactアプリケーションにおけるZustandを活用した状態管理の方法について、基本から応用まで解説しました。Zustandは、シンプルなAPIと高い柔軟性を持つ軽量ライブラリで、状態の分割やスケーラブルな設計に最適です。
具体的には、Zustandの概要からインストール方法、状態分割のメリット、スケーラブルなデザインパターン、Todoアプリでの実装例、依存性管理、パフォーマンス最適化、ベストプラクティス、トラブルシューティング、大規模アプリケーションへの応用例までを網羅しました。
Zustandを適切に活用することで、Reactアプリケーションの状態管理における複雑さを軽減し、効率的かつ拡張性の高い設計を実現できます。これらの知識を基に、さらに高度なプロジェクト構築に挑戦してください。
コメント