Web Components で社内デザインシステム基盤を作る:複数フレームワーク横断のコア層

社内で複数のフロントエンドフレームワークを運用していると、「React でも Vue でも同じボタンデザインを使いたい」という要望が出てきますよね。 しかし、フレームワークごとにコンポーネントを作り直すのは、保守性やデザインの一貫性に課題があります。
こうした悩みを解決する手段として、Web Components を活用した社内デザインシステム基盤の構築が注目されています。 本記事では、フレームワークに依存しない再利用可能なコンポーネント層を、Web Components でどう実現するかを具体的に解説していきます。
背景
複数フレームワーク環境の現実
現代の企業開発では、プロジェクトごとに異なるフレームワークを採用するケースが増えています。 React で構築された管理画面、Vue.js で開発された顧客向けサイト、Angular を使ったダッシュボードなど、技術スタックが混在することは珍しくありません。
こうした環境で共通のデザインシステムを維持するには、各フレームワーク向けに個別実装が必要となり、工数とメンテナンスコストが増大してしまいます。
Web Components の登場
Web Components は、ブラウザ標準の技術として登場したフレームワーク非依存のコンポーネント仕様です。 主に以下の 3 つの技術で構成されています。
# | 技術 | 概要 |
---|---|---|
1 | Custom Elements | 独自の HTML タグを定義できる仕様 |
2 | Shadow DOM | カプセル化された DOM・スタイルを提供 |
3 | HTML Templates | 再利用可能な HTML マークアップを定義 |
これらの技術により、React や Vue といった特定のフレームワークに依存せず、どこでも動作するコンポーネントを作成できるのです。
以下の図は、Web Components が各フレームワークで共通利用される構造を示しています。
mermaidflowchart TB
wc["Web Components<br/>コア層"]
react["React アプリ"]
vue["Vue.js アプリ"]
angular["Angular アプリ"]
vanilla["バニラ JS"]
wc -->|利用| react
wc -->|利用| vue
wc -->|利用| angular
wc -->|利用| vanilla
style wc fill:#4A90E2,color:#fff
この図が示すように、Web Components を中心に据えることで、複数のフレームワーク環境で統一されたコンポーネントを提供できます。
課題
フレームワーク個別実装の問題点
従来のアプローチでは、フレームワークごとに同じ UI コンポーネントを実装する必要がありました。 たとえば、ボタンコンポーネント 1 つをとっても、React 版・Vue 版・Angular 版と 3 つのコードベースを保守しなければなりません。
この方式には以下のような課題があります。
- 実装コストの増大: 同じ機能を複数回実装する無駄が発生
- デザイン不整合のリスク: 実装者によって微妙な差異が生まれやすい
- 修正の手間: バグ修正や機能追加を全フレームワークで対応する必要がある
- 学習コストの分散: 各フレームワークの記法を習得する必要がある
デザインシステムの保守性
デザインシステムは、一度作れば終わりではありません。 ブランドガイドラインの変更、アクセシビリティ対応の強化、新しいコンポーネントの追加など、継続的なメンテナンスが必要です。
フレームワークごとに実装が分散していると、変更が全体に波及するのに時間がかかり、結果としてデザインシステムが形骸化してしまうリスクがあります。
以下の図は、従来の個別実装と Web Components 基盤の違いを示しています。
mermaidflowchart LR
subgraph legacy ["従来の個別実装"]
direction TB
ds1["デザイン仕様"]
react1["React実装"]
vue1["Vue実装"]
angular1["Angular実装"]
ds1 --> react1
ds1 --> vue1
ds1 --> angular1
end
subgraph unified ["Web Components基盤"]
direction TB
ds2["デザイン仕様"]
wc2["Web Components<br/>コア実装"]
frameworks2["全フレームワーク"]
ds2 --> wc2
wc2 --> frameworks2
end
legacy -. 変革 .-> unified
統合型では、コア実装を 1 つに集約することで、保守性と一貫性を大幅に向上できます。
解決策
Web Components によるコア層設計
Web Components を活用することで、フレームワーク非依存の共通コンポーネント層を構築できます。 この層を社内デザインシステムの「コア」として位置づけ、各フレームワークから利用する設計が効果的です。
具体的には、以下のような設計アプローチを取ります。
# | 設計方針 | 詳細 |
---|---|---|
1 | Custom Elements で独自タグを定義 | <ds-button> などの独自タグとして提供 |
2 | Shadow DOM でスタイルをカプセル化 | 外部スタイルの影響を受けない独立性を確保 |
3 | Properties と Attributes で制御 | フレームワークから簡単に属性・プロパティで制御可能 |
4 | イベント駆動の設計 | Custom Event で親コンポーネントと通信 |
5 | TypeScript で型安全性を確保 | 開発者体験の向上と保守性の強化 |
設計の核心ポイント
Web Components を社内デザインシステムの基盤として活用する際、以下の 3 つが核心となります。
カプセル化: Shadow DOM により、コンポーネント内のスタイルと DOM ツリーが外部から隔離され、予期しないスタイル干渉を防げます。
再利用性: 標準仕様に基づいているため、どのフレームワークでも <ds-button>
のように HTML タグとして利用でき、学習コストが低く抑えられます。
メンテナンス性: コア実装が 1 つなので、修正や機能追加が一箇所で済み、全フレームワークに即座に反映されます。
以下の図は、Web Components ベースのデザインシステム全体像を示しています。
mermaidflowchart TB
subgraph core["コア層(Web Components)"]
direction LR
button["ds-button"]
input["ds-input"]
card["ds-card"]
modal["ds-modal"]
end
subgraph framework["フレームワーク層"]
direction LR
react["React"]
vue["Vue"]
angular["Angular"]
end
subgraph app["アプリケーション層"]
direction LR
admin["管理画面"]
customer["顧客サイト"]
dashboard["ダッシュボード"]
end
core --> framework
framework --> app
style core fill:#4A90E2,color:#fff
style framework fill:#7ED321,color:#fff
style app fill:#F5A623,color:#fff
この 3 層構造により、デザインの一貫性を保ちながら、各アプリケーションの独自性も維持できます。
具体例
ボタンコンポーネントの実装
ここでは、実際に Web Components でボタンコンポーネントを実装していきます。 段階的に説明しますので、初めての方でも理解しやすいかと思います。
ステップ 1: 基本構造の定義
まず、Custom Elements API を使って独自のボタン要素を定義します。
typescript// ds-button.ts
// カスタムエレメントのクラスを定義
class DSButton extends HTMLElement {
constructor() {
// 親クラスのコンストラクタを呼び出す
super();
// Shadow DOM をアタッチ(カプセル化のため)
this.attachShadow({ mode: 'open' });
}
}
このコードでは、HTMLElement
を継承したカスタムクラスを定義し、attachShadow()
で Shadow DOM を有効化しています。
ステップ 2: スタイルとテンプレートの作成
次に、ボタンのスタイルと HTML テンプレートを定義します。
typescript// ds-button.ts(続き)
// ボタンのスタイルを定義
const styles = `
:host {
display: inline-block;
}
button {
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background-color: #4A90E2;
color: #ffffff;
}
button:hover {
background-color: #357ABD;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* バリアント対応 */
:host([variant="secondary"]) button {
background-color: #7ED321;
}
:host([variant="danger"]) button {
background-color: #D0021B;
}
`;
:host
セレクタは、カスタムエレメント自身を指定する特別な擬似クラスです。
これにより、コンポーネントレベルでスタイルを制御できます。
typescript// ds-button.ts(続き)
// HTMLテンプレートを定義
const template = `
<style>${styles}</style>
<button part="button">
<slot></slot>
</button>
`;
<slot>
タグは、コンポーネント利用時に渡された子要素を挿入する場所を示します。
part
属性により、外部からこのボタン要素にスタイルを適用することも可能です。
ステップ 3: ライフサイクルメソッドの実装
Custom Elements には、ライフサイクルメソッドが用意されています。 これらを活用して、属性の変化に応じた振る舞いを実装します。
typescript// ds-button.ts(続き)
class DSButton extends HTMLElement {
// 監視する属性を指定
static get observedAttributes() {
return ['disabled', 'variant', 'size'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
// DOM に追加されたときに呼ばれる
connectedCallback() {
// テンプレートを Shadow DOM に挿入
if (this.shadowRoot) {
this.shadowRoot.innerHTML = template;
}
// ボタン要素への参照を取得
this._button = this.shadowRoot?.querySelector('button');
// クリックイベントを設定
this._button?.addEventListener(
'click',
this._handleClick.bind(this)
);
// 初期状態を反映
this._updateState();
}
// DOM から削除されたときに呼ばれる
disconnectedCallback() {
// イベントリスナーをクリーンアップ
this._button?.removeEventListener(
'click',
this._handleClick.bind(this)
);
}
}
connectedCallback()
はコンポーネントが DOM に追加された際に実行され、初期化処理を行う場所として最適です。
ステップ 4: 属性とプロパティの管理
属性の変更を検知して、コンポーネントの状態を更新する仕組みを実装します。
typescript// ds-button.ts(続き)
class DSButton extends HTMLElement {
// ... 前述のコード ...
// 属性が変更されたときに呼ばれる
attributeChangedCallback(
name: string,
oldValue: string,
newValue: string
) {
if (oldValue !== newValue) {
this._updateState();
}
}
// 内部状態を更新するメソッド
private _updateState() {
if (!this._button) return;
// disabled 属性の反映
const disabled = this.hasAttribute('disabled');
this._button.disabled = disabled;
// size 属性の反映
const size = this.getAttribute('size');
if (size === 'small') {
this._button.style.padding = '8px 16px';
this._button.style.fontSize = '14px';
} else if (size === 'large') {
this._button.style.padding = '16px 32px';
this._button.style.fontSize = '18px';
}
}
// disabled プロパティのゲッター・セッター
get disabled(): boolean {
return this.hasAttribute('disabled');
}
set disabled(value: boolean) {
if (value) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
}
属性とプロパティの両方でアクセス可能にすることで、HTML と JavaScript の両方から柔軟に制御できます。
ステップ 5: イベントの実装
ボタンクリック時に Custom Event を発火させ、親コンポーネントと通信します。
typescript// ds-button.ts(続き)
class DSButton extends HTMLElement {
// ... 前述のコード ...
// クリックイベントハンドラ
private _handleClick(event: Event) {
// disabled 状態ではイベントを発火しない
if (this.disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
// カスタムイベントを発火
const customEvent = new CustomEvent('ds-click', {
bubbles: true, // イベントバブリングを有効化
composed: true, // Shadow DOM の境界を超えて伝播
detail: {
timestamp: Date.now(),
variant: this.getAttribute('variant') || 'primary',
},
});
this.dispatchEvent(customEvent);
}
}
composed: true
を設定することで、Shadow DOM の境界を越えてイベントが伝播し、通常の DOM イベントと同様に扱えます。
ステップ 6: カスタムエレメントの登録
最後に、定義したクラスをカスタムエレメントとして登録します。
typescript// ds-button.ts(続き)
// カスタムエレメントとして登録
customElements.define('ds-button', DSButton);
// TypeScript の型定義をエクスポート
export { DSButton };
これで、<ds-button>
タグがブラウザで認識され、利用可能になります。
以下の図は、Web Components の内部構造とライフサイクルを示しています。
mermaidstateDiagram-v2
[*] --> Created: constructor()
Created --> Connected: connectedCallback()
Connected --> Rendered: テンプレート挿入
Rendered --> Listening: イベント設定
Listening --> AttrChanged: 属性変更
AttrChanged --> Listening: 状態更新
Listening --> Disconnected: disconnectedCallback()
Disconnected --> [*]: クリーンアップ
このライフサイクルを理解することで、適切なタイミングで処理を実行できます。
React での利用例
作成した Web Components を React で利用する方法を見ていきましょう。
ステップ 1: Web Components の読み込み
まず、作成した Web Components を React プロジェクトにインポートします。
typescript// App.tsx
import React, { useRef, useEffect } from 'react';
// Web Components をインポート(登録のため)
import './components/ds-button';
インポートするだけで customElements.define()
が実行され、カスタムエレメントが登録されます。
ステップ 2: TypeScript 型定義の拡張
TypeScript で Web Components を利用する際、JSX の型定義を拡張する必要があります。
typescript// types/custom-elements.d.ts
declare namespace JSX {
interface IntrinsicElements {
'ds-button': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
},
HTMLElement
>;
}
}
この型定義により、TypeScript の型チェックとエディタの補完が効くようになります。
ステップ 3: React コンポーネントでの利用
実際に React コンポーネントで Web Components を使ってみます。
typescript// App.tsx(続き)
const App: React.FC = () => {
// Web Components への参照
const buttonRef = useRef<HTMLElement>(null);
// カスタムイベントのリスナーを設定
useEffect(() => {
const button = buttonRef.current;
if (!button) return;
const handleClick = (event: Event) => {
const customEvent = event as CustomEvent;
console.log(
'ボタンがクリックされました:',
customEvent.detail
);
};
// ds-click イベントをリスン
button.addEventListener('ds-click', handleClick);
// クリーンアップ
return () => {
button.removeEventListener('ds-click', handleClick);
};
}, []);
return (
<div className='app'>
<h1>React で Web Components を利用</h1>
{/* Web Components をHTMLタグとして利用 */}
<ds-button ref={buttonRef} variant='primary'>
プライマリボタン
</ds-button>
<ds-button variant='secondary' size='large'>
セカンダリボタン
</ds-button>
<ds-button variant='danger' disabled>
無効化されたボタン
</ds-button>
</div>
);
};
export default App;
React でも通常の HTML タグと同様に、直感的に Web Components を利用できます。
Vue.js での利用例
次に、同じ Web Components を Vue.js で利用する方法を見ていきます。
ステップ 1: Vue での設定
Vue 3 では、カスタムエレメントを認識させるための設定が必要です。
typescript// main.ts
import { createApp } from 'vue';
import App from './App.vue';
// Web Components をインポート
import './components/ds-button';
const app = createApp(App);
// カスタムエレメントとして認識させる
app.config.compilerOptions.isCustomElement = (tag) => {
return tag.startsWith('ds-');
};
app.mount('#app');
isCustomElement
を設定することで、Vue が ds-
プレフィックスのタグをカスタムエレメントとして扱います。
ステップ 2: Vue コンポーネントでの利用
Vue の template 内で Web Components を利用します。
vue<!-- App.vue -->
<template>
<div class="app">
<h1>Vue で Web Components を利用</h1>
<!-- Web Components を利用 -->
<ds-button variant="primary" @ds-click="handleClick">
プライマリボタン
</ds-button>
<ds-button
variant="secondary"
size="large"
@ds-click="handleClick"
>
セカンダリボタン
</ds-button>
<ds-button
variant="danger"
:disabled="isDisabled"
@ds-click="handleClick"
>
{{ isDisabled ? '無効' : '有効' }}
</ds-button>
<button @click="toggleDisabled">
ボタンの状態を切り替え
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// リアクティブな状態管理
const isDisabled = ref(false);
// イベントハンドラ
const handleClick = (event: CustomEvent) => {
console.log('ボタンがクリックされました:', event.detail);
};
// disabled 状態を切り替え
const toggleDisabled = () => {
isDisabled.value = !isDisabled.value;
};
</script>
<style scoped>
.app {
padding: 20px;
}
ds-button {
margin: 8px;
}
</style>
Vue でも、カスタムイベントを @ds-click
のように記述でき、リアクティブなプロパティバインディングも機能します。
パッケージ化と配布
社内デザインシステムとして運用するため、Web Components をパッケージ化して配布する方法を解説します。
ステップ 1: プロジェクト構成
パッケージ化に適したプロジェクト構造を整えます。
bash# プロジェクトのディレクトリ構成
design-system/
├── src/
│ ├── components/
│ │ ├── ds-button/
│ │ │ ├── ds-button.ts
│ │ │ └── index.ts
│ │ ├── ds-input/
│ │ └── ds-card/
│ ├── index.ts
│ └── types/
│ └── index.d.ts
├── dist/
├── package.json
├── tsconfig.json
└── rollup.config.js
ステップ 2: package.json の設定
npm パッケージとして公開するための設定を記述します。
json{
"name": "@company/design-system",
"version": "1.0.0",
"description": "社内デザインシステム - Web Components",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/types/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.0",
"rollup": "^3.20.0",
"typescript": "^5.0.0"
},
"keywords": [
"web-components",
"design-system",
"custom-elements"
]
}
main
と module
を両方指定することで、CommonJS と ES Modules の両方に対応できます。
ステップ 3: ビルド設定
Rollup を使ってバンドル設定を行います。
javascript// rollup.config.js
import typescript from '@rollup/plugin-typescript';
export default {
// エントリーポイント
input: 'src/index.ts',
// 出力設定(複数フォーマット対応)
output: [
{
file: 'dist/index.js',
format: 'cjs', // CommonJS
sourcemap: true,
},
{
file: 'dist/index.esm.js',
format: 'esm', // ES Modules
sourcemap: true,
},
],
// プラグイン設定
plugins: [
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: 'dist/types',
}),
],
};
この設定により、TypeScript で書いたコードがバンドルされ、型定義ファイルも自動生成されます。
ステップ 4: 社内 npm レジストリへの公開
ビルドしたパッケージを社内 npm レジストリに公開します。
bash# パッケージをビルド
yarn build
# 社内レジストリを指定して公開
yarn publish --registry https://npm.company.internal
公開後、各プロジェクトで以下のようにインストールできます。
bash# プロジェクトでインストール
yarn add @company/design-system --registry https://npm.company.internal
以下の図は、パッケージ化から配布、利用までの全体フローを示しています。
mermaidflowchart LR
subgraph dev ["開発フロー"]
direction TB
code["コンポーネント<br/>実装"]
build["ビルド"]
publish["npm publish"]
code --> build
build --> publish
end
subgraph dist ["配布"]
direction TB
registry["社内npm<br/>レジストリ"]
end
subgraph useFlow ["利用フロー"]
direction TB
install["yarn add"]
importMod["import"]
useApp["アプリで利用"]
install --> importMod
importMod --> useApp
end
dev --> registry
registry --> useFlow
style dev fill:#4A90E2,color:#fff
style dist fill:#7ED321,color:#fff
style useFlow fill:#F5A623,color:#fff
この一連の流れにより、全社で統一されたデザインシステムを効率的に展開できます。
テストとドキュメント
Web Components の品質を保つため、テストとドキュメント整備も重要です。
単体テストの実装例
Web Components のテストには、Jest と Testing Library を組み合わせて使います。
typescript// ds-button.test.ts
import { screen, waitFor } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import './ds-button';
describe('ds-button', () => {
// 各テスト前にDOMをクリア
beforeEach(() => {
document.body.innerHTML = '';
});
test('レンダリングされること', () => {
// ボタンをDOMに追加
document.body.innerHTML =
'<ds-button>テストボタン</ds-button>';
// Shadow DOM 内のボタンを取得
const dsButton = document.querySelector('ds-button');
const button =
dsButton?.shadowRoot?.querySelector('button');
expect(button).toBeInTheDocument();
expect(button?.textContent).toBe('テストボタン');
});
test('クリックイベントが発火すること', async () => {
document.body.innerHTML =
'<ds-button>クリック</ds-button>';
const dsButton = document.querySelector('ds-button');
const eventHandler = jest.fn();
// カスタムイベントをリスン
dsButton?.addEventListener('ds-click', eventHandler);
// ボタンをクリック
const button =
dsButton?.shadowRoot?.querySelector('button');
await userEvent.click(button!);
// イベントが発火したことを確認
expect(eventHandler).toHaveBeenCalledTimes(1);
});
test('disabled 属性が機能すること', () => {
document.body.innerHTML =
'<ds-button disabled>無効</ds-button>';
const dsButton = document.querySelector(
'ds-button'
) as any;
const button =
dsButton?.shadowRoot?.querySelector('button');
// ボタンが無効化されていることを確認
expect(button?.disabled).toBe(true);
expect(dsButton.disabled).toBe(true);
});
});
Shadow DOM を考慮したテストコードにより、コンポーネントの品質を担保できます。
まとめ
本記事では、Web Components を活用した社内デザインシステム基盤の構築方法について解説しました。 ポイントを振り返りましょう。
Web Components の優位性: ブラウザ標準技術であり、React・Vue・Angular など、どのフレームワークでも利用可能な汎用性を持っています。
実装のコツ: Custom Elements、Shadow DOM、スロットなどの仕組みを理解し、適切にライフサイクルメソッドを活用することで、堅牢なコンポーネントを構築できます。
フレームワーク統合: 各フレームワークの設定を適切に行うことで、Web Components をネイティブな要素のように扱えます。
パッケージ化の重要性: npm パッケージとして配布することで、社内全体でデザインシステムを共有し、一元管理が実現できます。
Web Components によるデザインシステム基盤は、フレームワークの変遷にも強く、長期的な保守性に優れています。 初期構築には多少の学習コストがかかりますが、複数プロジェクトで一貫した UI を提供し続けられる価値は非常に大きいでしょう。
まずは小さなコンポーネント(ボタン、インプット)から始めて、段階的にデザインシステムを育てていく approach が現実的です。 ぜひ、貴社のプロジェクトでも Web Components ベースのデザインシステム構築にチャレンジしてみてください。
関連リンク
- article
Web Components で社内デザインシステム基盤を作る:複数フレームワーク横断のコア層
- article
Web Components で作るモーダルダイアログ:フォーカス管理・閉じる動線まで実装
- article
Web Components の API 設計原則:属性 vs プロパティ vs メソッドの境界線
- article
Web Components スタイリング速見表:`::part`/`::slotted`/AdoptedStyleSheets(Constructable Stylesheets)
- article
Web Components を Vite + TypeScript + yarn で最短セットアップする完全手順
- article
Web Components 全体像を一枚図で理解する:Shadow DOM/slots/ElementInternals の関係
- article
Convex で Presence(在席)機能を実装:ユーザーステータスのリアルタイム同期
- article
Next.js の RSC 境界設計:Client Components を最小化する責務分離戦略
- article
Mermaid 矢印・接続子チートシート:線種・方向・注釈の一覧早見
- article
Codex とは何か?AI コーディングの基礎・仕組み・適用範囲をやさしく解説
- article
MCP サーバー 設計ベストプラクティス:ツール定義、権限分離、スキーマ設計の要点まとめ
- article
Astro で動的 OG 画像を生成する:Satori/Canvas 連携の実装レシピ
- 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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来