T-CREATOR

Motion(旧 Framer Motion)データフェッチ同期設計:ローディング → 結果 → エラーの段階的演出

Motion(旧 Framer Motion)データフェッチ同期設計:ローディング → 結果 → エラーの段階的演出

Web アプリケーション開発において、データ取得中のユーザー体験は非常に重要です。ユーザーは待機中に何が起きているのかを知りたいと思いますし、エラーが発生した際には適切なフィードバックが必要になります。

Motion(旧 Framer Motion)を活用することで、データフェッチの各段階を美しくアニメーション化し、ユーザーに安心感を与える UI を実現できるのです。

本記事では、データフェッチの 3 つの状態(ローディング・結果表示・エラー表示)を段階的に演出する実装方法を、実践的なコード例とともに詳しく解説いたします。

背景

データフェッチにおける状態管理の重要性

現代の Web アプリケーションは、ほとんどの場合 API との通信を伴います。データを取得する際には、通信の遅延や失敗といった不確実性が常に存在し、これらの状態を適切にハンドリングする必要があります。

従来の実装では、単純にローディングフラグを切り替えるだけでしたが、ユーザーはこの状態遷移を視覚的に理解しづらく、突然画面が変わることで不安を感じることがありました。

Motion によるアニメーション駆動開発

Motion(旧 Framer Motion)は、React 向けの宣言的なアニメーションライブラリです。コンポーネントの状態変化に応じて、自動的に滑らかなアニメーションを適用してくれるため、複雑な状態管理とアニメーションの同期が簡単に実現できます。

以下の図は、Motion を使ったデータフェッチの全体的な流れを示しています。

mermaidflowchart TB
    start["コンポーネント<br/>マウント"] -->|データ取得開始| loading["ローディング状態<br/>(アニメーション表示)"]
    loading -->|成功| success["結果表示状態<br/>(フェードイン)"]
    loading -->|失敗| error["エラー状態<br/>(警告表示)"]
    success --> refetch["再取得ボタン"]
    error --> retry["リトライボタン"]
    refetch -->|再実行| loading
    retry -->|再実行| loading

この図からわかるように、データフェッチには明確な状態遷移があり、各状態で適切なアニメーションを適用することで、ユーザー体験を大きく向上させられます。

課題

データフェッチ UI における一般的な課題

データフェッチを実装する際、開発者は以下のような課題に直面します。

状態管理の複雑化

データ取得には loadingsuccesserror という最低 3 つの状態が存在します。さらに、初回読み込みなのか再取得なのか、部分的な更新なのかといった細かい状態も考慮すると、管理すべき状態が急激に増えていきます。

アニメーションタイミングの制御

状態が変わるたびに適切なアニメーションを発火させる必要がありますが、従来の CSS アニメーションでは状態とアニメーションの同期が難しく、コードが複雑になりがちです。

以下の表は、データフェッチにおける課題を整理したものです。

#課題カテゴリ具体的な問題影響範囲
1状態管理複数の状態フラグが散在し、整合性が取りづらい開発効率・バグ発生率
2アニメーションCSS と JavaScript の状態同期が困難ユーザー体験・コード品質
3エラーハンドリングエラー表示のタイミングと演出が不適切ユーザー満足度
4再取得処理リトライ時の状態リセット漏れ機能の信頼性

ユーザー体験における課題

技術的な課題だけでなく、ユーザー体験の観点でも問題があります。

突然コンテンツが表示されると、ユーザーは驚いてしまい、何が起きたのかを理解できません。また、エラーが発生した際に適切なフィードバックがないと、ユーザーは何をすべきかわからず、アプリケーションへの信頼を失ってしまうでしょう。

次の図は、適切なアニメーションがない場合とある場合の違いを示しています。

mermaidflowchart LR
    subgraph bad["❌ アニメーションなし"]
        b1["空白画面"] -.->|瞬時に切替| b2["データ表示"]
        b2 -.->|瞬時に切替| b3["エラー画面"]
    end

    subgraph good["✅ Motion による段階的演出"]
        g1["スピナー<br/>(フェードイン)"] -->|滑らか| g2["データ表示<br/>(スライド)"]
        g1 -->|滑らか| g3["エラー表示<br/>(シェイク)"]
    end

