UPSIDER Tech Blog

【GAS】スプレッドシートと連携するGASのメンテナンス性を劇的に向上させる実装テクニック

はじめに

こんにちは。支払い.comフロントエンドエンジニアのToriiです。

Google Apps Script(GAS)でスプレッドシートと連携したツールを作っていると、こんな悩みにぶつかったことはありませんか?

  • 本番環境で直接コードを編集していて、うっかりバグを仕込んでしまった
  • 複数人で開発しているのに、誰がどんな変更をしたかわからない
  • セルに数式を大量に設定したら、行数が増えるたびに重くなってきた
  • コードが何をやっているのか、後から見返してもわからない

業務で使うスプレッドシートは「本番環境」として扱われることが多く、一度壊してしまうと業務に大きな影響を与えてしまいます。かといって、テスト環境を用意するのも面倒...。

今回の記事では、こういった課題を解決してGASのメンテナンス性を劇的に向上させる実装テクニックを紹介します!

目次

開発環境の整備

課題:開発環境が存在しない × バージョン管理されていない

GASの最大の課題は、開発環境が存在しないことです。本番環境で直接コードを編集するのは、まるで地雷原を歩くようなものですよね...。

解決策:claspとGitで開発環境を構築する

clasp(Command Line Apps Script Projects )は、Googleが公式に提供しているGASのコマンドラインツールです。これを使えば、ローカルでコードを書いてGitで管理できるようになります!

github.com

claspを使うことで、VSCodeなどの使い慣れたエディタでコードを書き、変更をGASプロジェクトにプッシュできます。さらに、TypeScriptでの開発もサポートされており、型安全な開発が可能になります。また、webpackやesbuildなどのバンドラーを使えば、npmパッケージ(node_modules)も利用できるようになります。
claspをインストールして、既存のGASプロジェクトをクローンするか新規作成すれば、ローカルでの開発が可能になります。あとはGitでバージョン管理するだけで、コードレビューや履歴管理ができるようになります。

clasp設定ファイルを複数作ることで開発環境と本番環境を切り替える

clasp pushコマンドには設定ファイルのパスを指定できる--projectオプションがあります。これを活用して、環境ごとの設定ファイルを用意しましょう。

まず、環境ごとの設定ファイルを作成します:

.clasp.dev.json (開発環境用)

{
  "scriptId": "開発環境のスクリプトID",
  "rootDir": "./src"
}

.clasp.prod.json (本番環境用)

{
  "scriptId": "本番環境のスクリプトID",
  "rootDir": "./src"
}

次に、package.jsonにnpm scriptsを定義します:

{
  "scripts": {
    "push:dev": "clasp push --project .clasp.dev.json",
    "push:prod": "clasp push --project .clasp.prod.json"
  }
}

これで、以下のコマンドで環境を切り替えてプッシュできるようになります:

# 開発環境にプッシュ
npm run push:dev

# 本番環境にプッシュ
npm run push:prod

開発環境で十分にテストしてから本番環境にプッシュできるようになりました!

メリット

  • 事前に動作確認ができるため、安全に変更を加えられる
  • コードレビューを通るのでコードの品質向上 & 属人性解消
  • もしスプシが壊れてもいつでもGASの復元ができる
  • clasp pushによるソースコードの更新はすぐに完了するため、オペレーションを止める必要がない

実装の工夫

シートIDではなくシート名に依存させる

課題:開発環境と本番環境でシートIDが変わってしまう

開発環境と本番環境でスプレッドシートが異なる場合、シートIDも変わってしまいます。シートIDに依存したコードを書いていると、環境ごとにコードを書き換える必要が出てきて面倒です。

解決策:シート名で取得する

// ❌ 悪い例:シートIDに依存
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetById('1234567890abcdef')

// ✅ 良い例:シート名に依存
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');

メリット

  • 開発環境と本番環境でシートIDが変わっても困らない
  • コードがより自明になる

列番号ではなく列名に依存させる

課題:コードがわかりづらい、なぜか壊れやすい

こんなコードを見たことはありませんか?

// ❌ 悪い例:マジックナンバーだらけ
const [header, ...data] = sheet
    .getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns())
    .getValues();

const result = data.map(row => row[2] * row[4]);

数字だらけで、何をやっているのかさっぱりわかりませんよね...。

解決策:列名を使った実装

// ✅ 良い例:列名依存の実装
const [header, ...data] = sheet
    .getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns())
    .getValues();

const result = data.map(row => row[toIndex('単価')] * row[toIndex('数量')]);

// 列名から列番号を取得
const toIndex = (columnName) => header.indexOf(columnName);

より実践的な実装では、列定義を一元管理することでさらなる保守性の向上が可能です

