Use Cases (更新: 2026/6/6)

Tauriアプリが10MBで済む理由とRust権限設計:Claude Codeで作る実録

Electronなら150MBのアプリがTauriだと10MB。なぜ軽いのか、Rust側のパス制限はどう書くのか、Claude Codeに任せた実体験で解説します。

Tauriアプリが10MBで済む理由とRust権限設計:Claude Codeで作る実録

「同じメモアプリ、Electronで作ったら150MB、Tauriで作り直したら10MBだった」

これ、僕が実際に体験した数字です。中身はほぼ同じReactの画面。なのにインストーラーのサイズが15分の1。常駐メモリも300MB近かったのが40MBくらいに落ちました。社内に配るとき「重い」と言われなくなったのが、何より効きました。

ただ、軽さに釣られてTauriに飛びつくと痛い目を見ます。Tauriは「軽いから安全」ではありません。OSに近い処理をRustで書く以上、Rust側にうっかり穴を開けると、Electronより深い事故を起こせる。僕は最初、Claude Codeに任せたコマンドがアプリのデータフォルダの外まで読み書きできる状態になっていて、これに気づいたとき血の気が引きました。

この記事は、Tauri v2で小さなメモアプリを作りながら、なぜ軽いのか・Rust側のパス制限をどう書くのか・Claude Codeにどこまで任せるかを、僕の失敗込みで残したものです。コードは全部コピペで動く実物を載せます。

この記事の要点

  • Tauriが軽い理由は、Chromiumを同梱せずOS標準のWebViewを借りるから。Windowsは Edge WebView2、macOS/Linux は WebKit を使うので、ランタイムを丸ごと配らなくて済む。
  • 体感差は大きい。Tauriは2〜10MB・RAM 30〜50MB、Electronは80〜200MB・RAM 120MB超が目安。ただし起動環境にWebViewが要る点はトレードオフ。
  • 安全の急所は capability ではなく自作Rustコマンド..・絶対パス・親フォルダ参照をRust側で弾かないと、fs権限を狭めても意味がない。
  • Claude Codeへは「Tauriアプリ完成させて」ではなく、React UI / Rustコマンド / capability / ビルドの4層に分けて1つずつ依頼する。差分が追えるから危ない権限追加に気づける。
  • 作る順番は Rustのパス制限 → invokeの型合わせ → capability確認。UIから作ると保存先や権限が後付けになって差分が膨らむ。

Tauriが軽いのはなぜか(Electronとの一番の違い)

まず、ここを腹落ちさせると残り全部が早くなります。

Electronは、アプリの中にChromiumブラウザを丸ごと一緒に配ります。だからどんなに小さなアプリでも、自分のコードを1行書く前に80〜120MBの土台が乗ってきます。動きが安定するのはこの「自前ブラウザ」のおかげですが、その代償がサイズと常駐メモリです。

Tauriは逆です。OSにすでに入っているWebViewを借ります。Windowsなら Microsoft Edge WebView2、macOS と Linux なら WebKit。ブラウザエンジンを自分で持たないので、配布物は自分のRust製バイナリとフロントの成果物だけ。これが「同じ画面なのに15分の1」のからくりです。

数字で並べるとこうなります(プロジェクトや圧縮で前後しますが、桁の感覚として)。

項目Tauri v2Electron
配布サイズの目安約2〜10MB約80〜200MB
常駐メモリの目安約30〜50MB約120〜300MB
画面の描画OS標準WebView同梱Chromium
OSに近い処理を書く言語RustNode.js(JavaScript)
WebViewのバージョンOS依存(差分が出る)同梱版で固定

最後の行が、軽さの裏側にあるトレードオフです。Electronは同梱Chromiumなので、どのPCでも同じエンジンで動きます。TauriはOSのWebView任せなので、Windowsの古い環境ではWebView2が入っていない、Linuxではディストリごとにエンジンの挙動が違う、といった差分が出ます。僕は「軽い=正義」だと思って雑に選んで、配布先のWindowsでWebView2未導入のマシンに当たり、起動しない報告をもらったことがあります。

