JavaScript Tips | 配列ユーティリティ:ログ出力

JavaScript JavaScript
スポンサーリンク

テーマの整理:「配列ユーティリティ × ログ出力」とは何か

ここで話したいのは、「配列を扱う処理の動きや結果を、きちんとログに残すための小さなユーティリティ」です。 業務コードでは、配列まわりでバグが起きたときに「何が入っていたのか」「どんな順番で処理されたのか」が分からないと、調査が地獄になります。

だからこそ、配列ユーティリティとセットで「ログ出力のユーティリティ」を持っておくと、 「いつ・どこで・何が起きたか」を落ち着いて追えるようになります。

基本コンセプト:配列処理の“前後”と“途中”をログで押さえる

どこをログに残すと効くのか

配列ユーティリティでログを出すとき、意識したいポイントはだいたい次の 3 つです。

処理の「開始」と「終了」。 「入力の配列」と「出力の配列」。 「途中で何をしたか」(フィルタ・マップ・削除など)。

これを毎回手書きするのではなく、 「配列用のログユーティリティ」としてまとめておくと、コードがすっきりします。

ユーティリティ 1:配列の状態をきれいに出す logArray

実装と考え方

まずは、「配列の中身を分かりやすく出す」ための基本ユーティリティです。

function logArray(label, array) {
  if (!Array.isArray(array)) {
    console.log(`[${label}] not an array`, array);
    return;
  }

  console.log(`=== ${label} (length: ${array.length}) ===`);
  array.forEach((item, index) => {
    console.log(`  [${index}]`, item);
  });
}
JavaScript

ここでの重要ポイントをかみ砕きます。

label は「この配列が何なのか」を示す名前(例: “users before filter”)。 配列でなければ、そのことをログに出して終わる。 配列なら、長さと各要素をインデックス付きで出す。

「ただ console.log(array) する」のと違って、 どの配列なのか・何件あるのか・何番目に何が入っているかが一目で分かるようになります。

例題:フィルタ前後の配列をログに出す

const users = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob",   active: false },
  { id: 3, name: "Carol", active: true },
];

logArray("users before filter", users);

const activeUsers = users.filter((u) => u.active);

logArray("users after filter", activeUsers);
JavaScript

これを実行すると、

フィルタ前に「全ユーザー」、フィルタ後に「active なユーザーだけ」が、 インデックス付きできれいに出てくれます。

ユーティリティ 2:配列処理の“開始・終了”をまとめて出す logArrayProcess

実装と考え方

次に、「配列を処理する関数の前後をまとめてログに出す」ユーティリティです。

function logArrayProcess(label, input, output) {
  console.log(`--- ${label} ---`);
  logArray("input", input);
  logArray("output", output);
  console.log(`--- end ${label} ---`);
}
JavaScript

これを使うと、「この処理で配列がどう変わったか」を一気に見られます。

例題:ソート処理の前後をログに出す

function sortByName(users) {
  return users.slice().sort((a, b) => a.name.localeCompare(b.name));
}

const users = [
  { id: 2, name: "Bob" },
  { id: 3, name: "Carol" },
  { id: 1, name: "Alice" },
];

const sorted = sortByName(users);

logArrayProcess("sortByName", users, sorted);
JavaScript

ログを見ると、

input には元の順番の配列。 output には名前順に並び替えられた配列。

が並んで出てくるので、「この処理で何が変わったか」が一目で分かります。

ユーティリティ 3:差分とセットでログを出す logArrayDiff

差分検出とログ出力を組み合わせる

前のメッセージでやった「差分検出」と組み合わせると、 「何が追加・削除・更新されたか」をログで分かりやすく出せます。

function diffById(prev, next) {
  const prevMap = new Map(prev.map((item) => [item.id, item]));
  const nextMap = new Map(next.map((item) => [item.id, item]));

  const added = [];
  const removed = [];
  const updated = [];
  const unchanged = [];

  for (const [id, nextItem] of nextMap.entries()) {
    const prevItem = prevMap.get(id);
    if (!prevItem) {
      added.push(nextItem);
      continue;
    }

    const isSame =
      JSON.stringify(prevItem) === JSON.stringify(nextItem);

    if (isSame) {
      unchanged.push(nextItem);
    } else {
      updated.push({ before: prevItem, after: nextItem });
    }
  }

  for (const [id, prevItem] of prevMap.entries()) {
    if (!nextMap.has(id)) {
      removed.push(prevItem);
    }
  }

  return { added, removed, updated, unchanged };
}

