JavaScript | 非同期処理:設計・理解の深化 - 同期 API との境界

JavaScript JavaScript
スポンサーリンク

なぜ「同期 API との境界」を意識しないといけないのか

非同期処理を学び始めると、
「どこまでを同期で書いて、どこからを非同期にするのか」
がだんだん分からなくなってきます。

全部 async にしてしまうのも違うし、
逆に何でも同期でやろうとすると UI が固まる。

ここで大事になるのが、
「同期 API と非同期 API の境界を、意識して設計する」
という考え方です。

境界をちゃんと決めておくと、
どの関数はすぐ結果が返るのか
どの関数は await が必要なのか
どこから先は「待ち」が発生する世界なのか

が、コードから読み取りやすくなります。


そもそも「同期 API」と「非同期 API」の違い

同期 API:呼んだらすぐ終わる世界

同期 API は、呼び出した瞬間に処理が始まり、
終わるまで呼び出し元に制御が戻ってきません。

例えば、配列の長さを取るのは完全に同期です。

function getLength(arr) {
  return arr.length;
}

const len = getLength([1, 2, 3]); // すぐに 3 が返る
JavaScript

ここには「待ち時間」という概念はありません。
CPU が計算して、すぐ結果が返ってきます。

非同期 API:呼んだら「あとで結果が返ってくる」世界

非同期 API は、呼んだ瞬間には結果が手に入らず、
Promise を返して「あとで結果を教えるね」というスタイルになります。

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

const userPromise = fetchUser(1);   // ここではまだ結果はない
const user = await userPromise;     // ここで初めて結果が手に入る
JavaScript

ここには「待ち」があります。
ネットワークやディスクなど、時間のかかるものが絡みます。

重要なのは、
「同期か非同期か」は“実装の都合”ではなく、“呼び出し側の体験”に直結する仕様
だということです。


どこから非同期にするか?境界の考え方

「時間がかかる可能性があるところ」から先を非同期にする

基本的な考え方はシンプルです。

メモリ上の値をちょっと計算するだけ
配列をフィルタするだけ
オブジェクトを組み立てるだけ

こういうのは同期で十分です。

一方で、

ネットワーク通信(fetch)
ディスク I/O(IndexedDB など)
タイマー(setTimeout, setInterval)
重い計算(長時間のループなど)

こういう「時間が読めないもの」「長くなる可能性があるもの」は、
非同期の世界に追い出した方が安全です。

つまり、
「この処理は、ユーザーを待たせる可能性があるか?」
という視点で境界を決めていきます。

境界をまたぐ関数は「どちら側の顔」を持つかを決める

例えば、キャッシュとネットワークを組み合わせる関数を考えます。

キャッシュにあればすぐ返したい(同期っぽい)
なければネットワークから取ってきたい(非同期)

このとき、API をどう設計するか。

選択肢は二つあります。

一つ目は、「常に非同期 API として見せる」パターン。

async function getUser(id) {
  const cached = userCache.get(id);
  if (cached) {
    return cached; // ここは実質同期だが、async 関数なので Promise で返る
  }

  const res = await fetch(`/api/users/${id}`);
  const user = await res.json();
  userCache.set(id, user);
  return user;
}
JavaScript

呼び出し側は、常に await します。

const user = await getUser(1);
JavaScript

二つ目は、「同期 API と非同期 API を分ける」パターン。

function getUserFromCache(id) {
  return userCache.get(id) ?? null;
}

async function fetchUserFromServer(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}
JavaScript

呼び出し側は、こう書きます。

let user = getUserFromCache(1);
if (!user) {
  user = await fetchUserFromServer(1);
}
JavaScript

どちらが正解というより、
「この関数は“非同期の顔”で見せるのか、“同期の顔”と“非同期の顔”を分けるのか」
を意識して決めることが大事です。


ラッパーで「境界」をはっきりさせる

同期の世界を包んで非同期 API にする

ときどき、「中身は同期だけど、外からは非同期として扱いたい」
というケースがあります。