なので選び方はシンプルです。ローカルファイルを安全に触りたい・配布物を軽くしたいならTauri。どのPCでも描画を完全に揃えたい・チームがJS一本でやりたいならElectronも十分アリ、と僕は割り切っています。Electron側の事故の防ぎ方はClaude CodeでElectronアプリを安全に作るに別途まとめました。

Tauriを選ぶ場面と、選ばない場面

軽さの話をすると「全部Tauriでいいのでは」と思いがちですが、そうでもありません。

Tauriが向くのは、Web UIの開発体験は欲しいけれど、配布物はローカルアプリにしたい場面です。具体的には、顧客データをブラウザにアップロードしたくない社内ツール、手元のファイルを読んで変換するツール、ネットワークが弱い現場で使う入力アプリ。このあたりは「ローカルで完結する安心感」がそのまま価値になります。

逆に、ログイン中心で常にサーバーAPIに繋ぎっぱなし、OS機能はほぼ使わない、というアプリなら普通のWebアプリやPWAで十分なことが多いです。Tauriを選んだ瞬間、署名・インストーラー・自動更新・OS差分・Rustのビルド環境まで全部面倒を見ることになります。「軽いから」で選ぶと、この運用コストで後悔します。「ローカル機能を安全に持ちたいから選ぶ」と考えると外しません。

Claude Codeに最初に頼むときは、いきなり実装させず境界を切ります。

Tauri v2で小さなメモアプリを作ります。
まず実装せず、React UI / Rustコマンド / capability / ビルドの4層に分けた作業計画だけ出してください。
ファイルシステムはアプリのデータフォルダ配下だけに限定し、任意パスの読み書きは提案しないでください。

この一文があるだけで、Claude Codeがいきなり広いfs権限や任意パスの読み書きを入れてくるリスクをかなり下げられます。

開発に必要な前提を先に揃える

Tauriのつまずきの半分は、コードではなく環境です。Rustのビルドが絡むので、最初に土台を確認しておくと後が楽になります。Tauri公式のPrerequisitesが一次情報です。

  • Rust ツールチェーン: Windowsでは MSVC 版(x86_64-pc-windows-msvc)が標準。rustup で入れます。
  • C++ ビルドツール: Windowsは Visual Studio の「C++ によるデスクトップ開発」が必要。
  • WebView: Windowsは Microsoft Edge WebView2、Linuxは libwebkit2gtk-4.1-dev などディストリ別パッケージ、macOSは標準のWebKit。
  • Node.js: LTS版(20系以上)を推奨。Viteは新しめのNodeを要求します。

ここで一番ハマるのがWebViewです。開発機には大抵入っているので気づきにくいのですが、配布先がそうとは限りません。僕の起動しない事件は、まさにこれが原因でした。

# まず手元の前提を確認(古いとTauriのエラーに化けやすい)
rustc --version
node --version

全体像:JavaScriptはOSを直接触らない

Tauriのセキュリティモデルは、Electronとはっきり違います。Electronは preload と IPC で境界を切りますが、TauriはフロントのJavaScriptがOSを直接触らず、Rust側に定義したコマンドを invoke で呼ぶ形が基本です。ここが安全設計の中心になります。

flowchart LR
  UI["React or Svelte UI"] --> Invoke["invoke from @tauri-apps/api/core"]
  Invoke --> Command["Rust command"]
  Command --> Guard["path allowlist and validation"]
  Guard --> AppData["app data directory"]
  Command --> Result["typed result"]
  Result --> UI
  Capability["Tauri capability"] --> UI

ここで誤解しやすいのが、「capabilityを設定すれば自動で安全になる」わけではない点です。capabilityが制限するのは、フロントから使える「Tauri公式APIやプラグインの権限」だけ。自分で書いたRustコマンドは、アプリのプロセス権限でそのまま動きます。つまりRust側に「どのフォルダだけ許すか」「..で外に出られないか」「書き込み前に親フォルダを確かめるか」を自分で実装しないと、capabilityをいくら絞っても穴は残ります。僕が冒頭でやらかしたのは、まさにこの「Rust側の検証が空っぽ」状態でした。

