LITALICO Advent Calendar 2024🎄の16日目です。
ぜひ他の記事もご覧ください!
はじめに
こんにちは、@kgshoheiです。
みなさまはGAS(GoogleAppsScript)を開発する際に、何かしらのやりづらさを感じたことはないでしょうか?
自分は今、LITALICOのコーポレートエンジニアとして教室の先生方のお困りごとを解決しつつ、利用されているツールやシステムの保守運用を日々行っています。
教室ツールの多くがGoogleスプレッドシートで作られており、業務の平準化・効率化などのためにGASのプログラムが組まれて動いています。
今年の春に入社し、これまでTypeScriptやNodeJSでの開発経験はありましたが、GASは初めてでした。
GASにかかわる特性やお作法などをその度に調べつつ、どうすれば開発体験を向上させつつ開発を楽しめるだろうか、とここまでチームで取り組んできた内容をご紹介しようと思います。
📝 内容まとめ
いきなりですが、取り組んできた内容のまとめです。
ランタイム
IDE, パッケージ管理
- VSCode
- npm
コンポーネント分割
-
esbuildで
import/export
したものをバンドル
型安全
- JSDocで型定義
- @ts-checkで静的型チェック
- @types/google-apps-scriptでコード補完
デプロイ
- 開発と本番で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エディタで選択できるのはfunction1
とfunction2
のみになります。
// 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.Range
のgetValues()はGASプログラミングで頻出ですが、
値は、セルの値に応じて、Number、Boolean、Date、String のいずれかになります。空のセルは、配列内の空の文字列で表されます。
とリファレンスにある通り、返ってきたany型の2次元配列の中身はセルの値に応じて何が入っているか分かりません。書かれている変数名から判断し切れなければ、実物のスプレッドシートを眺めることになります。getValues()
が出てくる度に確認するのは、とても苦痛です。
コードを読んでも何をしているかがほぼ分からない、というのは、特に新規参画者の理解をはばみオンボーディングを妨げる大きな要因になります。
そのため、ここでことさら「型」に注目しているのは、つくりの堅牢性に加え可読性を重視した結果になります。
JSDoc
JSDocが何かは、こちらを一読いただければ分かると思います。
ブロックコメントに所定のアノテーションを付与することで、ローカル開発中に型定義の恩恵を受けることができます。
自分がよく使うのは、@type
、@typedef
、@param
、@returns
です。この4つで、型安全なコーディングが概ね実現できると感じています。@typedef
はObject
型をつくることができ、便利です。
オブジェクトには@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のEnvironment
やSecrets
へ定義しておきます。定義するための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
を拾って型付けしていく方が楽であり、そのためにもまずは本稿の内容を推進していくのを優先したいと思っています
- 本稿の内容はTS一歩手前の状態だと感じており、本稿では取り上げなかった
✏️ おわりに
ここまで取り組んできた内容をご紹介してきましたが、まだ触れていない内容も多くあり、別の機会があればまた記事にしたいなと考えています。
- GAS環境で
sourcemap
が使えない - SDKがなく、テストが自動化しづらい
- GASのリファクタリング観点
- GASでオススメしたいlint、整形設定など
など。
また、今回記事を書くにあたり、曖昧な理解だった部分をあらためて見直したりして、とても勉強になりました。このような機会をいただけたことに感謝します🙏
では良いアドベントライフを!
明日は17日目のLITALICO Advent Calendar 2024🎅🏻です!
どうぞよろしくお願いいたします。
参考:https://www.typescriptlang.org/ja/docs/handbook/intro-to-js-ts.html