// 列の定義を一元管理
const COLUMNS = {
  unitPrice: '単価',
  quantity: '数量',
  totalPrice: '合計金額'
};

const [header, ...data] = sheet
    .getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns())
    .getValues();

// 列名から列番号を取得
const toIndex = (columnName) => header.indexOf(columnName);

// 使用例
const result = data.map(row => row[toIndex(COLUMNS.unitPrice)] * row[toIndex(COLUMNS.quantity)]);

メリット

  • シート中の列の順番を変更しても壊れないため、オペレーションに合わせて自由に列の順番を調整することができる
  • マジックナンバーが出てこないのでコードがより自明になる

パフォーマンス改善のテクニック

課題:処理が重くてタイムアウトしてしまう

スプレッドシートと連携するGASでは、以下のような原因でパフォーマンスが悪化することがあります:

  • getValues/setValuesを繰り返し実行している
  • セルに複雑な数式を大量に設定している
  • リアルタイムである必要がない処理も都度実行している

解決策1:getValues, setValuesの回数を最小限にする

以下のようなコードになっていませんか?

// ❌ 悪い例:セルごとに値を取得・設定
for (let i = 1; i <= 100; i++) {
  const value = sheet.getRange(i, 1).getValue();
  const result = processValue(value);
  sheet.getRange(i, 2).setValue(result);
}

スプレッドシートのデータへのアクセス・書き込みは重い処理のため、何度も繰り返し実行すると処理が重くなります。以下のように一度だけ呼び出すようにすることで、処理速度が向上し、副作用のある処理が最小限になってコードの見通しが良くなります。

// ✅ 良い例:一括で取得・設定
const range = sheet.getRange(1, 1, 100, 1);
const values = range.getValues();

const results = values.map(row => row.map(processValue));
range.setValues(results);

解決策2:BYROWやARRAYFORMULAの使用

// スプレッドシートの数式
=ARRAYFORMULA(IF(A2:A="", "", A2:A * B2:B * 1.1))

列数を増やした際にオートフィルで初期設定が不要になるメリットもあります。

解決策3:カスタム関数の活用

数式の計算が複雑ならカスタム関数化することで、メンテナンス性とパフォーマンスの両方を改善できます。

function CALCULATE_TOTAL(prices, quantities, taxRate) {
  // 配列で受け取ることでパフォーマンスを改善
  const priceArray = Array.isArray(prices) ? prices : [prices];
  const quantityArray = Array.isArray(quantities) ? quantities : [quantities];
  
  return zip(priceArray, quantityArray).map(([price, quantity]) => price * quantity * taxRate);
}

const zip = (arr1, arr2) => arr1.map((val, i) => [val, arr2[i]]);

使用方法:

=CALCULATE_TOTAL(A2:A100, B2:B100, 0.1)

注意点:セルごとにカスタム関数を呼び出すとパフォーマンスがかなり悪いので、配列値を受け取るような関数にしておきましょう。これにより、関数の評価回数が一回になるので軽くなります。

解決策3:リアクティブでない値はGASで手動更新にするのもあり

更新頻度が低いデータなら、GASで手動更新する方法も有効です。

// スプレッドシートを開いた時にメニューを追加
function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('カスタム処理')
    .addItem('データを更新', 'updateStaticData')
    .addToUi();
}

// 手動更新用の関数
function updateStaticData() {
  const sheet = SpreadsheetApp.getActiveSheet();
  
  // 静的なデータを計算
  const data = calculateStaticData();
  
  // 一括で書き込み
  sheet.getRange(2, 1, data.length, data[0].length).setValues(data);
  
  // 完了通知
  SpreadsheetApp.getUi().alert('データの更新が完了しました!');
}

注意点:スピルするような数式との併用で壊れやすくなることがあるため、計算を関数に寄せるかGASに寄せるか決める必要があります。

おわりに

今回の記事では、スプレッドシートと連携するGASのメンテナンス性を向上させるテクニックを紹介しました。

最初は少し面倒に感じるかもしれませんが、これらのテクニックを実践することで:

  • バグの発生率が大幅に減少
  • 開発スピードが向上
  • チーム開発がスムーズに

といったメリットを享受できます。

特に業務で使用されているスプレッドシートは、多くの人に影響を与える「本番環境」です。今回紹介したテクニックを使って、安全で効率的な開発環境を構築してみてください!

最後に、これらのテクニックは一度に全部導入する必要はありません。まずはclaspによるバージョン管理から始めて、徐々に他のテクニックも取り入れていくのがおすすめです。

快適なGAS開発ライフを!

We Are Hiring !!

支払い.comをはじめUPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

カジュアル面談はこちら!

herp.careers

Culture Deckはこちら📣

speakerdeck.com