ReactまたはSvelteで始める

新規なら、公式の create-tauri-app が一番安全です。ReactでもSvelteでも、TypeScriptを選んでおくと invoke の戻り値やUI状態がレビューしやすくなります。

npm create tauri-app@latest taskdesk
cd taskdesk
npm install
npm run tauri dev

途中の質問で ReactSvelte、言語は TypeScript を選びます。既存のViteアプリに後付けする場合は、Vite側を作ってからTauri CLIで初期化します。Viteは新しめのNode(20系以上)を要求するので、Nodeが古いままTauriのエラーに見えるケースに注意してください。

npm create vite@latest taskdesk -- --template react-ts
cd taskdesk
npm install
npm install -D @tauri-apps/cli@latest
npm install @tauri-apps/api@latest
npx tauri init
npx tauri dev

Svelteで始めるならテンプレートだけ変えます。

npm create vite@latest taskdesk -- --template svelte-ts

Claude Codeには、セットアップ直後に「依存を足す」より先に、生成された src-tauripackage.jsontauri.conf.json を読ませます。TauriはテンプレートやCLIの更新でファイル構成が変わるので、古い記事の固定構成を前提にさせないことが大事です。

tauri.conf.json は小さく保つ

tauri.conf.json は、フロントの開発URL、ビルド成果物、ウィンドウ、capability参照をつなぐ設定です。以下はVite前提の最小例。実プロジェクトでは丸ごと上書きせず、差分として確認してください。

{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "TaskDesk",
  "version": "0.1.0",
  "identifier": "com.example.taskdesk",
  "build": {
    "beforeDevCommand": "npm run dev",
    "devUrl": "http://localhost:5173",
    "beforeBuildCommand": "npm run build",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [
      {
        "title": "TaskDesk",
        "width": 1000,
        "height": 700
      }
    ],
    "security": {
      "capabilities": ["main-capability"]
    }
  },
  "bundle": {
    "active": true,
    "targets": "all"
  }
}

Claude Codeに設定を触らせるときは、最低この3点をレビューします。identifier を仮のままにしない、devUrl とViteのポートを合わせる、frontendDistdist を指しているか確認する。地味ですが、ここがズレると「devは動くのにbuildで白画面」になりがちです。

Rustコマンドを作る(ここが安全の本丸)

メモの読み書きと一覧をRustコマンドにします。任意パスは受け取らず、アプリのデータフォルダ配下だけを許可します。..(親フォルダ参照)、絶対パス、Windowsのドライブ指定を拒否し、既存ファイルは canonicalize で実体パスを確認してフォルダの外に出ていないか検査します。

// src-tauri/src/note_commands.rs
use serde::Serialize;
use std::{
    fs,
    path::{Component, Path, PathBuf},
};
use tauri::{AppHandle, Manager};

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoteFile {
    name: String,
    path: String,
    bytes: u64,
    is_dir: bool,
}

// 門番1: 相対パスの中身を検査。`..`や絶対パスが混じったら即拒否する
fn reject_unsafe_relative(path: &Path) -> Result<(), String> {
    for component in path.components() {
        match component {
            Component::Normal(_) | Component::CurDir => {}
            _ => return Err("アプリデータ内の相対パスだけ使えます".to_string()),
        }
    }
    Ok(())
}

// アプリのデータフォルダを取得し、実体パスに正規化しておく
fn app_data_root(app: &AppHandle) -> Result<PathBuf, String> {
    let root = app
        .path()
        .app_data_dir()
        .map_err(|error| format!("app data dirの取得に失敗: {error}"))?;
    fs::create_dir_all(&root).map_err(|error| format!("app data dirの作成に失敗: {error}"))?;
    root.canonicalize()
        .map_err(|error| format!("app data dirの解決に失敗: {error}"))
}