例えば、将来ネットワークアクセスに変わるかもしれない処理。

function getConfigSync() {
  return {
    theme: "dark",
    language: "ja",
  };
}
JavaScript

今は同期で十分ですが、
将来サーバーから設定を取ってくるように変わるかもしれません。

そのときに備えて、最初から「非同期 API の顔」を用意しておく、という設計もあります。

async function getConfig() {
  return getConfigSync();
}
JavaScript

呼び出し側は、最初からこう書きます。

const config = await getConfig();
JavaScript

中身が同期か非同期かは、呼び出し側には関係ありません。
境界の外から見たときは、
「設定は await して手に入るもの」
という仕様だけが大事です。

ここが重要です。
「境界の外から見たときの“顔”を先に決めておくと、内部の実装を変えても呼び出し側を壊さずに済む。」

非同期の世界を包んで同期 API に見せるのは基本的に無理

逆に、「非同期処理を同期 API のように見せたい」という欲望も出てきます。

例えば、こういうのはできません。

// これはダメな例
function getUserSync(id) {
  const res = await fetch(`/api/users/${id}`); // ここで await は使えない
  return res.json();
}
JavaScript

JavaScript では、
「本当に時間のかかる処理」を完全に同期 API にすることは基本的にできません。

やろうとすると、
UI をブロックする危険なコードになります。

function blockFor(ms) {
  const start = Date.now();
  while (Date.now() - start < ms) {
    // ひたすら待つ(UI が固まる)
  }
}
JavaScript

だから、
「非同期を同期に“偽装”する」のではなく、
“非同期であることを正直に API に出す”

というのが健全な設計です。


UI から見た「同期と非同期の境界」

UI ロジックは「非同期の世界の手前」で止める

例えば、ボタンを押したらユーザー情報を読み込んで表示する UI を考えます。

何も考えずに書くと、こうなりがちです。

button.addEventListener("click", async () => {
  const res = await fetch("/api/user");
  const user = await res.json();
  header.textContent = `こんにちは、${user.name}さん`;
});
JavaScript

これでも動きますが、
UI と非同期処理がべったりくっついています。

境界を意識すると、こう分けられます。

async function fetchCurrentUser() {
  const res = await fetch("/api/user");
  if (!res.ok) throw new Error("ユーザー取得に失敗しました");
  return res.json();
}

function renderUserHeader(user) {
  header.textContent = `こんにちは、${user.name}さん`;
}

button.addEventListener("click", async () => {
  try {
    const user = await fetchCurrentUser();
    renderUserHeader(user);
  } catch (err) {
    showErrorMessage(err.message);
  }
});
JavaScript

ここでの境界は、

fetchCurrentUser より内側 → 非同期の世界(ネットワーク)
renderUserHeader より外側 → UI の世界(同期)

です。

UI 側から見たとき、
「非同期の世界に入るのはここから」
という線が見えるようにしておくと、
コードの見通しがかなり良くなります。


初心者として「同期 API との境界」で意識してほしいこと

最後に、頭の中に置いておいてほしい問いをまとめます。

この処理は、本当に同期でやっても大丈夫な軽さか?
この関数は、呼び出し側から見て“同期の顔”か“非同期の顔”か、はっきりしているか?
キャッシュとネットワークが混ざるところで、境界の設計をサボっていないか?
UI のコードの中に、非同期の詳細(fetch や長い await)が入り込みすぎていないか?
「将来中身が非同期に変わっても、呼び出し側を壊さない顔つき」になっているか?

おすすめの練習は、
自分のコードの中から一つ関数を選んで、

これは「同期 API」として見せたいのか
それとも「非同期 API」として見せたいのか

を言語化してみることです。

そのうえで、

同期として見せたいなら、中で時間のかかることをしていないか?
非同期として見せたいなら、Promise を返すきれいな形になっているか?

をチェックしてみてください。

その「境界」を意識し始めた瞬間から、
あなたの非同期コードはただ動くだけのものから、
「設計されたコード」 に変わっていきます。

タイトルとURLをコピーしました