SolidJS クリーンアーキテクチャ実践:UI・状態・副作用を厳密に分離する

SolidJS はリアクティブシステムの強力さと、きめ細かな更新制御により注目を集めています。しかし、アプリケーションが成長するにつれて、UI ロジック・状態管理・副作用処理が混在し、保守性やテストのしやすさが損なわれることも少なくありません。
そこで本記事では、クリーンアーキテクチャの原則を SolidJS に適用し、UI・状態・副作用を厳密に分離する実践手法をご紹介します。レイヤーごとの責務を明確化し、依存関係を一方向に保つことで、変更に強く、拡張しやすいコードベースを実現できるでしょう。
背景
クリーンアーキテクチャとは
クリーンアーキテクチャは、Robert C. Martin(Uncle Bob)が提唱したソフトウェア設計の原則です。ビジネスロジックをフレームワークや UI、データベースといった外部要素から独立させることで、長期的な保守性と変更容易性を確保します。
以下の図は、クリーンアーキテクチャの基本構造を示しています。
mermaidflowchart TB
subgraph presentation["プレゼンテーション層"]
ui["UI コンポーネント"]
end
subgraph application["アプリケーション層"]
usecase["ユースケース"]
end
subgraph domain["ドメイン層"]
entity["エンティティ"]
end
subgraph infrastructure["インフラ層"]
api["API Client"]
storage["Storage"]
end
ui -->|呼び出し| usecase
usecase -->|操作| entity
usecase -->|依存注入| api
usecase -->|依存注入| storage
この図から分かるように、中心にあるドメイン層(エンティティ)は外部に依存せず、アプリケーション層(ユースケース)を介してプレゼンテーション層やインフラ層と連携します。依存の方向は必ず内側(ドメイン)に向かい、外側の層は内側の層のインターフェースを利用します。
SolidJS の特性とクリーンアーキテクチャの親和性
SolidJS はリアクティブプリミティブ(Signal、Effect、Memo)を用いて、細粒度の更新を実現するライブラリです。仮想 DOM を持たないため、パフォーマンスに優れ、状態管理も直感的に記述できます。
一方で、この柔軟性は「どこに何を書いても動く」という状況を生み出しやすく、コンポーネント内にビジネスロジックや副作用が混在しがちです。クリーンアーキテクチャを適用することで、責務を明確に分離し、テストしやすく、変更に強い設計を実現できるでしょう。
課題
コンポーネントへのロジック集中
SolidJS では createSignal
や createEffect
を用いてコンポーネント内で状態と副作用を管理できます。しかし、この便利さゆえに、以下のような問題が発生しやすくなります。
mermaidflowchart LR
component["コンポーネント"]
component -->|含む| ui["UI ロジック"]
component -->|含む| state["状態管理"]
component -->|含む| effect["副作用処理"]
component -->|含む| business["ビジネスロジック"]
component -->|含む| api["API 呼び出し"]
この図が示すように、コンポーネントが複数の責務を抱え込むことで、以下の課題が顕在化します。
- テストの困難さ: UI と副作用が密結合しているため、単体テストでモックを用意するのが難しい
- 再利用性の低下: ロジックがコンポーネントに埋め込まれ、他の画面で同じロジックを使えない
- 変更の影響範囲拡大: ビジネスルールの変更が UI にも波及し、修正箇所が増える
- 依存関係の複雑化: コンポーネントが直接 API クライアントやストレージに依存し、テストや差し替えが困難
状態と副作用の境界不明瞭
SolidJS の createEffect
は非常に強力ですが、コンポーネント内で無秩序に使うと、どの Effect がどの Signal に依存しているか把握しづらくなります。また、副作用をコンポーネント外に切り出す際の指針が不明確だと、リファクタリングも困難です。
解決策
レイヤー分割の基本方針
クリーンアーキテクチャを SolidJS に適用する際、以下の 4 つのレイヤーに分割します。
# | レイヤー | 責務 | SolidJS における実装例 |
---|---|---|---|
1 | ドメイン層 | ビジネスルールとエンティティ | 純粋な TypeScript クラス・型定義 |
2 | アプリケーション層 | ユースケース・状態管理 | カスタムフック(createXxx )、Store |
3 | インフラ層 | 外部通信・永続化 | API Client、LocalStorage ラッパー |
4 | プレゼンテーション層 | UI コンポーネント | SolidJS コンポーネント(JSX) |
依存関係は必ず 外側から内側へ 向かい、内側の層は外側の層を知りません。これにより、ドメイン層はフレームワークや UI に依存せず、純粋なロジックとして独立します。
以下の図は、各レイヤーの依存関係を示しています。
mermaidflowchart TB
presentation["プレゼンテーション層<br/>(UI コンポーネント)"]
application["アプリケーション層<br/>(ユースケース・State)"]
domain["ドメイン層<br/>(エンティティ・ビジネスルール)"]
infrastructure["インフラ層<br/>(API・Storage)"]
presentation -->|依存| application
application -->|依存| domain
application -.->|依存注入で利用| infrastructure
infrastructure -.->|実装| domain
この図から、プレゼンテーション層はアプリケーション層のみを知り、アプリケーション層はドメイン層のみを知ることが分かります。インフラ層はドメイン層のインターフェースを実装し、アプリケーション層に注入されます。
各レイヤーの役割と実装方針
1. ドメイン層
ビジネスロジックとエンティティを定義します。SolidJS のリアクティブシステムには一切依存しません。
責務:
- エンティティ(データ構造)の定義
- ビジネスルールの実装
- リポジトリインターフェースの定義
禁止事項:
createSignal
やcreateEffect
の使用- UI コンポーネントへの依存
- 具体的な API クライアントやストレージへの依存
2. アプリケーション層
ユースケースを実装し、ドメイン層とインフラ層を組み合わせて、アプリケーション固有の状態管理を行います。
責務:
- ユースケースの実装(カスタムフック形式)
- 状態管理(Store、Signal)
- ドメイン層とインフラ層の橋渡し
注意点:
- UI に関するロジックは含めない
- 副作用は最小限に留め、インフラ層に委譲する
3. インフラ層
外部システムとの通信や永続化を担当します。ドメイン層で定義したインターフェースを実装します。
責務:
- API 呼び出し
- LocalStorage、IndexedDB へのアクセス
- リポジトリインターフェースの実装
注意点:
- ビジネスロジックは含めない
- データの取得・保存のみに専念する
4. プレゼンテーション層
ユーザーに表示する UI コンポーネントを実装します。アプリケーション層のカスタムフックを利用して、状態とユースケースにアクセスします。
責務:
- JSX による UI 記述
- ユーザーイベントのハンドリング
- アプリケーション層へのデータ受け渡し
禁止事項:
- ビジネスロジックの実装
- 直接的な API 呼び出し
- 複雑な状態管理(アプリケーション層に委譲)
依存性逆転の原則(DIP)の適用
インフラ層がドメイン層に依存しないよう、依存性逆転の原則を適用します。ドメイン層でインターフェース(抽象)を定義し、インフラ層がそれを実装することで、ドメイン層は具体的な実装を知らずに済みます。
mermaidflowchart LR
usecase["ユースケース<br/>(アプリケーション層)"]
repo_interface["リポジトリ<br/>インターフェース<br/>(ドメイン層)"]
repo_impl["リポジトリ<br/>実装<br/>(インフラ層)"]
usecase -->|依存| repo_interface
repo_impl -.->|実装| repo_interface
usecase -.->|注入| repo_impl
この図から、ユースケースはリポジトリインターフェースに依存し、具体的な実装はアプリケーション起動時に注入されることが分かります。これにより、テスト時にモックを簡単に差し替えられます。
具体例
ここでは、ユーザー管理機能を例に、各レイヤーの実装を段階的に示します。
ステップ 1: ドメイン層の実装
エンティティの定義
ドメイン層では、アプリケーションの中核となるデータ構造を定義します。
typescript// domain/entities/User.ts
/**
* ユーザーエンティティ
* ビジネスロジックの中核となるデータ構造を定義
*/
export class User {
constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string,
public readonly createdAt: Date
) {}
/**
* ユーザー名が有効かどうかを検証
*/
isValidName(): boolean {
return this.name.length >= 2 && this.name.length <= 50;
}
/**
* メールアドレスが有効かどうかを検証
*/
isValidEmail(): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(this.email);
}
}
このコードでは、User
クラスに基本的なバリデーションロジックを含めています。外部ライブラリや SolidJS のリアクティブシステムには一切依存していません。
リポジトリインターフェースの定義
次に、ユーザーデータの取得・保存を抽象化したインターフェースを定義します。
typescript// domain/repositories/UserRepository.ts
import { User } from '../entities/User';
/**
* ユーザーリポジトリインターフェース
* データの取得・保存方法を抽象化し、具体的な実装は隠蔽
*/
export interface UserRepository {
/**
* すべてのユーザーを取得
*/
findAll(): Promise<User[]>;
/**
* ID でユーザーを取得
*/
findById(id: string): Promise<User | null>;
/**
* ユーザーを作成
*/
create(
user: Omit<User, 'id' | 'createdAt'>
): Promise<User>;
/**
* ユーザーを削除
*/
delete(id: string): Promise<void>;
}
このインターフェースにより、アプリケーション層は具体的な API 実装を知らずに、ユーザーデータを操作できます。
ステップ 2: インフラ層の実装
API クライアントの実装
インフラ層では、ドメイン層で定義したインターフェースを実装します。
typescript// infrastructure/api/UserApiClient.ts
import { User } from '../../domain/entities/User';
import { UserRepository } from '../../domain/repositories/UserRepository';
/**
* ユーザー API クライアント
* リポジトリインターフェースを実装し、実際の HTTP 通信を行う
*/
export class UserApiClient implements UserRepository {
constructor(private readonly baseUrl: string) {}
async findAll(): Promise<User[]> {
const response = await fetch(`${this.baseUrl}/users`);
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
return data.map(
(item: any) =>
new User(
item.id,
item.name,
item.email,
new Date(item.createdAt)
)
);
}
async findById(id: string): Promise<User | null> {
const response = await fetch(
`${this.baseUrl}/users/${id}`
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const item = await response.json();
return new User(
item.id,
item.name,
item.email,
new Date(item.createdAt)
);
}
async create(
user: Omit<User, 'id' | 'createdAt'>
): Promise<User> {
const response = await fetch(`${this.baseUrl}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
const item = await response.json();
return new User(
item.id,
item.name,
item.email,
new Date(item.createdAt)
);
}
async delete(id: string): Promise<void> {
const response = await fetch(
`${this.baseUrl}/users/${id}`,
{
method: 'DELETE',
}
);
if (!response.ok) {
throw new Error('Failed to delete user');
}
}
}
このコードは、HTTP 通信を行う具体的な実装です。ドメイン層のインターフェースを実装しているため、アプリケーション層からは抽象化されたメソッドを通じてアクセスできます。
ステップ 3: アプリケーション層の実装
ユースケースの実装(カスタムフック)
アプリケーション層では、ユースケースをカスタムフック形式で実装します。SolidJS の createSignal
や createResource
を使用して、状態管理と副作用を制御します。
typescript// application/usecases/useUserManagement.ts
import { createSignal, createResource } from 'solid-js';
import { UserRepository } from '../../domain/repositories/UserRepository';
import { User } from '../../domain/entities/User';
/**
* ユーザー管理ユースケース
* リポジトリを注入し、状態管理と副作用を統合
*/
export function useUserManagement(
repository: UserRepository
) {
// ユーザー一覧の取得(リソース形式)
const [users, { mutate, refetch }] = createResource<
User[]
>(async () => {
return await repository.findAll();
});
// ローディング状態
const [isCreating, setIsCreating] = createSignal(false);
const [isDeleting, setIsDeleting] = createSignal(false);
/**
* ユーザーを作成
*/
const createUser = async (
name: string,
email: string
) => {
setIsCreating(true);
try {
const newUser = await repository.create({
name,
email,
});
// 既存のユーザーリストに新しいユーザーを追加
mutate((prev) =>
prev ? [...prev, newUser] : [newUser]
);
return newUser;
} catch (error) {
console.error('Failed to create user:', error);
throw error;
} finally {
setIsCreating(false);
}
};
/**
* ユーザーを削除
*/
const deleteUser = async (id: string) => {
setIsDeleting(true);
try {
await repository.delete(id);
// 既存のユーザーリストから削除
mutate((prev) =>
prev ? prev.filter((u) => u.id !== id) : []
);
} catch (error) {
console.error('Failed to delete user:', error);
throw error;
} finally {
setIsDeleting(false);
}
};
return {
users,
isCreating,
isDeleting,
createUser,
deleteUser,
refetch,
};
}
このカスタムフックは、リポジトリを注入され、ユーザー一覧の取得・作成・削除を行います。createResource
を用いて非同期データ取得を行い、createSignal
でローディング状態を管理しています。
UI コンポーネントはこのフックを呼び出すだけで、ビジネスロジックや副作用を意識せずに済みます。
ステップ 4: プレゼンテーション層の実装
UI コンポーネントの実装
プレゼンテーション層では、アプリケーション層のカスタムフックを利用して、UI を構築します。
typescript// presentation/components/UserList.tsx
import { Component, For, createSignal } from 'solid-js';
import { useUserManagement } from '../../application/usecases/useUserManagement';
import { UserApiClient } from '../../infrastructure/api/UserApiClient';
/**
* ユーザー一覧コンポーネント
* アプリケーション層のユースケースを利用し、UI のみに専念
*/
const UserList: Component = () => {
// リポジトリの注入(実際のアプリでは DI コンテナを使用することが多い)
const repository = new UserApiClient(
'https://api.example.com'
);
const {
users,
isCreating,
isDeleting,
createUser,
deleteUser,
refetch,
} = useUserManagement(repository);
const [name, setName] = createSignal('');
const [email, setEmail] = createSignal('');
const handleCreate = async () => {
try {
await createUser(name(), email());
setName('');
setEmail('');
} catch (error) {
alert('ユーザーの作成に失敗しました');
}
};
const handleDelete = async (id: string) => {
if (!confirm('本当に削除しますか?')) return;
try {
await deleteUser(id);
} catch (error) {
alert('ユーザーの削除に失敗しました');
}
};
return (
<div>
<h1>ユーザー管理</h1>
{/* ユーザー作成フォーム */}
<div>
<input
type='text'
placeholder='名前'
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
/>
<input
type='email'
placeholder='メールアドレス'
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
/>
<button
onClick={handleCreate}
disabled={isCreating()}
>
{isCreating() ? '作成中...' : 'ユーザーを作成'}
</button>
</div>
{/* ユーザー一覧 */}
<ul>
<For each={users()}>
{(user) => (
<li>
{user.name} ({user.email})
<button
onClick={() => handleDelete(user.id)}
disabled={isDeleting()}
>
削除
</button>
</li>
)}
</For>
</ul>
</div>
);
};
export default UserList;
このコンポーネントは、useUserManagement
フックを呼び出して、状態とメソッドを取得しています。UI ロジックのみに専念し、ビジネスロジックや API 呼び出しは一切含まれていません。
ステップ 5: 依存注入の改善
実際のアプリケーションでは、コンポーネント内でリポジトリを直接インスタンス化するのではなく、依存注入コンテナを使用します。
typescript// infrastructure/di/container.ts
import { UserApiClient } from '../api/UserApiClient';
import { UserRepository } from '../../domain/repositories/UserRepository';
/**
* 依存注入コンテナ
* リポジトリの実装を一元管理し、テストや差し替えを容易にする
*/
export const container = {
userRepository: new UserApiClient(
'https://api.example.com'
) as UserRepository,
};
コンポーネントでは、このコンテナから取得します。
typescript// presentation/components/UserList.tsx(改善版)
import { Component, For, createSignal } from 'solid-js';
import { useUserManagement } from '../../application/usecases/useUserManagement';
import { container } from '../../infrastructure/di/container';
const UserList: Component = () => {
// コンテナからリポジトリを取得
const {
users,
isCreating,
isDeleting,
createUser,
deleteUser,
refetch,
} = useUserManagement(container.userRepository);
const [name, setName] = createSignal('');
const [email, setEmail] = createSignal('');
const handleCreate = async () => {
try {
await createUser(name(), email());
setName('');
setEmail('');
} catch (error) {
alert('ユーザーの作成に失敗しました');
}
};
const handleDelete = async (id: string) => {
if (!confirm('本当に削除しますか?')) return;
try {
await deleteUser(id);
} catch (error) {
alert('ユーザーの削除に失敗しました');
}
};
return (
<div>
<h1>ユーザー管理</h1>
<div>
<input
type='text'
placeholder='名前'
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
/>
<input
type='email'
placeholder='メールアドレス'
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
/>
<button
onClick={handleCreate}
disabled={isCreating()}
>
{isCreating() ? '作成中...' : 'ユーザーを作成'}
</button>
</div>
<ul>
<For each={users()}>
{(user) => (
<li>
{user.name} ({user.email})
<button
onClick={() => handleDelete(user.id)}
disabled={isDeleting()}
>
削除
</button>
</li>
)}
</For>
</ul>
</div>
);
};
export default UserList;
この改善により、テスト時にはモックリポジトリに差し替えるだけで済みます。
ステップ 6: テストの実装
クリーンアーキテクチャの最大の利点は、各レイヤーを独立してテストできることです。
ドメイン層のテスト
typescript// domain/entities/User.test.ts
import { describe, it, expect } from 'vitest';
import { User } from './User';
describe('User Entity', () => {
it('有効な名前を検証できる', () => {
const user = new User(
'1',
'John Doe',
'john@example.com',
new Date()
);
expect(user.isValidName()).toBe(true);
});
it('短すぎる名前を検証できる', () => {
const user = new User(
'1',
'J',
'john@example.com',
new Date()
);
expect(user.isValidName()).toBe(false);
});
it('有効なメールアドレスを検証できる', () => {
const user = new User(
'1',
'John Doe',
'john@example.com',
new Date()
);
expect(user.isValidEmail()).toBe(true);
});
it('無効なメールアドレスを検証できる', () => {
const user = new User(
'1',
'John Doe',
'invalid-email',
new Date()
);
expect(user.isValidEmail()).toBe(false);
});
});
ドメイン層は純粋な TypeScript クラスなので、モックやスタブなしでテストできます。
アプリケーション層のテスト(モックリポジトリを使用)
typescript// application/usecases/useUserManagement.test.ts
import { describe, it, expect, vi } from 'vitest';
import {
renderHook,
waitFor,
} from '@solidjs/testing-library';
import { useUserManagement } from './useUserManagement';
import { UserRepository } from '../../domain/repositories/UserRepository';
import { User } from '../../domain/entities/User';
describe('useUserManagement', () => {
it('ユーザー一覧を取得できる', async () => {
// モックリポジトリを作成
const mockRepository: UserRepository = {
findAll: vi
.fn()
.mockResolvedValue([
new User(
'1',
'Alice',
'alice@example.com',
new Date()
),
new User(
'2',
'Bob',
'bob@example.com',
new Date()
),
]),
findById: vi.fn(),
create: vi.fn(),
delete: vi.fn(),
};
const { result } = renderHook(() =>
useUserManagement(mockRepository)
);
await waitFor(() => {
expect(result.users()).toHaveLength(2);
expect(result.users()?.[0].name).toBe('Alice');
});
});
it('ユーザーを作成できる', async () => {
const newUser = new User(
'3',
'Charlie',
'charlie@example.com',
new Date()
);
const mockRepository: UserRepository = {
findAll: vi.fn().mockResolvedValue([]),
findById: vi.fn(),
create: vi.fn().mockResolvedValue(newUser),
delete: vi.fn(),
};
const { result } = renderHook(() =>
useUserManagement(mockRepository)
);
await result.createUser(
'Charlie',
'charlie@example.com'
);
expect(mockRepository.create).toHaveBeenCalledWith({
name: 'Charlie',
email: 'charlie@example.com',
});
});
});
このテストでは、モックリポジトリを注入することで、実際の API 呼び出しを行わずにユースケースをテストしています。
ディレクトリ構成の全体像
最後に、プロジェクト全体のディレクトリ構成を示します。
bashsrc/
├── domain/ # ドメイン層
│ ├── entities/
│ │ ├── User.ts
│ │ └── User.test.ts
│ └── repositories/
│ └── UserRepository.ts
├── application/ # アプリケーション層
│ └── usecases/
│ ├── useUserManagement.ts
│ └── useUserManagement.test.ts
├── infrastructure/ # インフラ層
│ ├── api/
│ │ └── UserApiClient.ts
│ └── di/
│ └── container.ts
└── presentation/ # プレゼンテーション層
└── components/
└── UserList.tsx
この構成により、各レイヤーの責務が明確になり、変更の影響範囲が限定されます。
まとめ
本記事では、SolidJS にクリーンアーキテクチャを適用し、UI・状態・副作用を厳密に分離する実践手法をご紹介しました。
重要なポイント:
- レイヤー分割: ドメイン、アプリケーション、インフラ、プレゼンテーションの 4 層に分離することで、責務を明確化
- 依存関係の方向: 外側から内側へ一方向に依存し、ドメイン層はフレームワークに依存しない
- 依存性逆転の原則: インターフェースを定義し、具体的な実装を注入することでテストしやすさを確保
- カスタムフックの活用: アプリケーション層でユースケースをカスタムフック形式で実装し、状態管理と副作用を統合
- テストのしやすさ: 各レイヤーを独立してテストでき、モックやスタブの導入が容易
クリーンアーキテクチャは初期コストがかかるものの、長期的にはコードの保守性、拡張性、テストのしやすさを大幅に向上させます。SolidJS のリアクティブシステムと組み合わせることで、パフォーマンスと設計品質の両立が実現できるでしょう。
ぜひ、実際のプロジェクトでこのアプローチを試し、変更に強いアプリケーションを構築してみてください。
関連リンク
- article
SolidJS クリーンアーキテクチャ実践:UI・状態・副作用を厳密に分離する
- article
SolidJS フック相当 API 速見表:createSignal/createMemo/createEffect… 一覧
- article
SolidJS を macOS + yarn で最速構築:ESLint・Prettier・TSconfig の鉄板レシピ
- article
SolidJS × TanStack Query vs createResource:データ取得手段の実測比較
- article
SolidJS の hydration mismatch を根絶する:原因パターン 12 と再発防止チェック
- article
SolidJS のリアクティブ思考法:signal と effect を“脳内デバッグ”で理解
- article
Vue.js コンポーネント API 設計:props/emit/slot を最小 API でまとめる
- article
GitHub Copilot 前提のコーディング設計:コメント駆動 → テスト → 実装の最短ループ
- article
Tailwind CSS マルチブランド設計:CSS 変数と data-theme で横断対応
- article
Svelte フォーム体験設計:Optimistic UI/エラー復旧/再送戦略の型
- article
GitHub Actions でゼロダウンタイムリリース:canary/blue-green をパイプライン実装
- article
Git エイリアス 50 連発:長コマンドを一行にする仕事術まとめ
- 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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来