// 読み込み用: 既存ファイルを正規化し、データフォルダの外なら弾く
fn existing_path(app: &AppHandle, relative: &str) -> Result<PathBuf, String> {
    let root = app_data_root(app)?;
    let requested = Path::new(relative);
    reject_unsafe_relative(requested)?;
    let full = root
        .join(requested)
        .canonicalize()
        .map_err(|error| format!("パスが存在しません: {error}"))?;

    // 門番2: 正規化後もrootの内側に居るかを最終確認(シンボリックリンク対策)
    if !full.starts_with(&root) {
        return Err("アプリデータの外には出られません".to_string());
    }

    Ok(full)
}

// 書き込み用: 新規ファイルは正規化できないので、親フォルダで検査する
fn writable_path(app: &AppHandle, relative: &str) -> Result<PathBuf, String> {
    let root = app_data_root(app)?;
    let requested = Path::new(relative);
    reject_unsafe_relative(requested)?;
    let full = root.join(requested);
    let parent = full.parent().ok_or("親フォルダがありません")?;
    fs::create_dir_all(parent).map_err(|error| format!("親フォルダの作成に失敗: {error}"))?;
    let parent = parent
        .canonicalize()
        .map_err(|error| format!("親フォルダの解決に失敗: {error}"))?;

    if !parent.starts_with(&root) {
        return Err("アプリデータの外には出られません".to_string());
    }

    Ok(full)
}

#[tauri::command]
pub fn read_note(app: AppHandle, path: String) -> Result<String, String> {
    let safe_path = existing_path(&app, &path)?;
    fs::read_to_string(safe_path).map_err(|error| format!("ノートの読み込みに失敗: {error}"))
}

#[tauri::command]
pub fn write_note(app: AppHandle, path: String, content: String) -> Result<(), String> {
    let safe_path = writable_path(&app, &path)?;
    fs::write(safe_path, content).map_err(|error| format!("ノートの書き込みに失敗: {error}"))
}

#[tauri::command]
pub fn list_notes(app: AppHandle, dir: String) -> Result<Vec<NoteFile>, String> {
    let safe_dir = existing_path(&app, &dir)?;
    let mut files = Vec::new();

    for entry in fs::read_dir(safe_dir).map_err(|error| format!("フォルダの読み込みに失敗: {error}"))? {
        let entry = entry.map_err(|error| format!("エントリの読み込みに失敗: {error}"))?;
        let metadata = entry
            .metadata()
            .map_err(|error| format!("メタデータの読み込みに失敗: {error}"))?;
        let name = entry.file_name().to_string_lossy().to_string();

        files.push(NoteFile {
            name: name.clone(),
            path: name,
            bytes: metadata.len(),
            is_dir: metadata.is_dir(),
        });
    }

    Ok(files)
}

別モジュールに置いたコマンドは lib.rs で登録します。Tauri公式のCalling Rust from the Frontendでも、別モジュールのコマンドは pub にして generate_handler! に渡す流れが示されています。

// src-tauri/src/lib.rs
mod note_commands;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            note_commands::read_note,
            note_commands::write_note,
            note_commands::list_notes
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Rustに不慣れで、ここのエラーで止まる人はClaude CodeでRust開発入門を先に読むと、Result やモジュール分割の感覚がつかめます。

フロントエンドから呼び出す

フロントでは @tauri-apps/api/coreinvoke を使います。Rust側の #[serde(rename_all = "camelCase")] に合わせて、TypeScriptの型も isDir にしておくと食い違いません。

// src/lib/notesApi.ts
import { invoke } from "@tauri-apps/api/core";

export type NoteFile = {
  name: string;
  path: string;
  bytes: number;
  isDir: boolean;
};

export const notesApi = {
  read(path: string) {
    return invoke<string>("read_note", { path });
  },
  write(path: string, content: string) {
    return invoke<void>("write_note", { path, content });
  },
  list(dir = ".") {
    return invoke<NoteFile[]>("list_notes", { dir });
  },
};

Reactなら、まずは保存と読み込みだけの小さな画面で十分です。

// src/App.tsx
import { useState } from "react";
import { notesApi } from "./lib/notesApi";

