Remix と React の連携パターン集

Webアプリケーション開発の現場では、パフォーマンスとSEOの両立が求められる今、RemixとReactの連携が注目を集めています。本記事では、実際の開発で活用できる連携パターンを体系的に解説し、モダンなWebアプリケーション構築のためのベストプラクティスをご紹介いたします。
RemixはReactをベースとしたフルスタックフレームワークとして、従来のSPAが抱える課題を解決する革新的なアプローチを提供します。この連携により、開発者はより効率的で保守性の高いアプリケーションを構築できるでしょう。
背景
Remixの特徴とReactとの親和性
RemixはReactエコシステムの上に構築されたフルスタックフレームワークです。Reactの宣言的UI構築の利点を保ちながら、サーバーサイドでのデータ処理とクライアントサイドでのインタラクションを seamlessly に統合できます。
RemixとReactの技術的関係性を図で確認してみましょう。
mermaidflowchart TD
react[React Core] -->|ベース| remix[Remix Framework]
remix -->|SSR| server[Server Side]
remix -->|CSR| client[Client Side]
server -->|HTML| browser[ブラウザ]
client -->|Interactive| browser
remix -->|Routing| nested[Nested Routes]
remix -->|Data| loaders[Loaders/Actions]
この図が示すように、RemixはReactを中核として、サーバーとクライアント両方での最適化を実現しています。
Remixが注目される理由は以下の通りです。
# | 特徴 | 説明 |
---|---|---|
1 | Web標準準拠 | FormDataやRequest/Responseオブジェクトなど、ブラウザ標準APIを活用 |
2 | プログレッシブエンハンスメント | JavaScriptが無効でも基本機能が動作する設計 |
3 | ネストしたルーティング | ページ構造に沿った直感的なルート設計 |
フルスタックフレームワークとしてのRemix
Remixは単なるReactのラッパーではありません。データ層からプレゼンテーション層まで、アプリケーション全体を統合的に設計できるフルスタックソリューションです。
mermaidsequenceDiagram
participant User as ユーザー
participant Browser as ブラウザ
participant Remix as Remix Server
participant DB as データベース
User->>Browser: ページアクセス
Browser->>Remix: HTTP Request
Remix->>DB: データ取得
DB->>Remix: データ返却
Remix->>Browser: HTML + React App
Browser->>User: 初期表示
User->>Browser: ユーザー操作
Browser->>Remix: Action実行
Remix->>DB: データ更新
DB->>Remix: 更新完了
Remix->>Browser: 更新されたデータ
このシーケンス図では、初期ロードからユーザー操作まで、RemixがどのようにReactアプリケーションとデータ層を仲介するかを示しています。
SSRとCSRの使い分け
RemixではSSR(Server-Side Rendering)とCSR(Client-Side Rendering)を適切に使い分けることで、パフォーマンスとユーザー体験の最適化を実現できます。
図で理解できる要点:
- 初期ロードはSSRで高速表示
- 操作後はCSRでスムーズな体験
- データ更新は自動的に最適化される
課題
従来のReactアプリケーションの限界
従来のReact SPAには、いくつかの根本的な課題が存在します。これらの課題は、特に大規模なアプリケーションにおいて顕著に現れます。
React SPAの主な課題を整理してみましょう。
# | 課題 | 影響 | 発生場面 |
---|---|---|---|
1 | 初期ロード時間の長期化 | ユーザー離脱率増加 | 大容量バンドル、ネットワーク制約 |
2 | SEO対応の複雑化 | 検索流入減少 | 動的コンテンツ、メタデータ管理 |
3 | 状態管理の複雑化 | 開発・保守コスト増 | 大規模アプリ、チーム開発 |
特に初期ロードの問題は深刻です。
javascript// 従来のReact SPAの問題例
// すべてのJavaScriptがロードされるまで何も表示されない
function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// API呼び出し(クライアントサイドでのみ実行)
fetchData().then(response => {
setData(response);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>; // 初期表示まで時間がかかる
}
return <div>{data}</div>;
}
この実装では、JavaScript bundle のダウンロード、パース、実行、そしてAPI呼び出しがすべて完了するまで、ユーザーは meaningful content を見ることができません。
パフォーマンスとSEOの両立問題
React SPAでは、パフォーマンスとSEOの両立が困難な課題となります。
javascript// SEO対応のための複雑な設定例
import { Helmet } from 'react-helmet';
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
// クライアントサイドでのメタタグ設定
// 検索エンジンに適切に認識されない可能性
return (
<>
<Helmet>
<title>{product?.name || 'Loading...'}</title>
<meta name="description" content={product?.description || ''} />
</Helmet>
{product ? <ProductDetails product={product} /> : <Spinner />}
</>
);
}
このアプローチでは、検索エンジンのクローラーが適切なメタデータを取得できない場合があります。
開発効率と保守性のバランス
大規模なReactアプリケーションでは、開発効率と保守性のバランスを取ることが困難になります。
javascript// 複雑な状態管理の例
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
const [loading, setLoading] = useState({});
const [errors, setErrors] = useState({});
// 複数のuseEffectと複雑な依存関係
useEffect(() => {
// 認証チェック
}, []);
useEffect(() => {
// 商品データ取得
}, [user]);
useEffect(() => {
// カート同期
}, [user, products]);
// 大量のcontext value
const value = {
user, setUser,
products, setProducts,
cart, setCart,
loading, setLoading,
errors, setErrors,
// ... 数十個のステートとアクション
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
この設計では、ステートの依存関係が複雑になり、デバッグや機能追加が困難になります。
課題整理の要点:
- 初期ロード問題は UX に直接影響
- SEO対応の複雑化は集客に影響
- 保守性の低下は開発コストに影響
解決策
Remixが提供する連携パターン
RemixはReactアプリケーションの課題を解決するため、いくつかの革新的な連携パターンを提供します。これらのパターンを理解することで、効率的なWebアプリケーションを構築できます。
主要な連携パターンを図で確認しましょう。
mermaidflowchart LR
subgraph "Remix連携パターン"
loader[Loader Pattern]
action[Action Pattern]
nested[Nested Routing]
meta[Meta Function]
error[Error Boundary]
end
subgraph "React Integration"
loader --> ssr[SSR Components]
action --> forms[React Forms]
nested --> components[Nested Components]
meta --> seo[SEO Optimization]
error --> fallback[Error Fallback]
end
ssr --> performance[パフォーマンス向上]
forms --> ux[UX改善]
components --> maintainability[保守性向上]
seo --> visibility[検索性向上]
fallback --> reliability[信頼性向上]
この図は、Remixの各パターンがReactとどのように統合され、最終的にどのような価値を生み出すかを示しています。
LoaderとActionによるデータ処理
Remixの最も特徴的な機能の一つが、LoaderとActionによるデータ処理パターンです。
Loaderパターンの実装
typescript// app/routes/products.$productId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
// Loader関数:サーバーサイドでデータを取得
export async function loader({ params }: LoaderFunctionArgs) {
const productId = params.productId;
// データベースからデータを取得
const product = await db.product.findUnique({
where: { id: productId },
include: { reviews: true, category: true }
});
if (!product) {
throw new Response("Product not found", { status: 404 });
}
return json({ product });
}
Loaderで取得したデータをReactコンポーネントで使用します。
typescript// 同じファイル内のReactコンポーネント
export default function ProductPage() {
// useLoaderDataでサーバーサイドのデータを取得
const { product } = useLoaderData<typeof loader>();
return (
<div className="product-page">
<h1>{product.name}</h1>
<img src={product.imageUrl} alt={product.name} />
<p>{product.description}</p>
<div className="price">¥{product.price.toLocaleString()}</div>
{/* Reactコンポーネントとして通常通り記述 */}
<ProductReviews reviews={product.reviews} />
</div>
);
}
Actionパターンの実装
Actionパターンでは、フォーム送信やデータ更新処理をサーバーサイドで実行できます。
typescript// app/routes/products.$productId.edit.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
// Action関数:フォーム送信時の処理
export async function action({ request, params }: ActionFunctionArgs) {
const formData = await request.formData();
const productId = params.productId;
// バリデーション
const name = formData.get("name");
const price = formData.get("price");
if (!name || !price) {
return json({ error: "名前と価格は必須です" }, { status: 400 });
}
// データベース更新
await db.product.update({
where: { id: productId },
data: {
name: name.toString(),
price: parseInt(price.toString())
}
});
return redirect(`/products/${productId}`);
}
Actionを使用するReactフォームコンポーネントです。
typescriptexport default function EditProduct() {
const { product } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
return (
<Form method="post" className="edit-form">
<div className="field">
<label htmlFor="name">商品名</label>
<input
type="text"
id="name"
name="name"
defaultValue={product.name}
required
/>
</div>
<div className="field">
<label htmlFor="price">価格</label>
<input
type="number"
id="price"
name="price"
defaultValue={product.price}
required
/>
</div>
{/* エラー表示 */}
{actionData?.error && (
<div className="error">{actionData.error}</div>
)}
<button type="submit">更新</button>
</Form>
);
}
Reactコンポーネントの最適化手法
RemixでReactコンポーネントを最適化するための手法をご紹介します。
レンダリング最適化
typescript// app/components/ProductCard.tsx
import { memo } from "react";
// React.memoでコンポーネントを最適化
export const ProductCard = memo(function ProductCard({
product,
onAddToCart
}: {
product: Product;
onAddToCart: (productId: string) => void;
}) {
return (
<div className="product-card">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>¥{product.price.toLocaleString()}</p>
<button onClick={() => onAddToCart(product.id)}>
カートに追加
</button>
</div>
);
});
Suspenseを活用した非同期処理
typescript// app/routes/dashboard.tsx
import { Suspense } from "react";
import { Await, defer } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader() {
// 重要なデータは即座に取得
const criticalData = await getCriticalData();
// 重要でないデータは遅延読み込み
const nonCriticalData = getNonCriticalData(); // Promiseを返す
return defer({
criticalData,
nonCriticalData
});
}
export default function Dashboard() {
const { criticalData, nonCriticalData } = useLoaderData<typeof loader>();
return (
<div>
{/* 重要なデータは即座に表示 */}
<CriticalSection data={criticalData} />
{/* 重要でないデータは遅延読み込み */}
<Suspense fallback={<div>データを読み込み中...</div>}>
<Await resolve={nonCriticalData}>
{(data) => <NonCriticalSection data={data} />}
</Await>
</Suspense>
</div>
);
}
解決策の要点:
- Loaderで SSR によるデータ取得を簡素化
- Action で フォーム処理を Web 標準化
- React の最適化手法と組み合わせて性能向上
具体例
基本的なRemix + Reactアプリケーション
実際のWebアプリケーション構築を通じて、RemixとReactの連携パターンを具体的に学んでいきましょう。
プロジェクト初期設定
まず、Remixプロジェクトを作成します。
bash# Remixアプリケーションの作成
npx create-remix@latest my-remix-app
# 依存関係のインストール
cd my-remix-app
yarn install
# 開発サーバーの起動
yarn dev
基本的なディレクトリ構造を確認します。
bashmy-remix-app/
├── app/
│ ├── routes/ # ページルート
│ ├── components/ # 再利用可能コンポーネント
│ ├── root.tsx # アプリケーションルート
│ └── entry.client.tsx
├── public/ # 静的ファイル
└── package.json
ルート構造の設計
Remixのネストルーティングを活用したアプリケーション構造を作成します。
typescript// app/root.tsx
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export default function App() {
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<div id="app">
<header>
<nav>
<a href="/">ホーム</a>
<a href="/products">商品一覧</a>
<a href="/about">会社概要</a>
</nav>
</header>
<main>
<Outlet />
</main>
</div>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
メインページのルートを作成します。
typescript// app/routes/_index.tsx
import type { MetaFunction } from "@remix-run/node";
export const meta: MetaFunction = () => {
return [
{ title: "ホーム | MyShop" },
{ name: "description", content: "最高品質の商品をお届けします" },
];
};
export default function Index() {
return (
<div className="home">
<h1>MyShopへようこそ</h1>
<p>最新の商品をチェックしてください。</p>
<a href="/products" className="cta-button">
商品を見る
</a>
</div>
);
}
データフェッチパターンの実装
商品一覧ページでのデータフェッチパターンを実装します。
typescript// app/routes/products._index.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, Link } from "@remix-run/react";
// 型定義
interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
}
// Loader関数でデータを取得
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const category = url.searchParams.get("category");
// データベースから商品を取得(例:Prisma使用)
const products = await db.product.findMany({
where: category ? { category } : undefined,
orderBy: { createdAt: "desc" },
take: 20,
});
const categories = await db.category.findMany();
return json({ products, categories, selectedCategory: category });
}
商品一覧コンポーネントを実装します。
typescript// 同じファイル内のReactコンポーネント
export default function ProductsIndex() {
const { products, categories, selectedCategory } = useLoaderData<typeof loader>();
return (
<div className="products-page">
<h1>商品一覧</h1>
{/* カテゴリーフィルター */}
<div className="category-filter">
<Link
to="/products"
className={!selectedCategory ? "active" : ""}
>
すべて
</Link>
{categories.map((category) => (
<Link
key={category.id}
to={`/products?category=${category.slug}`}
className={selectedCategory === category.slug ? "active" : ""}
>
{category.name}
</Link>
))}
</div>
{/* 商品グリッド */}
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
// 商品カードコンポーネント
function ProductCard({ product }: { product: Product }) {
return (
<Link to={`/products/${product.id}`} className="product-card">
<img src={product.imageUrl} alt={product.name} />
<div className="product-info">
<h3>{product.name}</h3>
<p className="price">¥{product.price.toLocaleString()}</p>
<p className="description">{product.description}</p>
</div>
</Link>
);
}
フォーム処理とバリデーション
商品レビュー投稿フォームを実装します。
typescript// app/routes/products.$productId.reviews.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useLoaderData } from "@remix-run/react";
// バリデーション関数
function validateReview(formData: FormData) {
const errors: Record<string, string> = {};
const rating = formData.get("rating");
const comment = formData.get("comment");
if (!rating) {
errors.rating = "評価は必須です";
} else if (isNaN(Number(rating)) || Number(rating) < 1 || Number(rating) > 5) {
errors.rating = "評価は1〜5の範囲で入力してください";
}
if (!comment) {
errors.comment = "コメントは必須です";
} else if (comment.toString().length < 10) {
errors.comment = "コメントは10文字以上で入力してください";
}
return Object.keys(errors).length > 0 ? errors : null;
}
// Action関数
export async function action({ request, params }: ActionFunctionArgs) {
const formData = await request.formData();
const productId = params.productId;
// バリデーション
const errors = validateReview(formData);
if (errors) {
return json({ errors }, { status: 400 });
}
// レビューを保存
await db.review.create({
data: {
productId,
rating: Number(formData.get("rating")),
comment: formData.get("comment")!.toString(),
authorName: formData.get("authorName")!.toString(),
},
});
return redirect(`/products/${productId}`);
}
レビューフォームコンポーネントを実装します。
typescriptexport default function ProductReviews() {
const { product } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
return (
<div className="reviews-section">
<h2>レビューを投稿する</h2>
<Form method="post" className="review-form">
<div className="field">
<label htmlFor="authorName">お名前</label>
<input
type="text"
id="authorName"
name="authorName"
required
/>
{actionData?.errors?.authorName && (
<span className="error">{actionData.errors.authorName}</span>
)}
</div>
<div className="field">
<label htmlFor="rating">評価</label>
<select id="rating" name="rating" required>
<option value="">選択してください</option>
<option value="5">⭐⭐⭐⭐⭐ (5)</option>
<option value="4">⭐⭐⭐⭐ (4)</option>
<option value="3">⭐⭐⭐ (3)</option>
<option value="2">⭐⭐ (2)</option>
<option value="1">⭐ (1)</option>
</select>
{actionData?.errors?.rating && (
<span className="error">{actionData.errors.rating}</span>
)}
</div>
<div className="field">
<label htmlFor="comment">コメント</label>
<textarea
id="comment"
name="comment"
rows={4}
placeholder="商品の感想をお聞かせください"
required
/>
{actionData?.errors?.comment && (
<span className="error">{actionData.errors.comment}</span>
)}
</div>
<button type="submit">レビューを投稿</button>
</Form>
</div>
);
}
認証システムの構築
セッションベースの認証システムを実装します。
typescript// app/utils/auth.server.ts
import { createCookieSessionStorage, redirect } from "@remix-run/node";
import bcrypt from "bcryptjs";
// セッションストレージの設定
const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 60 * 60 * 24 * 30, // 30日
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET!],
secure: process.env.NODE_ENV === "production",
},
});
// ユーザー認証
export async function authenticateUser(email: string, password: string) {
const user = await db.user.findUnique({ where: { email } });
if (!user || !await bcrypt.compare(password, user.hashedPassword)) {
return null;
}
return { id: user.id, email: user.email, name: user.name };
}
// セッション作成
export async function createUserSession(userId: string, redirectTo: string) {
const session = await sessionStorage.getSession();
session.set("userId", userId);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session),
},
});
}
// ユーザー情報取得
export async function getUserFromSession(request: Request) {
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
const userId = session.get("userId");
if (!userId) return null;
return await db.user.findUnique({
where: { id: userId },
select: { id: true, email: true, name: true },
});
}
ログインページを実装します。
typescript// app/routes/login.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
import { authenticateUser, createUserSession, getUserFromSession } from "~/utils/auth.server";
// 既にログイン済みの場合はリダイレクト
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUserFromSession(request);
if (user) {
return redirect("/dashboard");
}
return json({});
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
if (!email || !password) {
return json(
{ error: "メールアドレスとパスワードを入力してください" },
{ status: 400 }
);
}
const user = await authenticateUser(email, password);
if (!user) {
return json(
{ error: "メールアドレスまたはパスワードが正しくありません" },
{ status: 400 }
);
}
return createUserSession(user.id, "/dashboard");
}
export default function Login() {
const actionData = useActionData<typeof action>();
return (
<div className="login-page">
<h1>ログイン</h1>
<Form method="post" className="login-form">
<div className="field">
<label htmlFor="email">メールアドレス</label>
<input
type="email"
id="email"
name="email"
required
/>
</div>
<div className="field">
<label htmlFor="password">パスワード</label>
<input
type="password"
id="password"
name="password"
required
/>
</div>
{actionData?.error && (
<div className="error">{actionData.error}</div>
)}
<button type="submit">ログイン</button>
</Form>
</div>
);
}
具体例の実装ポイント:
- Loader と Action の使い分けでデータフローを整理
- React フォームと Web 標準の組み合わせ
- セッション管理とセキュリティの考慮
まとめ
各パターンの使い分け指針
RemixとReactの連携パターンを効果的に活用するために、プロジェクトの特性に応じた使い分けが重要です。
# | パターン | 適用場面 | メリット | 注意点 |
---|---|---|---|---|
1 | Loaderパターン | 初期データ取得、SEO重視 | SSR最適化、高速表示 | サーバー負荷 |
2 | Actionパターン | フォーム処理、CRUD操作 | プログレッシブエンハンスメント | 複雑なバリデーション |
3 | Nested Routing | 複雑な画面構成 | コード分割、保守性 | 学習コスト |
4 | Defer/Suspense | 部分的データ取得 | 体感速度向上 | 状態管理複雑化 |
プロジェクト規模別の選択指針
小規模プロジェクト(〜10ページ)
- 基本的なLoaderとActionパターンを中心に構成
- 認証が必要な場合はセッションベースで実装
- SEO対策はmeta関数で対応
中規模プロジェクト(10〜50ページ)
- Nested Routingを活用した構造化
- DeferとSuspenseを組み合わせた部分的データ読み込み
- エラーハンドリングの充実
大規模プロジェクト(50ページ以上)
- マイクロフロントエンド的なアプローチ
- 高度なキャッシュ戦略
- パフォーマンス監視の導入
開発チーム向けのベストプラクティス
RemixとReactを用いたチーム開発では、以下のプラクティスを推奨いたします。
typescript// チーム開発向けのディレクトリ構造例
app/
├── components/ # 共通コンポーネント
│ ├── ui/ # UIコンポーネント
│ ├── forms/ # フォーム関連
│ └── layout/ # レイアウト関連
├── routes/ # ページルート
│ ├── api/ # API routes
│ ├── admin/ # 管理画面
│ └── auth/ # 認証関連
├── utils/ # ユーティリティ
│ ├── auth.server.ts # 認証処理
│ ├── db.server.ts # DB接続
│ └── validators.ts # バリデーション
├── styles/ # スタイル
└── types/ # 型定義
コーディング規約の例
typescript// Loader関数の命名規則と型定義
export async function loader({ request, params }: LoaderFunctionArgs) {
// 1. パラメータの取得と検証
const { userId } = params;
if (!userId) throw new Response("User ID required", { status: 400 });
// 2. 認証チェック(必要な場合)
const currentUser = await getUserFromSession(request);
// 3. データ取得
const userData = await getUserData(userId);
// 4. 型安全な返却
return json({ userData, currentUser });
}
// Action関数の統一的なエラーハンドリング
export async function action({ request }: ActionFunctionArgs) {
try {
const formData = await request.formData();
// バリデーション
const result = validateFormData(formData);
if (!result.success) {
return json({ errors: result.errors }, { status: 400 });
}
// 処理実行
await executeAction(result.data);
return json({ success: true });
} catch (error) {
console.error("Action error:", error);
return json({ error: "処理中にエラーが発生しました" }, { status: 500 });
}
}
RemixとReactの連携により、従来のSPAの課題を解決しながら、開発効率と保守性を両立できます。適切なパターンの選択と一貫した開発手法により、スケーラブルで高品質なWebアプリケーションを構築していただけるでしょう。
今回ご紹介した連携パターンを参考に、プロジェクトの要件に最適な実装を検討してみてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来