ADVERTISEMENT

【Googleスプレッドシート】Apps Scriptでasync・awaitを使う!非同期処理の書き方

【Googleスプレッドシート】Apps Scriptでasync・awaitを使う!非同期処理の書き方
🛡️ 超解決

GoogleスプレッドシートのApps Scriptで非同期処理を書く際に、コールバックやPromiseのチェーンでコードが複雑になり、困ったことはありませんか。特に、外部APIの呼び出しやファイルの読み込みなど時間のかかる処理を順番に実行したい場合、見通しの悪いコードになりがちです。この記事では、ES2017で導入されたasync/awaitを使って、同期処理のように読みやすい非同期コードを書く方法を解説します。具体的な書き方やよくある注意点まで、実際に動作するサンプルコードを交えて説明しますので、ぜひ最後までご覧ください。

【要点】Apps Scriptでasync/awaitを使うために知っておくべき3つのポイント

  • async functionの宣言: 非同期処理を含む関数の先頭にasyncを付けることで、関数内でawaitキーワードが使用可能になります。
  • awaitでPromiseの完了を待機: Promiseを返す関数の前にawaitを置くことで、そのPromiseが解決されるまで後続の処理を一時停止し、結果を同期処理のように受け取れます。
  • エラー処理はtry…catchで行う: async関数内で発生したエラーは、try…catchブロックで同期処理と同じようにキャッチできます。

ADVERTISEMENT

async/awaitが解決する非同期処理の課題

Apps Scriptでは、UrlFetchApp.fetch()やSpreadsheetApp.openById()など、時間のかかる処理を実行する関数が多く存在します。これらの関数はデフォルトで同期実行されますが、実際には内部で非同期的な動作を行うものもあります。特に、外部APIへのリクエストや大量のセル操作を含む処理では、スクリプトの実行時間制限(6分)に引っかかるリスクがあります。従来の非同期処理はPromiseと.then()のチェーンで書かれていましたが、複数の非同期処理を順番に実行したり、並列で実行して結果を待つようなコードは、ネストが深くなりがちでした。async/awaitを使うと、これらの非同期処理をあたかも同期処理のように上から下へと読み下せるコードで記述できるため、可読性と保守性が大幅に向上します。

ただし、Apps ScriptのランタイムはV8ではなくRhinoベースのため、ES2017のasync/awaitをそのまま使えるのか疑問に思う方もいるでしょう。実は、Apps Scriptは2020年以降のアップデートでV8ランタイムがデフォルトになり、async/awaitが使用可能になりました。スクリプトエディタの「実行」メニューから「V8対応にする」設定を確認してください。本記事では、V8ランタイムを前提としたasync/awaitの書き方を解説します。

async/awaitの基本的な書き方と実装手順

ここでは、実際にGoogleスプレッドシートのデータを非同期で取得・加工するサンプルを交えて、async/awaitの書き方をステップごとに説明します。まずは、単一の非同期処理を待つ基本的なパターンから始めましょう。

  1. 関数の先頭にasyncを付ける
    非同期処理を含む関数を定義するときは、functionキーワードの前にasyncを付けます。アロー関数の場合は (params) => { … } の前にasyncを置きます。
  2. Promiseを返す関数の前にawaitを書く
    例えば、UrlFetchApp.fetch()はHTTPResponseオブジェクトを返しますが、それをPromiseとしてラップした関数を用意し、その前にawaitを書きます。ただし、UrlFetchApp.fetch()自体は同期的に動作するため、実際の非同期処理はUtilities.sleep()や外部スクリプトの呼び出しなどで発生します。公式の非同期関数として、SpreadsheetApp.getActiveSpreadsheet()などは同期的です。ここでは例として、1秒待つユーティリティ関数を使って説明します。
  3. エラー処理はtry…catchで囲む
    awaitで待機中にPromiseがrejectされた場合、例外がスローされます。それをキャッチするために、awaitを含むブロックをtry…catchで囲みます。

具体的なコード例を示します。まず、簡単な非同期処理として、指定したミリ秒後に解決するPromiseを作成する関数を定義します。

function delay(ms) {
  return new Promise(function(resolve) {
    Utilities.sleep(ms);
    resolve();
  });
}

このdelay関数は、Utilities.sleep()で指定時間待った後にresolveするPromiseを返します。次に、この関数をasync関数内でawaitを使って呼び出します。

async function sampleAsyncFunction() {
  console.log('開始します');
  await delay(2000);
  console.log('2秒後に実行されます');
}