export default function App() {
  const [content, setContent] = useState("");
  const [message, setMessage] = useState("Ready");
  const fileName = "daily-note.txt";

  async function loadNote() {
    try {
      setContent(await notesApi.read(fileName));
      setMessage("Loaded");
    } catch (error) {
      setMessage(String(error));
    }
  }

  async function saveNote() {
    try {
      await notesApi.write(fileName, content);
      setMessage("Saved");
    } catch (error) {
      setMessage(String(error));
    }
  }

  return (
    <main>
      <h1>TaskDesk</h1>
      <textarea value={content} onChange={(event) => setContent(event.target.value)} />
      <button onClick={loadNote}>Load</button>
      <button onClick={saveNote}>Save</button>
      <p>{message}</p>
    </main>
  );
}

Svelteでも同じAPIをそのまま呼べます。

<!-- src/App.svelte -->
<script lang="ts">
  import { notesApi } from "./lib/notesApi";

  let content = "";
  let message = "Ready";
  const fileName = "daily-note.txt";

  async function loadNote() {
    try {
      content = await notesApi.read(fileName);
      message = "Loaded";
    } catch (error) {
      message = String(error);
    }
  }

  async function saveNote() {
    try {
      await notesApi.write(fileName, content);
      message = "Saved";
    } catch (error) {
      message = String(error);
    }
  }
</script>

<main>
  <h1>TaskDesk</h1>
  <textarea bind:value={content}></textarea>
  <button on:click={loadNote}>Load</button>
  <button on:click={saveNote}>Save</button>
  <p>{message}</p>
</main>

capability(権限)の落とし穴

Tauri v2のcapabilityは、どのウィンドウやWebViewにどの権限を与えるかを定義します。プラグインのFile System APIをフロントから直接使うなら、src-tauri/capabilities 配下で範囲を明示します。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Main window permissions for TaskDesk.",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:default",
    "fs:allow-app-read-recursive",
    "fs:allow-app-write-recursive"
  ]
}

ここで大事なのは、この記事のサンプルはRustコマンドでファイル操作をしている点です。だから上の fs 権限は「フロントがFile System pluginを直接使う場合」の例として載せています。Rustコマンドだけで読み書きするなら、capabilityに fs:scope-document-recursive のような広い権限をわざわざ足す必要はありません。公式のCapabilitiesFile System pluginを見て、本当に要る権限だけに絞ってください。

Claude Codeに権限を足させるときは、必ず監査プロンプトを挟みます。

変更せずにレビューだけしてください。
対象は src-tauri/capabilities 配下と src-tauri/src/note_commands.rs です。
1. フロントから呼べるTauri APIとRustコマンドを列挙する
2. 各APIが触れるファイルパスの範囲を説明する
3. アプリデータ外・絶対パス・親フォルダ参照・広すぎるwildcardを重大度つきで指摘する
4. 権限を減らせる具体案を出す

これはClaude Codeを実装者ではなく監査役として使うためのプロンプトです。自分で書かせた直後に同じセッションでレビューさせると判定が甘くなりやすいので、できれば別セッションを開いて差分だけ読ませます。

ビルドとテスト

確認コマンドは、フロント・Rust・Tauriの3つに分けます。

npm run build
cd src-tauri
cargo test
cd ..
npm run tauri build

npm run build はViteの本番ビルド、cargo test はRust側のテスト、npm run tauri build はOS向けの配布物を作る段階です。最後のビルドは時間がかかるので、毎回いきなり通すより、フロントとRustが通ってから実行する方が原因を切り分けやすいです。

パス検証のテストは、AppHandle が絡まない純粋関数 reject_unsafe_relative を狙うのが現実的です。危険な入力を弾く小さい関数から固めます。

#[cfg(test)]
mod tests {
    use super::reject_unsafe_relative;
    use std::path::Path;

    #[test]
    fn rejects_parent_directory() {
        assert!(reject_unsafe_relative(Path::new("../secret.txt")).is_err());
    }

    #[test]
    fn accepts_simple_relative_path() {
        assert!(reject_unsafe_relative(Path::new("notes/today.txt")).is_ok());
    }
}

Claude Codeへの修正依頼も、ビルド全体ではなく落ちている層を名指しします。範囲を絞るほど余計なファイルを触られません。

