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

:::note info
[LITALICO Advent Calendar 2024](https://qiita.com/advent-calendar/2024/litalico)🎄の16日目です。
ぜひ他の記事もご覧ください！
:::

## はじめに

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

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

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

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

## 📝 内容まとめ

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

![スクリーンショット 2024-12-09 16.35.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734237/9b77c269-ff5d-2c1a-b55c-a9a995bef9c4.png)


### ランタイム
- [V8ランタイム](https://developers.google.com/apps-script/guides/v8-runtime?hl=ja)
### IDE, パッケージ管理
- VSCode
- npm
### コンポーネント分割
- [esbuild](https://esbuild.github.io/)で`import/export`したものをバンドル
### 型安全
- [JSDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html)で型定義
- [@ts-check](https://www.typescriptlang.org/ja/docs/handbook/intro-to-js-ts.html)で静的型チェック
- [@types/google-apps-script](https://www.npmjs.com/package/@types/google-apps-script)でコード補完
### デプロイ
- 開発と本番でGoogleWorkspaceを分離
- [clasp](https://github.com/google/clasp)でデプロイ
- 本番デプロイは`Github Actions`
### テスト
- 開発用Googleアカウントでテスト

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

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

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

一方で、EUD[^2]が進んだ結果として、スプレッドシート＋GASの業務が大規模化し、一定程度の堅牢性をもった開発プロセスが必要になるケースも多いかと思います。
自分が直面したのはこのケースで、向き合うにあたり悩ましい点がありました。
- 大規模な業務に対し、そもそもスプレッドシート＋GASは正しいのか
- そもそも論がある構成に対し、どの程度リソースをかけるべきなのか

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

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

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

## 🔎 内容の説明

### V8ランタイム
最新のECMAScript構文が利用できるとありますので、積極的に利用しましょう。

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

意識することは少ないですが、最新構文を厳密にサポートしているというわけでもなさそうなため[^3]、気になる場合は[V8でサポートされている構文例](https://developers.google.com/apps-script/guides/v8-runtime?hl=ja#v8-syntax)もチェックすると良いと思います。

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

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

### コンポーネント分割
GASではES6より導入された`import/export`構文が利用できません。
※`import/export`についてはこちらが分かりやすいです

https://ics.media/entry/16511/#top

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

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

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

https://esbuild.github.io/

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

esbuildでは、`entryPoints`に公開する関数などを集めたモジュールを記載し、`outfile`へ出力先を定義することで、芋づるで`import/export`関係を拾って１つのモジュールへバンドルしてくれます。
GASに適応するため、`esbuild-gas-plugin`も前述のnpmでesbuildと合わせて`devDependencies`としてインストールしておきます。
```js
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`のみになります。
```js
// 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エディタからは選択できず、非公開となります。
```js
function function1_ () { ... }
```

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

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

例として`GoogleAppsScript.Spreadsheet.Range`の[getValues()](https://developers.google.com/apps-script/reference/spreadsheet/range?hl=ja#getvalues)はGASプログラミングで頻出ですが、
> 値は、セルの値に応じて、Number、Boolean、Date、String のいずれかになります。空のセルは、配列内の空の文字列で表されます。

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

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

#### JSDoc

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

https://www.typescriptlang.org/ja/docs/handbook/jsdoc-supported-types.html

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

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

https://code.visualstudio.com/docs/languages/javascript#_type-checking

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

https://www.npmjs.com/package/@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`をいっぺんにかけるようにしています。
```js
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`しなくて済みます。
```js
  const { spreadsheetApp, utilities } = googleAppsScript();
```

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

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

https://github.com/google/clasp

`clasp command`の形で操作します。
基本的な操作は
- ログイン：`clasp login`
- プロジェクト取得：`clasp clone`
- 最新取得：`clasp pull`
- デプロイ：`clasp push`

あたりです。
`clasp login`すると、アクセストークンなどが書かれた`.clasprc.json`がログインしたマシンに作成されて、ログイン情報が記録されます。
また、GASプロジェクトのスクリプトIDを指定して`clasp clone`を行うと、同様にcloneをしたマシンへスクリプトIDの書かれた`.clasp.json`が作成されて、GASプロジェクトとの紐づけがなされます。
```json
{
  "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
```json
  "scripts": {
    "build": "node ./esbuild.js && cp -f ./src/appsscript.json ./dist",
    "dev-deploy": "npm run build && clasp push"
  }
```
- workflow.yml
```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、整形設定など

など。

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

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

:::note info
明日は17日目の[LITALICO Advent Calendar 2024](https://qiita.com/advent-calendar/2024/litalico)🎅🏻です！
どうぞよろしくお願いいたします。
:::



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

[^1]:[スクリプト プロジェクト](https://developers.google.com/apps-script/guides/projects?hl=ja)
[^2]:[End User Development](https://kanagawa-u.repo.nii.ac.jp/records/13697)
[^3]:[最新のECMAScript構文](https://developers.google.com/apps-script/guides/v8-runtime?hl=ja#v8-syntax)