このように、awaitの行で処理が一時停止し、delayが完了してから次の行に進むため、console.logの出力が期待通り2秒後に表示されます。次に、複数の非同期処理を順番に実行する場合を見てみましょう。

複数のawaitを順番に使うパターン

複数の非同期処理を直列で実行するには、順番にawaitを並べます。例えば、スプレッドシートからデータを取得し、それを加工して別のシートに書き込む場合を考えます。

async function sequentialProcessing() {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
    const data = sheet.getDataRange().getValues();
    // 何らかの非同期処理(例:外部API)をawait
    const transformedData = await someAsyncTransformation(data);
    // 結果を別シートに書き込む
    const outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('出力');
    outputSheet.getRange(1, 1, transformedData.length, transformedData[0].length).setValues(transformedData);
    console.log('処理が完了しました');
  } catch (error) {
    console.error('エラーが発生しました: ' + error.message);
  }
}

ここでsomeAsyncTransformationは、外部APIを呼び出すなど時間のかかる非同期関数を想定しています。awaitによって、変換処理が完了するまで次の行に進まないため、データの書き込みが正しい順序で行われます。

複数の非同期処理を並列実行するパターン

複数の独立した非同期処理を同時に実行し、すべての完了を待つには、Promise.all()とawaitを組み合わせます。

async function parallelProcessing() {
  const promises = [];
  for (let i = 0; i < 5; i++) {
    promises.push(delay(1000)); // 1秒待つ処理を5つ同時に開始
  }
  await Promise.all(promises);
  console.log('すべての処理が完了しました');
}

このコードでは、5つのdelay処理が並行して実行され、すべてが完了するまで待機します。合計時間は約1秒です。もしawaitをそれぞれ順番に書くと5秒かかるため、並列実行によって大幅に時間を短縮できます。

async/await使用時の注意点とよくあるトラブル

awaitはasync関数内でしか使えない

awaitキーワードを使用できるのは、asyncで宣言された関数の中だけです。トップレベルのスクリプトや普通の関数でawaitを使うと、構文エラーになります。そのため、メインの処理は必ずasync関数でラップして呼び出す必要があります。

V8ランタイムの確認

古いRhinoランタイムではasync/awaitが使えません。スクリプトエディタの「設定」から「V8対応にする」が有効になっているか確認してください。2024年現在、新しいプロジェクトはデフォルトでV8ですが、古いプロジェクトでは手動で切り替える必要があります。

スプレッドシートのAPI呼び出しは同期的に動作する

SpreadsheetAppやSheetクラスのほとんどのメソッドは同期的に実行されます。async/awaitは主にUrlFetchAppや外部サービスとの通信、または自分で作成した非同期処理に有効です。スプレッドシート操作自体を非同期化したい場合は、SpreadsheetApp.flush()や大量データの分割処理を検討してください。

awaitでUIがブロックされるわけではない

クライアントサイドのJavaScriptとは異なり、Apps Scriptはサーバーサイドで実行されるため、awaitによるブロッキングはスクリプトの実行スレッド内で発生します。UIがブロックされることはありませんが、長時間のawaitはスクリプトの実行時間制限に注意してください。

ADVERTISEMENT

async/awaitと従来のPromise方式の比較

項目 async/await Promise.then()
コードの可読性 同期処理のように書けるため高い チェーンが長くなるとネストが深くなりやすい
エラー処理 try...catchで一元的に扱える .catch()チェーンまたは.catch()の末尾に追加
並列処理 Promise.allとawaitの組み合わせで直感的 Promise.all().then()で書くがawaitほど読みやすくない
デバッグのしやすさ ステップ実行でawaitの終了を待てる 非同期の流れを追いにくい場合がある
Apps Scriptでの使用可否 V8ランタイムでのみ使用可能 V8/Rhinoの両方で使用可能(ただしRhinoは非推奨)

まとめ

この記事では、Google Apps Scriptでasync/awaitを使った非同期処理の書き方を解説しました。async/awaitを利用することで、非同期コードを同期処理のように読みやすく記述でき、エラー処理もtry...catchで簡潔に行えます。複数の非同期処理を順番に実行する直列パターンや、Promise.allで並列実行するパターンも、それぞれのサンプルコードを参考にしてください。特に、外部APIとの連携や時間のかかる処理を効率的に扱いたい場合、async/awaitは強力なツールです。次に、実際のプロジェクトでUrlFetchAppや独自のPromiseを組み合わせて、より複雑なワークフローに挑戦してみてください。


ADVERTISEMENT

この記事の監修者
✍️

超解決 第一編集部

疑問解決ポータル「超解決」の編集チーム。正確な検証と、現場視点での伝わりやすい解説を心がけています。