Haeminway haeminway
English
기술 노트로
5 분 분량

설치됨 ≠ 작동함: 백업 트리거가 한 번도 안 돌았다

GAS에서 스코프·트리거·권한은 에러 없이 조용히 실패한다. 매니페스트 스코프 하나가 빠지면 트리거가 한 번도 안 돈다. 왜 조용히 실패하는지, 멱등 설치로 20개 한도를 피하는 법, 하트비트와 '설치 직후 증명'까지.

GAS에선 “설치됨”이 “작동함”이 아니다. 스코프·트리거·권한은 에러를 안 내고 조용히 안 돈다. 백업 코드가 있었고 트리거도 “설치”했는데, 성공 기록이 단 한 번도 없었다. appsscript.jsonscript.scriptapp 스코프 하나가 빠져서 트리거가 조용히 안 떴던 거다.

설치됨은 작동함이 아니다: 트리거는 '설치'됐지만 한 번도 안 돎. 하트비트(마지막 성공 시각)로만 살아있다고 판정한다

왜 중요한가

안 도는 걸 아무도 모른다. 사고가 터져 복구하려는 순간에야 “백업이 없다”는 걸 발견한다. 실효 복구 지점(RPO)이 무한이라는 뜻이다. 눈에 보이는 기능은 사용자가 리포트라도 하지만, 백업·동기·정리 같은 백그라운드 자동화는 조용히 죽으면 조용히 묻힌다.

왜 조용히 실패하나

세 가지가 대표적이고, 셋 다 에러를 안 낸다.

  • 매니페스트 스코프 누락 — 트리거 생성엔 script.scriptapp이 필요하다.
  • 트리거 20개 한도 — 재설치를 반복하면 중복이 쌓여 조용히 실패한다.
  • 재동의 안 됨 — 스코프를 바꾸면 사용자가 다시 승인해야 활성화된다.

스코프부터 확인하라. 없으면 트리거가 안 뜬다.

// appsscript.json — 트리거를 쓰려면 script.scriptapp이 반드시 있어야 한다
{
  "timeZone": "Asia/Seoul",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.scriptapp"
  ]
}

정답: 멱등 설치 + 하트비트 + 헬스체크

설치는 멱등으로(같은 handler의 옛 트리거를 지우고 다시) 해서 20개 한도를 피한다. 그리고 실행마다 “마지막 성공 시각”을 남긴다.

const ADMIN = "[email protected]";

// 멱등 설치: 같은 handler의 기존 트리거를 지우고 새로 만든다 (중복·한도 방지)
function installBackupTrigger_() {
  ScriptApp.getProjectTriggers()
    .filter((t) => t.getHandlerFunction() === "runBackup")
    .forEach((t) => ScriptApp.deleteTrigger(t));
  ScriptApp.newTrigger("runBackup").timeBased().everyHours(6).create();
}

// 백업이 돌 때마다 '마지막 성공 시각'을 남긴다 = 하트비트
function runBackup() {
  doBackup_();
  PropertiesService.getScriptProperties()
    .setProperty("backup.lastOk", new Date().toISOString());
}

// 헬스체크(별도 트리거): 하트비트가 오래됐으면 '설치됨'이 아니라 '죽음'
function assertBackupAlive_() {
  const last = PropertiesService.getScriptProperties().getProperty("backup.lastOk");
  const ageMs = last ? Date.now() - new Date(last).getTime() : Infinity;
  if (ageMs > 26 * 60 * 60 * 1000) {                // 26시간 넘게 성공 없음
    MailApp.sendEmail(ADMIN, "백업 하트비트 끊김", "last=" + last);
  }
}

설치 직후에 증명하라

“설치했다”로 끝내지 마라. 설치 직후 한 번 실제로 돌려서 하트비트가 찍히는지 확인한다. 이게 스코프 누락·재동의 실패를 그 자리에서 잡는다.

// 설치가 아니라 '한 번이라도 성공'을 증거로 삼는다
function installAndProve_() {
  installBackupTrigger_();
  runBackup();                                       // 즉시 1회 실행
  const ok = PropertiesService.getScriptProperties().getProperty("backup.lastOk");
  if (!ok) throw new Error("BACKUP_NEVER_RAN");       // 하트비트 없으면 설치 실패로 간주
}

놓치기 쉬운 것

  • 매니페스트 스코프. 트리거 생성은 appsscript.jsonoauthScopesscript.scriptapp이 있어야 한다.
  • 성공을 기록하라, 설치가 아니라. “트리거 만들었음” 로그는 증거가 아니다. “최근 성공 시각”만이 살아있음의 증거다.
  • 트리거 20개 한도. 재설치를 반복하면 중복이 쌓여 조용히 실패한다. 만들기 전에 같은 handler의 옛 트리거를 지워라(멱등 설치).
  • 스코프 바꾸면 재동의. oauthScopes 변경 후 한 번은 사람이 실행해 권한을 다시 승인하라. cron만 돌면 조용히 실패한다.

깊이: 눈에 안 보이는 자동화일수록 하트비트를

백업·동기·정리처럼 사용자가 안 보는 자동화는 전부 마지막 성공 시각을 남기고, 그 값이 끊기면 알람하라. “설치했다”는 과거형이고, “26시간 내 성공”은 현재형이다. 핵심 한 줄: 자동화는 “설치됨”이 아니라 “마지막 성공 시각”으로만 살아있다고 판정하라.

자주 묻는 질문

Apps Script 트리거를 설치했는데 안 도는 이유는?
appsscript.json에 script.scriptapp 스코프가 빠졌거나, 트리거 20개 한도에 걸렸거나, 스코프 추가 후 권한 재동의가 안 된 경우입니다. 공통점은 에러 없이 조용히 안 돈다는 것이라, 하트비트 없이는 발견이 늦습니다.
백업이나 자동화가 실제로 도는지 어떻게 확인하나요?
매 실행마다 '마지막 성공 시각'을 스크립트 속성에 기록하고, 그 값이 오래되면 알람하는 헬스체크를 별도 트리거로 둡니다. '설치했다'가 아니라 '최근에 성공했다'로 살아있음을 판정하세요.
GAS에서 트리거 생성에 필요한 스코프는 뭔가요?
시간 기반 트리거 등 ScriptApp의 트리거 API를 쓰려면 appsscript.json의 oauthScopes에 script.scriptapp이 있어야 합니다. 빠지면 설치 자체가 실패하거나 트리거가 뜨지 않습니다.
스코프를 추가했는데도 왜 안 되나요?
oauthScopes를 바꾸면 사용자가 권한을 다시 동의해야 새 스코프가 활성화됩니다. 무인(cron)으로 돌리면 재동의가 안 돼 조용히 실패할 수 있으니, 스코프 변경 후 한 번은 사람이 실행해 재승인하세요.