アニメーションなしの場合は状態遷移が唐突で、ユーザーが状況を把握しづらくなります。一方、Motion を活用した段階的演出では、各状態への移行が視覚的にわかりやすく、安心感を与えられるのです。

解決策

Motion の AnimatePresence と variants による状態管理

Motion が提供する AnimatePresencevariants という機能を組み合わせることで、データフェッチの各状態を宣言的に管理し、自動的にアニメーションを適用できます。

AnimatePresence はコンポーネントのマウント・アンマウント時のアニメーションを制御し、variants は各状態に対応するアニメーション定義を一元管理する仕組みです。

基本的な設計思想

解決策の核心は、データフェッチの状態とアニメーションを完全に同期させることにあります。状態が変われば自動的にアニメーションが発火し、開発者は状態管理だけに集中できるようになります。

以下の図は、Motion による状態とアニメーションの同期メカニズムを示しています。

mermaidstateDiagram-v2
    [*] --> Idle: コンポーネント初期化
    Idle --> Loading: fetchData()

    Loading --> Success: データ取得成功
    Loading --> Error: データ取得失敗

    Success --> Loading: 再取得
    Error --> Loading: リトライ

    state Loading {
        [*] --> SpinnerFadeIn
        SpinnerFadeIn --> SpinnerRotate
    }

    state Success {
        [*] --> ContentSlideIn
        ContentSlideIn --> ContentVisible
    }

    state Error {
        [*] --> ErrorShake
        ErrorShake --> ErrorVisible
    }

この状態図からわかるように、各状態には対応するアニメーションがあり、状態遷移と同時にアニメーションも自動的に切り替わります。

型安全な状態管理の実装

TypeScript を活用して、型安全にデータフェッチの状態を管理します。これにより、存在しない状態への遷移や、不正なデータアクセスを防げます。

#実装要素役割Motion の機能
1状態型定義データフェッチの状態を型で表現-
2カスタムフック状態管理ロジックを再利用可能に-
3variants 定義各状態のアニメーション設定variants
4AnimatePresenceマウント・アンマウント制御AnimatePresence
5motion コンポーネントアニメーション適用motion.div など

これらの要素を組み合わせることで、保守性が高く、拡張しやすい実装が実現できます。

具体例

プロジェクトのセットアップ

まず、必要なパッケージをインストールします。Motion は React と組み合わせて使用するため、React プロジェクトが必要です。

bash# プロジェクト作成(Next.js を使用)
yarn create next-app data-fetch-animation --typescript

# プロジェクトディレクトリに移動
cd data-fetch-animation

# Motion をインストール
yarn add motion

Motion のバージョンは、Framer Motion から独立した新しいパッケージです。以前の framer-motion から移行する場合も、基本的な API は同じですので、スムーズに移行できます。

型定義の作成

データフェッチの状態を表現する型を定義します。これにより、コード全体で一貫した型安全性を確保できるのです。

typescript// types/dataFetch.ts

// データフェッチの状態を表す型
type FetchStatus = 'idle' | 'loading' | 'success' | 'error';

// データフェッチの結果を表す型
interface FetchState<T> {
  status: FetchStatus;
  data: T | null;
  error: Error | null;
}

この型定義では、FetchStatus でデータ取得の状態を、FetchState で状態とデータとエラーを一つにまとめています。

次に、API から取得するデータの型を定義します。

typescript// types/dataFetch.ts(続き)

// ユーザー情報の型(例)
interface User {
  id: number;
  name: string;
  email: string;
  avatar: string;
}

// API レスポンスの型
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

これらの型定義により、データの構造が明確になり、開発時の補完やエラー検出が効果的に機能します。

カスタムフックの実装

データフェッチのロジックをカスタムフックとして切り出します。これにより、コンポーネント間で再利用でき、テストも容易になります。

typescript// hooks/useDataFetch.ts

import { useState, useCallback } from 'react';
import type { FetchState } from '@/types/dataFetch';

// データフェッチフックの型定義
interface UseDataFetchReturn<T> {
  state: FetchState<T>;
  fetchData: () => Promise<void>;
  reset: () => void;
}

