19
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LITALICOAdvent Calendar 2024

Day 16

GASで良い感じの開発体験をしたい

Last updated at Posted at 2024-12-15

LITALICO Advent Calendar 2024🎄の16日目です。
ぜひ他の記事もご覧ください!

はじめに

こんにちは、@kgshoheiです。
みなさまはGAS(GoogleAppsScript)を開発する際に、何かしらのやりづらさを感じたことはないでしょうか?

自分は今、LITALICOのコーポレートエンジニアとして教室の先生方のお困りごとを解決しつつ、利用されているツールやシステムの保守運用を日々行っています。

教室ツールの多くがGoogleスプレッドシートで作られており、業務の平準化・効率化などのためにGASのプログラムが組まれて動いています。

今年の春に入社し、これまでTypeScriptやNodeJSでの開発経験はありましたが、GASは初めてでした。
GASにかかわる特性やお作法などをその度に調べつつ、どうすれば開発体験を向上させつつ開発を楽しめるだろうか、とここまでチームで取り組んできた内容をご紹介しようと思います。

📝 内容まとめ

いきなりですが、取り組んできた内容のまとめです。

スクリーンショット 2024-12-09 16.35.32.png

ランタイム

IDE, パッケージ管理

  • VSCode
  • npm

コンポーネント分割

  • esbuildimport/exportしたものをバンドル

型安全

デプロイ

  • 開発と本番でGoogleWorkspaceを分離
  • claspでデプロイ
  • 本番デプロイはGithub Actions

テスト

  • 開発用Googleアカウントでテスト

💡 観点

「開発体験」は広い言葉ですが、自分は 「心理的な安全性」 が大事だととらえています。

安全に修正したい、安全にデプロイしたい、安全に試したい・・
「安全に」は開発していると必ずついて回るワードだと思います。

GASには専用のエディタ1があり、その場で修正・保存して実行が可能です。
場合や状況によっては、そのような運用がマッチするケースもあると思いますし、それがGASのウリだとも考えられます。

一方で、EUD2が進んだ結果として、スプレッドシート+GASの業務が大規模化し、一定程度の堅牢性をもった開発プロセスが必要になるケースも多いかと思います。
自分が直面したのはこのケースで、向き合うにあたり悩ましい点がありました。

  • 大規模な業務に対し、そもそもスプレッドシート+GASは正しいのか
  • そもそも論がある構成に対し、どの程度リソースをかけるべきなのか

とはいえ、日々業務は動き、変わり、ときにはエラーを吐いて、悩んでるのを待ってはくれません。

実際、Googleスプレッドシートは強力なツールで、GASとかけ合わせることでかなり柔軟かつ自由度高く業務のデザイン&効率化が実現できます。
しかし、裸のままのベアなGAS開発は「安全な」開発プロセスとは感じられず、何かしらの工夫が必要だと考えました。

スプレッドシート+GASを用いた業務をいかに素敵に、事業価値やスピード感を損なわず、かつどのように「一定程度の堅牢性」を保つか。
このような観点から 「手をかけすぎず効果が高そう💎」 な対応を直近のテーマとして考え取り組んできました。
レガシーなJavaScriptプロジェクトの堅牢化といったテーマに近しいと思います。

🔎 内容の説明

V8ランタイム

最新のECMAScript構文が利用できるとありますので、積極的に利用しましょう。

とはいえ、移行に際して一部の既存コードやライブラリでエラーが出るとの報告もあります。検証して安全を確認してから移行するよう、注意しましょう。

意識することは少ないですが、最新構文を厳密にサポートしているというわけでもなさそうなため3、気になる場合はV8でサポートされている構文例もチェックすると良いと思います。

VSCode, npm

GASエディタではなくローカルでコーディングします。エディタにはVSCodeを利用します。
本稿でも挙げる@ts-checkの型チェックを行いたいのに加えて、lintや整形、補完やその他拡張の恩恵は大きく、強いこだわりがない場合はVSCodeを利用するのが良いでしょう。