`cargo test`だけが落ちています。
src-tauri/src/note_commands.rs のパス検証テストを読んで、最小差分で直してください。
capability・React UI・tauri.conf.json は変更しないでください。
修正後に実行すべき確認コマンドも最後に列挙してください。

僕がTauriでやらかした失敗3つ

正直に書きます。最初のTauriアプリは、軽さに浮かれて中身が雑でした。

ひとつ目は、Rust側の検証を空っぽにしたこと。capabilityさえ設定すれば安全だと勘違いして、read_note に渡されたパスをそのまま fs::read_to_string に流していました。試しに ../../ を投げたら、普通にアプリ外のファイルが読めた。reject_unsafe_relativestarts_with を足すまで、ここはずっと穴でした。

ふたつ目は、配布先のWebViewを確認しなかったこと。開発機では完璧に動くのに、社内の古いWindowsで起動しない。原因はEdge WebView2が未導入だっただけでした。Tauriは「OSのWebViewを借りる」設計なので、借りる相手が居ないと動きません。これはElectronにはない、Tauri特有のつまずきです。

みっつ目は、Claude Codeに「Tauriアプリ完成させて」と丸投げしたこと。UI・権限・保存形式・更新・配布が一度に変わって、差分が追えずレビュー不能になりました。「コマンド1つ」「capability 1つ」「テスト1つ」に割って依頼してからは、危ない権限追加にもすぐ気づけるようになりました。

よくある質問

Q. TauriとElectron、どっちを選べばいい? A. 配布物を軽くしたい・ローカルファイルを安全に扱いたいならTauri。どのPCでも描画を完全に揃えたい・チームがJS一本でやりたいならElectronも有力です。軽さだけで決めず、WebViewのOS差分を許容できるかで判断してください。

Q. Rustが書けなくてもTauriは使える? A. テンプレートのままなら最小限のRustで動きます。ただしファイル操作などOSに近い処理を入れるなら、Rust側の検証は避けて通れません。Claude Codeに書かせて、自分はレビューに回るのが現実的です。

Q. capabilityを設定すればRustコマンドも安全になる? A. なりません。capabilityが制限するのはフロントから使うTauri APIやプラグイン権限だけです。自作Rustコマンドはアプリ権限でそのまま動くので、パス検証はRust側に自分で書く必要があります。

Q. 「軽い」以外にTauriを選ぶ理由は? A. ローカル処理をRustで書ける点です。大きなファイルの処理やパス制限、ネイティブに近い速度が要る部分を、画面と分けて型安全に実装できます。逆にサーバー通信が中心ならメリットは薄いです。

Q. 配布先で起動しないときは何を疑う? A. まずWebViewです。Windowsなら Edge WebView2 の有無、LinuxならディストリのWebKitパッケージを確認します。次に署名やアプリIDなど、開発中だけ動く設定を本番に持ち込んでいないかを見ます。

実際に試した結果

同じメモアプリをElectronとTauriの両方で作って一番効いたのは、サイズより**「先にRust側の境界を決める」順番**でした。Rustのパス制限を作り、次にTypeScriptの invoke 型を合わせ、最後にcapabilityを確認する。この順だと差分が小さく、Claude Codeのレビューも刺さります。逆にUIから作ると、保存先や権限が後付けになって差分が膨らみ、危ない権限追加を見逃しかけました。

軽さは確かに魅力です。150MBが10MBになって、社内配布で「重い」と言われなくなりました。でもTauriの本当の価値は、OSに近い処理をRustで安全に切り出せることだと今は思っています。軽いから選ぶのではなく、ローカル機能を安全に持ちたいから選ぶ。これがTauriで失敗しないコツです。

実装意図を持って読んでいる人ほど、次の一歩は「自分のローカルツールを作る」だと思います。レビュー観点を固定したいなら教材一覧のプロンプトテンプレートを、チームで権限や CLAUDE.md、確認コマンドを整えたいならClaude Code研修・相談をのぞいてみてください。

#Claude Code #Tauri #Rust #デスクトップアプリ #セキュリティ
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。