まず、フックの戻り値の型を定義しました。state で現在の状態を、fetchData でデータ取得を、reset で状態リセットを行います。

次に、フック本体を実装します。

typescript// hooks/useDataFetch.ts(続き)

export function useDataFetch<T>(
  fetchFn: () => Promise<T>
): UseDataFetchReturn<T> {
  // 初期状態の定義
  const initialState: FetchState<T> = {
    status: 'idle',
    data: null,
    error: null,
  };

  const [state, setState] = useState<FetchState<T>>(initialState);

初期状態では、idle(待機中)に設定し、データもエラーも存在しない状態から始めます。

データ取得関数を実装します。

typescript// hooks/useDataFetch.ts(続き)

  // データ取得処理
  const fetchData = useCallback(async () => {
    // ローディング状態に遷移
    setState({
      status: 'loading',
      data: null,
      error: null,
    });

    try {
      // データ取得実行
      const result = await fetchFn();

ローディング状態に遷移した後、渡された fetchFn を実行します。この関数は Promise を返すため、await で結果を待ちます。

成功時とエラー時の処理を追加します。

typescript// hooks/useDataFetch.ts(続き)

      // 成功状態に遷移
      setState({
        status: 'success',
        data: result,
        error: null,
      });
    } catch (err) {
      // エラー状態に遷移
      setState({
        status: 'error',
        data: null,
        error: err instanceof Error ? err : new Error('Unknown error'),
      });
    }
  }, [fetchFn]);

成功時は success 状態にしてデータを保存し、エラー時は error 状態にしてエラー情報を保存します。エラーオブジェクトの型チェックも行い、安全性を確保しています。

最後に、リセット関数とフックの戻り値を定義します。

typescript// hooks/useDataFetch.ts(続き)

  // 状態をリセットする関数
  const reset = useCallback(() => {
    setState(initialState);
  }, []);

  return {
    state,
    fetchData,
    reset,
  };
}

これで、データフェッチのロジックを持つ再利用可能なフックが完成しました。

アニメーション variants の定義

Motion の variants を使って、各状態に対応するアニメーションを定義します。この定義により、状態が変わるだけで自動的にアニメーションが適用されるのです。

typescript// animations/fetchAnimations.ts

import type { Variants } from 'motion/react';

// ローディングスピナーのアニメーション
export const spinnerVariants: Variants = {
  // 初期状態(非表示)
  hidden: {
    opacity: 0,
    scale: 0.8,
  },
  // 表示状態
  visible: {
    opacity: 1,
    scale: 1,
    transition: {
      duration: 0.3,
    },
  },

スピナーは、透明度とスケールを変化させてフェードイン&拡大しながら表示されます。

スピナーの回転アニメーションも追加します。

typescript// animations/fetchAnimations.ts(続き)

  // 回転アニメーション
  rotating: {
    rotate: 360,
    transition: {
      duration: 1,
      repeat: Infinity,
      ease: 'linear',
    },
  },
  // 終了状態(非表示)
  exit: {
    opacity: 0,
    scale: 0.8,
    transition: {
      duration: 0.2,
    },
  },
};

rotating では、360 度回転を無限にループさせています。exit では、スピナーが消える際のアニメーションを定義しました。

次に、コンテンツ表示のアニメーションを定義します。

typescript// animations/fetchAnimations.ts(続き)

// コンテンツ表示のアニメーション
export const contentVariants: Variants = {
  // 初期状態(下から登場)
  hidden: {
    opacity: 0,
    y: 20,
  },
  // 表示状態
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.5,
      ease: 'easeOut',
    },
  },

コンテンツは下から 20px の位置からスライドアップしながらフェードインします。

子要素を段階的に表示するアニメーションも追加します。

typescript// animations/fetchAnimations.ts(続き)

  // 子要素を順番に表示
  staggered: {
    visible: {
      transition: {
        staggerChildren: 0.1,
      },
    },
  },
  // 終了状態
  exit: {
    opacity: 0,
    y: -20,
    transition: {
      duration: 0.3,
    },
  },
};