npmについては後述しますが、@types/google-apps-scriptでの型補完を効かせたいのと、esbuildを使いたいため入れています。
npm以外でもyarnなど、お好みで良いと思います。

コンポーネント分割

GASではES6より導入されたimport/export構文が利用できません。
import/exportについてはこちらが分かりやすいです

これは、GASエディタ上では.gsファイルをいくつ分割しても他ファイルから参照することができ、import/exportを明示的に書く必要が無いのと関係していそうです。

GASエディタでコーディングするならまだしも、ローカルのVSCodeで書く際には不便ですし、コンポーネント分割による書き味や保守性などのメリットがあるため、実現したいところです。

そこで、esbuildを導入します。

esbuildを導入することで、ローカルでコンポーネント等の望ましい単位に分割して開発しつつ、GAS実行環境においてはimport/exportが解決されて1つにバンドルされたスクリプトとして実行可能になります。

esbuildでは、entryPointsに公開する関数などを集めたモジュールを記載し、outfileへ出力先を定義することで、芋づるでimport/export関係を拾って1つのモジュールへバンドルしてくれます。
GASに適応するため、esbuild-gas-pluginも前述のnpmでesbuildと合わせてdevDependenciesとしてインストールしておきます。

import esbuild from "esbuild";
import { GasPlugin } from "esbuild-gas-plugin";