function logArrayDiff(label, prev, next) {
  const diff = diffById(prev, next);

  console.log(`=== diff: ${label} ===`);
  console.log("added:");
  diff.added.forEach((item) => console.log("  +", item));

  console.log("removed:");
  diff.removed.forEach((item) => console.log("  -", item));

  console.log("updated:");
  diff.updated.forEach(({ before, after }) => {
    console.log("  ~ before:", before);
    console.log("    after :", after);
  });

  console.log("unchanged:");
  diff.unchanged.forEach((item) => console.log("  =", item));
}
JavaScript

例題:ユーザー一覧の差分をログに出す

const prevUsers = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob",   active: false },
];

const nextUsers = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob Jr.", active: false },
  { id: 3, name: "Carol", active: true },
];

logArrayDiff("users", prevUsers, nextUsers);
JavaScript

ログを見ると、

added に id=3 のユーザー。 removed はなし。 updated に id=2 の「before / after」。 unchanged に id=1 のユーザー。

というふうに、「何がどう変わったか」が視覚的に分かります。

ユーティリティ 4:非同期配列処理のログ asyncMapWithLog

非同期処理の“並列・順次”とログ

配列ユーティリティで非同期処理をするときも、 「どの要素がいつ処理されたか」をログに残しておくと、 レート制限やエラーの調査がかなり楽になります。

ここでは、シンプルな「非同期 map +ログ」を作ってみます。

async function asyncMapWithLog(label, array, asyncMapper) {
  if (!Array.isArray(array)) {
    console.log(`[${label}] not an array`);
    return [];
  }
  if (typeof asyncMapper !== "function") {
    console.log(`[${label}] mapper is not a function`);
    return array.slice();
  }

  console.log(`=== asyncMap start: ${label} (length: ${array.length}) ===`);

  const promises = array.map(async (item, index) => {
    console.log(`[${label}] start index=${index}`, item);
    const result = await asyncMapper(item, index);
    console.log(`[${label}] end   index=${index}`, result);
    return result;
  });

  const output = await Promise.all(promises);

  console.log(`=== asyncMap end: ${label} ===`);
  return output;
}
JavaScript

例題:ユーザー ID を並列で API 取得しつつログを出す

async function fetchUser(id) {
  await new Promise((r) => setTimeout(r, 300));
  return { id, name: `User-${id}` };
}

async function main() {
  const ids = [1, 2, 3];

  const users = await asyncMapWithLog("fetchUsers", ids, (id) => fetchUser(id));

  console.log("result:", users);
}

main();
JavaScript

ログを見ると、

どの index の処理がいつ start していつ end したか。 全体の開始と終了。

が分かるので、「非同期配列処理の動き」がかなり追いやすくなります。

配列ログユーティリティで意識してほしい重要ポイント

「何をログに残すか」を絞る

ログは多ければいいわけではなく、 「必要なときに、必要な情報だけが見える」ことが大事です。

配列ユーティリティでは、特に次を意識するといいです。

入力と出力の配列。 差分(追加・削除・更新)。 非同期処理の開始・終了・エラー。

これをユーティリティとしてまとめておくと、 「とりあえず全部 console.log」で画面が埋まる、みたいな状態を避けられます。

「ラベル」をちゃんと付ける

どのログがどの処理から出ているか分からないと、 ログがただのノイズになります。

だからこそ、label を毎回ちゃんと付けるのが大事です。

例として、

“users before filter” “users after filter” “fetchUsers asyncMap”

など、「何をしているログなのか」が一目で分かる名前を付けておくと、 後からログを読む自分やチームメイトがかなり楽になります。

手を動かして「配列ログ」の感覚をつかむ