staggerChildren を使うと、子要素が 0.1 秒ずつずれて順番に表示されます。これにより、リスト表示などで視覚的に美しい演出が実現できるのです。

エラー表示のアニメーションを定義します。

typescript// animations/fetchAnimations.ts(続き)

// エラー表示のアニメーション
export const errorVariants: Variants = {
  // 初期状態
  hidden: {
    opacity: 0,
    scale: 0.9,
  },
  // 表示状態(シェイクアニメーション付き)
  visible: {
    opacity: 1,
    scale: 1,
    x: [0, -10, 10, -10, 10, 0],
    transition: {
      duration: 0.5,
      x: {
        duration: 0.4,
        times: [0, 0.2, 0.4, 0.6, 0.8, 1],
      },
    },
  },

エラー表示では、左右にシェイクするアニメーションを適用しています。x プロパティに配列を指定することで、複数の位置を順番に移動するアニメーションが実現できます。

エラーの終了アニメーションも追加します。

typescript// animations/fetchAnimations.ts(続き)

  // 終了状態
  exit: {
    opacity: 0,
    scale: 0.9,
    transition: {
      duration: 0.2,
    },
  },
};

これで、ローディング、コンテンツ表示、エラー表示のすべてのアニメーション定義が完成しました。

UI コンポーネントの実装

実際に画面に表示する UI コンポーネントを作成します。各状態に応じた表示を Motion のアニメーションと組み合わせて実装していきます。

typescript// components/DataFetchDisplay.tsx

import { motion, AnimatePresence } from 'motion/react';
import { useDataFetch } from '@/hooks/useDataFetch';
import {
  spinnerVariants,
  contentVariants,
  errorVariants,
} from '@/animations/fetchAnimations';
import type { User } from '@/types/dataFetch';

必要なモジュールとコンポーネントをインポートします。AnimatePresence は、コンポーネントのマウント・アンマウント時のアニメーションを制御するために必須です。

コンポーネントの基本構造を定義します。

typescript// components/DataFetchDisplay.tsx(続き)