esbuild
  .build({
    entryPoints: ["./src/index.js"],
    bundle: true,
    charset: "utf8",
    outfile: "./dist/index.js",
    plugins: [GasPlugin],
...

entryPointsに指定するjsは、公開したい関数のみを定義しておきます。
以下の場合、GASエディタで選択できるのはfunction1function2のみになります。

// src/index.js
import { function1 } from "@/components/function1";
import { function2 } from "@/components/function2";
/**
 * @description
 *  - GASで公開する関数を定義します
 *  - 公開するとGASエディタで選択・実行することができます
 *  - GASへ公開したい関数のみをglobal変数へ設定してください
 */

/**
 * 処理1
 */
global.function1 = function1;

/**
 * 処理2
 */
global.function2 = function2;

GASでimport/exportを実現する方法はいくつかありますが、esbulidの設定が割と簡単なのと、entryPointsに指定するindex.jsが直感的かつ導入コストのバランス感が合ったため、この方法を採用しています。

ℹ️GASエディタで関数の公開を切り替える方法

上記でindex.jsでglobalから公開する方法を記載しましたが、esbuildを使わない構成の場合でも、GASエディタ上で関数名末尾にアンダースコアを付け外しすることで、簡単に公開/非公開を切り替えることができます。
以下の関数は、GASエディタからは選択できず、非公開となります。

function function1_ () { ... }

ともすればGASエディタのプルダウンに長大な関数リストが並ぶことになるので、公開しない関数は積極的に非公開設定を行ったほうが良いでしょう。

型安全

スプレッドシート+GASの特徴として、どのようなデータが扱われているか、コードを見るだけだと非常にわかりづらい、という点があります。

例としてGoogleAppsScript.Spreadsheet.RangegetValues()はGASプログラミングで頻出ですが、

値は、セルの値に応じて、Number、Boolean、Date、String のいずれかになります。空のセルは、配列内の空の文字列で表されます。

とリファレンスにある通り、返ってきたany型の2次元配列の中身はセルの値に応じて何が入っているか分かりません。書かれている変数名から判断し切れなければ、実物のスプレッドシートを眺めることになります。getValues()が出てくる度に確認するのは、とても苦痛です。
コードを読んでも何をしているかがほぼ分からない、というのは、特に新規参画者の理解をはばみオンボーディングを妨げる大きな要因になります。

そのため、ここでことさら「型」に注目しているのは、つくりの堅牢性に加え可読性を重視した結果になります。

JSDoc

JSDocが何かは、こちらを一読いただければ分かると思います。

ブロックコメントに所定のアノテーションを付与することで、ローカル開発中に型定義の恩恵を受けることができます。
自分がよく使うのは、@type@typedef@param@returnsです。この4つで、型安全なコーディングが概ね実現できると感じています。@typedefObject型をつくることができ、便利です。

オブジェクトには@typedefで型定義を行ったうえで@typeを付けて宣言し、関数は@param@returnsで引数と戻り値を定義する、というのがよくやる使い方で、これでコード補完を効かせつつ大体の型チェックが事足ります。

// 型定義
/**
 * @typedef {Object} TestType
 * @property {string} field1
 * @property {number} field2
 * @property {boolean} field3
 */

// 関数
/**
 * @description サンプル関数です
 * @param {TestType} param
 * @returns {boolean}
 */
const function1 = (param) => {
  const { field1, field2, field3 } = param;
  console.log(`field1: ${field1}, field2: ${field2}, field3: ${field3}`);
  // -> field1: test, field2: 10, field3: true
  return !field3;
};

// 型付変数宣言
/** @type {TestType} */
const param = {
  field1: "test",
  field2: 10,
  field3: true,
};

// 関数呼び出し
const result = function1(param);
console.log(result);
// -> false

詳細な説明や使い方は、前述のリファレンスをご覧ください。

@ts-check

実は前述のJSDocを定義しただけでは、エディタでエラーが出ません。
型チェックのエラーを出すには、VSCodeのType checking機能を利用します。

使い方は簡単で、ファイルの先頭へ// @ts-checkと入れるだけです。
これで以下のようなコードは、その場でエラーが出るようになります。

// @ts-check
/**
 * @typedef {Object} TestType
 * @property {string} field1
 * @property {number} field2
 * @property {boolean} field3
 */
 
/** @type {TestType} */
const test = {
  field1: "field1",
  field2: "10", // --> エラー
  field3: true,
};

@types/google-apps-script

「型」パートの最後は、@types/google-apps-scriptの利用です。

GASエディタではGoogleAppsScript.Spreadsheet.Rangeなどと打つとgetValue()が補完できますが、ローカルだとそのままでは補完できません。
この点、Googleが型定義を提供してくれており、こちらもdevDependenciesへインストールすることで、ローカルでも補完可能となります。

ℹ️lintを入れている場合

eslintで未定義変数をチェックしている場合、オブジェクトとしてSpreadsheetAppを使おうとするとlintエラーになります。typesしかインストールしていないため当然で、/* eslint-disable no-undef */などで逃げることになります。

SpreadsheetAppを書く度にdisableするのもかなり煩雑であるため、自分の場合は、google-apps-scriptの型を使う定義をまとめて、以下のようにdisableをいっぺんにかけるようにしています。

export function googleAppsScript() {
  /* eslint-disable no-undef */
  const spreadsheetApp = SpreadsheetApp;
  const driveApp = DriveApp;
  const utilities = Utilities;
  /* eslint-enable no-undef */

  return {
    spreadsheetApp: spreadsheetApp,
    driveApp: driveApp,
    utilities: utilities,
  };
}

使う側では、exportされたものを選んで使う感じです。これで、都度disableしなくて済みます。

  const { spreadsheetApp, utilities } = googleAppsScript();

デプロイ

開発と本番でGoogle Workspaceを分けて、それぞれのアカウントへ紐づくGASへデプロイします。(冒頭の構成図はこちら)
アカウントはそれぞれが互いにアクセスできないように権限を設定しておき、後述のclaspで各アカウントへログインしたうえでpushすることで、スクリプトID間違いによる誤デプロイを防ぎます。

clasp

clasp(Command Line Apps Script Projects)はgoogleが提供している、GASプロジェクトを操作するためのCLIです。

clasp commandの形で操作します。
基本的な操作は

  • ログイン:clasp login
  • プロジェクト取得:clasp clone
  • 最新取得:clasp pull
  • デプロイ:clasp push

あたりです。
clasp loginすると、アクセストークンなどが書かれた.clasprc.jsonがログインしたマシンに作成されて、ログイン情報が記録されます。
また、GASプロジェクトのスクリプトIDを指定してclasp cloneを行うと、同様にcloneをしたマシンへスクリプトIDの書かれた.clasp.jsonが作成されて、GASプロジェクトとの紐づけがなされます。

{
  "scriptId": "指定したスクリプトID",
  "rootDir": "."
}

まずはGASプロジェクトを開発アカウントで作成した後に、初回のclasp cloneをローカルで実施し.clasp.jsonを作成して、コーディングとclasp pushを回す、といった流れで開発・テストを行います。
複数人で行う場合、自身のGASプロジェクトを各人で作って開発を回したりもします。

GitHub Actions

本番のGASプロジェクトへローカルからclasp push可能だと危険なため、本番GASプロジェクトは本番アカウントでのみアクセスできるように権限を設定します。
ローカルから本番アカウントへclasp loginすることはせず、また、コード管理とも連動させるためGitHub Actions(以降GHAと呼びます)から本番アカウントへログインし、clasp pushを行います。

GHAで本番アカウントへログインできるよう.clasprc.jsonはworkflow内で自動生成します。json内で変数となるrefresh_tokenなどはGitHubのEnvironmentSecretsへ定義しておきます。定義するためのtoken内容は、手元で本番アカウントへ一度clasp loginして確認したものを使います。

前述のesbuildによるバンドルも、以下のようにGHAのworkflowで定義しておきます。

  • package.json
  "scripts": {
    "build": "node ./esbuild.js && cp -f ./src/appsscript.json ./dist",
    "dev-deploy": "npm run build && clasp push"
  }
  • workflow.yml
      # esbuild 実行
      - name: Build js sources
        run: |
          npm install
          npm run build

⤴ TypeScript化

TypeScript(以降TSと呼びます)は目指したい世界の一つではありますが、急がず次のステップで良さそうかな、というのが今の所見です。

  • 本稿内容とTSの比較

    • 実際にTSでプロトタイプのGASプロジェクトを作って開発してみたこともありましたが、本稿内容での書き味は、TSと比べてそこまで遜色ないと感じました
    • 型定義や宣言はTSに比べ多少面倒であるものの、型推論まで含めたチェックも効いており、良い開発体験ができると思います
  • TSは必要?

    • 現状の自分たちのスプレッドシートを取り巻く環境が、周辺の他システムと多く連携しているわけでもなく、TSの持ち味であるユニオン型やユーティリティ型といったそこまでの柔軟な型定義を必要としていないかも、とも思います
    • 一方で、TSにかかわるテストなどのエコシステムは魅力的なため、TS化は少しずつでも進めたいです
  • TSへの移行

    • 本稿の内容はTS一歩手前の状態だと感じており、本稿では取り上げなかったjsconfig.jsonといった設定ファイルも流用できそうな印象です
    • 何にせよ移行に際してはコードのリライトが必要になるため、JSDoc@type@typedefを拾って型付けしていく方が楽であり、そのためにもまずは本稿の内容を推進していくのを優先したいと思っています

✏️ おわりに

ここまで取り組んできた内容をご紹介してきましたが、まだ触れていない内容も多くあり、別の機会があればまた記事にしたいなと考えています。

  • GAS環境でsourcemapが使えない
  • SDKがなく、テストが自動化しづらい
  • GASのリファクタリング観点
  • GASでオススメしたいlint、整形設定など

など。

また、今回記事を書くにあたり、曖昧な理解だった部分をあらためて見直したりして、とても勉強になりました。このような機会をいただけたことに感謝します🙏

では良いアドベントライフを!

明日は17日目のLITALICO Advent Calendar 2024🎅🏻です!
どうぞよろしくお願いいたします。

参考:https://www.typescriptlang.org/ja/docs/handbook/intro-to-js-ts.html

  1. スクリプト プロジェクト

  2. End User Development

  3. 最新のECMAScript構文

19
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?