次のコードをまとめて実行して、 「配列の状態」「差分」「非同期処理」のログがどう見えるかを体感してみてください。

function logArray(label, array) {
  if (!Array.isArray(array)) {
    console.log(`[${label}] not an array`, array);
    return;
  }

  console.log(`=== ${label} (length: ${array.length}) ===`);
  array.forEach((item, index) => {
    console.log(`  [${index}]`, item);
  });
}

function diffById(prev, next) {
  const prevMap = new Map(prev.map((item) => [item.id, item]));
  const nextMap = new Map(next.map((item) => [item.id, item]));

  const added = [];
  const removed = [];
  const updated = [];
  const unchanged = [];

  for (const [id, nextItem] of nextMap.entries()) {
    const prevItem = prevMap.get(id);
    if (!prevItem) {
      added.push(nextItem);
      continue;
    }

    const isSame =
      JSON.stringify(prevItem) === JSON.stringify(nextItem);

    if (isSame) {
      unchanged.push(nextItem);
    } else {
      updated.push({ before: prevItem, after: nextItem });
    }
  }

  for (const [id, prevItem] of prevMap.entries()) {
    if (!nextMap.has(id)) {
      removed.push(prevItem);
    }
  }

  return { added, removed, updated, unchanged };
}

function logArrayDiff(label, prev, next) {
  const diff = diffById(prev, next);

  console.log(`=== diff: ${label} ===`);
  console.log("added:");
  diff.added.forEach((item) => console.log("  +", item));

  console.log("removed:");
  diff.removed.forEach((item) => console.log("  -", item));

  console.log("updated:");
  diff.updated.forEach(({ before, after }) => {
    console.log("  ~ before:", before);
    console.log("    after :", after);
  });

  console.log("unchanged:");
  diff.unchanged.forEach((item) => console.log("  =", item));
}

async function asyncMapWithLog(label, array, asyncMapper) {
  if (!Array.isArray(array)) {
    console.log(`[${label}] not an array`);
    return [];
  }
  if (typeof asyncMapper !== "function") {
    console.log(`[${label}] mapper is not a function`);
    return array.slice();
  }

  console.log(`=== asyncMap start: ${label} (length: ${array.length}) ===`);

  const promises = array.map(async (item, index) => {
    console.log(`[${label}] start index=${index}`, item);
    const result = await asyncMapper(item, index);
    console.log(`[${label}] end   index=${index}`, result);
    return result;
  });

  const output = await Promise.all(promises);

  console.log(`=== asyncMap end: ${label} ===`);
  return output;
}

async function demo() {
  const prev = [
    { id: 1, name: "Alice", active: true },
    { id: 2, name: "Bob",   active: false },
  ];

  const next = [
    { id: 1, name: "Alice", active: true },
    { id: 2, name: "Bob Jr.", active: false },
    { id: 3, name: "Carol", active: true },
  ];

  logArray("prev users", prev);
  logArray("next users", next);
  logArrayDiff("users", prev, next);

  const ids = [1, 2, 3];
  await asyncMapWithLog("fetchUsers", ids, async (id) => {
    await new Promise((r) => setTimeout(r, 200));
    return { id, name: `User-${id}` };
  });
}

demo();
JavaScript

まとめ:配列ログユーティリティで「配列の動きを見える化する」

配列ユーティリティにログ出力を組み合わせると、 「配列がどう変わっているか」「どんな処理が走っているか」を、ちゃんと“目で見て”確認できるようになります。

プロジェクトに例えば次のような形で置いておくイメージです。

export function logArray(label, array) { ... }
export function logArrayDiff(label, prev, next) { ... }
export async function asyncMapWithLog(label, array, mapper) { ... }
JavaScript

そして、

配列まわりでバグが出やすいところ、 差分や非同期処理が絡むところには、必ずこれらを通してログを残す。

こう決めておくと、 「何が起きているか分からない」という不安が減り、 配列処理のコードを自信を持って触れるようになっていきます。

ログは“うるさいもの”ではなく、“自分の味方”です。 配列ユーティリティと仲良くさせておくと、業務コードの調査と改善がかなり楽になります。

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