export function DataFetchDisplay() {
  // データフェッチフックを使用
  const { state, fetchData } = useDataFetch<User[]>(async () => {
    // モックデータを取得(実際はAPI呼び出し)
    const response = await fetch('/api/users');
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json();
  });

useDataFetch フックを使用して、データ取得の状態を管理します。ここでは ​/​api​/​users からユーザー一覧を取得する例を示しています。

次に、レンダリングロジックを実装します。

typescript// components/DataFetchDisplay.tsx(続き)

  return (
    <div className="container">
      {/* データ取得ボタン */}
      <button
        onClick={fetchData}
        disabled={state.status === 'loading'}
        className="fetch-button"
      >
        {state.status === 'loading' ? 'データ取得中...' : 'データを取得'}
      </button>

      {/* 各状態の表示エリア */}
      <div className="display-area">

ボタンは、ローディング中は無効化し、状態に応じてテキストを変更します。

AnimatePresence を使って、各状態のコンポーネントを制御します。

typescript// components/DataFetchDisplay.tsx(続き)

        <AnimatePresence mode="wait">
          {/* ローディング表示 */}
          {state.status === 'loading' && (
            <motion.div
              key="loading"
              variants={spinnerVariants}
              initial="hidden"
              animate={['visible', 'rotating']}
              exit="exit"
              className="loading-container"
            >
              <div className="spinner" />
              <p>データを取得しています...</p>
            </motion.div>
          )}

mode="wait" を指定すると、前のコンポーネントの終了アニメーションが完了してから、次のコンポーネントが表示されます。animate に配列を渡すことで、複数のアニメーションを同時に適用できるのです。

成功時のコンテンツ表示を実装します。

typescript// components/DataFetchDisplay.tsx(続き)

          {/* 成功時のデータ表示 */}
          {state.status === 'success' && state.data && (
            <motion.div
              key="success"
              variants={contentVariants}
              initial="hidden"
              animate="visible"
              exit="exit"
              className="content-container"
            >
              <h2>取得結果</h2>
              <motion.ul
                variants={contentVariants.staggered}
                initial="hidden"
                animate="visible"
              >

ユーザーリストを段階的に表示するために、ul 要素にも variants を適用しています。

リストアイテムをレンダリングします。

typescript// components/DataFetchDisplay.tsx(続き)

                {state.data.map((user) => (
                  <motion.li
                    key={user.id}
                    variants={contentVariants}
                    className="user-item"
                  >
                    <img src={user.avatar} alt={user.name} />
                    <div>
                      <h3>{user.name}</h3>
                      <p>{user.email}</p>
                    </div>
                  </motion.li>
                ))}
              </motion.ul>
            </motion.div>
          )}

各リストアイテムも motion.li にすることで、親の staggerChildren 設定により、順番にアニメーションが適用されます。

エラー表示を実装します。

typescript// components/DataFetchDisplay.tsx(続き)

          {/* エラー表示 */}
          {state.status === 'error' && (
            <motion.div
              key="error"
              variants={errorVariants}
              initial="hidden"
              animate="visible"
              exit="exit"
              className="error-container"
            >
              <div className="error-icon">⚠️</div>
              <h2>エラーが発生しました</h2>
              <p>{state.error?.message || '不明なエラー'}</p>
              <button onClick={fetchData} className="retry-button">
                再試行
              </button>
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
}

エラー時には、エラーメッセージと再試行ボタンを表示します。シェイクアニメーションにより、エラーの発生が視覚的に強調されるのです。

スタイリングの実装

コンポーネントに適用する CSS を定義します。アニメーションは Motion が担当するため、CSS では基本的な見た目だけを整えます。

css/* styles/DataFetchDisplay.module.css */

.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.fetch-button {
  width: 100%;
  padding: 1rem 2rem;
  font-size: 1.1rem;
  background: #0070f3;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.2s;
}

ボタンの基本スタイルを定義しました。transition は Motion のアニメーションではなく、ホバー時の背景色変化に使用します。

ボタンの状態別スタイルを追加します。

css/* styles/DataFetchDisplay.module.css(続き) */

.fetch-button:hover:not(:disabled) {
  background: #0051cc;
}

.fetch-button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.display-area {
  margin-top: 2rem;
  min-height: 400px;
  position: relative;
}

無効化されたボタンは灰色にし、カーソルも変更します。表示エリアには最小高さを設定し、アニメーション中にレイアウトが崩れないようにしました。

ローディング表示のスタイルを定義します。

css/* styles/DataFetchDisplay.module.css(続き) */

.loading-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 3rem;
}

.spinner {
  width: 50px;
  height: 50px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #0070f3;
  border-radius: 50%;
}

スピナーは CSS で円形の枠を作り、Motion の rotate アニメーションで回転させます。

コンテンツとエラー表示のスタイルを追加します。

css/* styles/DataFetchDisplay.module.css(続き) */

.content-container {
  background: white;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.user-item {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem;
  border-bottom: 1px solid #eee;
}

.error-container {
  background: #fff5f5;
  border: 2px solid #fc8181;
  border-radius: 12px;
  padding: 2rem;
  text-align: center;
}

エラーコンテナは赤系の背景色にし、視覚的に問題が発生したことを伝えます。

API モックの実装

開発時に使用する API モックを作成します。実際の API がまだ存在しない場合でも、この モックを使って UI の動作確認ができます。

typescript// pages/api/users.ts

import type { NextApiRequest, NextApiResponse } from 'next';
import type { User, ApiResponse } from '@/types/dataFetch';

// モックデータ
const mockUsers: User[] = [
  {
    id: 1,
    name: '山田太郎',
    email: 'yamada@example.com',
    avatar: '/avatars/1.png',
  },
  {
    id: 2,
    name: '佐藤花子',
    email: 'sato@example.com',
    avatar: '/avatars/2.png',
  },

実際のアプリケーションでは、ここでデータベースから取得したり、外部 API を呼び出したりします。

API ハンドラーを実装します。

typescript// pages/api/users.ts(続き)

  {
    id: 3,
    name: '鈴木一郎',
    email: 'suzuki@example.com',
    avatar: '/avatars/3.png',
  },
];

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<ApiResponse<User[]>>
) {
  // 意図的に遅延を追加(ローディング状態を確認するため)
  await new Promise((resolve) => setTimeout(resolve, 1500));

1.5 秒の遅延を追加することで、ローディングアニメーションを確認できます。実際の API でも、ネットワーク遅延は常に発生するため、この遅延は現実的です。

ランダムにエラーを発生させる処理を追加します。

typescript// pages/api/users.ts(続き)

  // ランダムにエラーを発生させる(30%の確率)
  if (Math.random() < 0.3) {
    return res.status(500).json({
      success: false,
      data: [],
      message: 'Error 500: Internal Server Error - サーバーで問題が発生しました',
    });
  }

  // 正常なレスポンスを返す
  res.status(200).json({
    success: true,
    data: mockUsers,
  });
}

30% の確率でエラーを返すことで、エラーハンドリングとエラーアニメーションの動作確認ができます。エラーメッセージには必ずエラーコード(Error 500)を含めることで、検索性を高めています。

応用例:段階的データ読み込み

大量のデータを扱う場合、一度にすべてを表示するのではなく、段階的に読み込むパターンも実装できます。

typescript// hooks/useIncrementalFetch.ts

import { useState, useCallback } from 'react';

interface IncrementalFetchState<T> {
  items: T[];
  hasMore: boolean;
  isLoading: boolean;
  error: Error | null;
}

export function useIncrementalFetch<T>(
  fetchFn: (page: number) => Promise<T[]>,
  pageSize: number = 10
) {
  const [page, setPage] = useState(0);
  const [state, setState] = useState<IncrementalFetchState<T>>({
    items: [],
    hasMore: true,
    isLoading: false,
    error: null,
  });

ページネーションの情報を保持し、段階的にデータを追加していきます。

次のページを読み込む関数を実装します。

typescript// hooks/useIncrementalFetch.ts(続き)

  const loadMore = useCallback(async () => {
    if (state.isLoading || !state.hasMore) return;

    setState((prev) => ({ ...prev, isLoading: true }));

    try {
      const newItems = await fetchFn(page);

      setState((prev) => ({
        items: [...prev.items, ...newItems],
        hasMore: newItems.length === pageSize,
        isLoading: false,
        error: null,
      }));

      setPage((p) => p + 1);
    } catch (err) {
      setState((prev) => ({
        ...prev,
        isLoading: false,
        error: err instanceof Error ? err : new Error('Unknown error'),
      }));
    }
  }, [page, state.isLoading, state.hasMore, fetchFn, pageSize]);

  return { ...state, loadMore };
}

既存のアイテムに新しいアイテムを追加し、取得したアイテム数がページサイズ未満の場合は「これ以上データがない」と判断します。

この段階的読み込みも、Motion のアニメーションと組み合わせることで、スムーズな UX を実現できるのです。

まとめ

Motion(旧 Framer Motion)を活用したデータフェッチの段階的演出について、実践的な実装方法を解説してきました。

データ取得の 3 つの状態(ローディング・結果表示・エラー表示)を適切にアニメーション化することで、ユーザーは現在何が起きているのかを直感的に理解でき、安心してアプリケーションを使用できます。

特に重要なポイントは以下の通りです。

型安全な状態管理により、バグを未然に防ぎ、開発効率を向上させられます。TypeScript の型システムを最大限活用することで、存在しない状態への遷移や不正なデータアクセスを防げるのです。

variants による宣言的なアニメーション定義により、状態とアニメーションの同期が自動的に行われ、コードが大幅にシンプルになります。CSS でアニメーションを管理する従来の方法と比べて、保守性が格段に向上しました。

AnimatePresence によるマウント・アンマウント制御により、コンポーネントの表示・非表示時にも滑らかなアニメーションを適用でき、ユーザー体験が向上します。

これらの技術を組み合わせることで、プロフェッショナルな品質のデータフェッチ UI を、少ないコード量で実現できるのです。ぜひ、皆さんのプロジェクトでも Motion を活用して、ユーザーに喜ばれる UI を作ってみてください。

段階的な演出により、データ取得という不確実性の高い処理を、安心感のある体験に変えられることを実感していただけるでしょう。

関連リンク