- 2026.07.29 Apps Script 프로젝트가 늘어나면 반드시 망가지는 것들
- 2026.07.16 매일 돌던 Apps Script 트리거가 갑자기 멈추는 이유
- 2026.07.15 브라우저 저장소에 고객정보 넣으면 생기는 문제
- 2026.07.07 브라우저가 내 데이터를 버릴 때: 초록 체크가 거짓말을 한다
- 2026.07.07 설치됨 ≠ 작동함: 백업 트리거가 한 번도 안 돌았다
- 2026.07.07 GAS엔 점진 배포가 없다: '맞는' 검사 하나가 전 사용자를 잠근다 지금 보는 글
- 2026.07.07 시트가 텍스트를 날짜로 바꾼다: '2026-06'이 완료를 영영 막을 때
- 2026.06.18 여러 사람이 한 시트에 쓰는 점검 앱: 동시성·중복·내보내기 설계
- 2026.06.12 셀을 한 칸씩 읽지 마라: 서비스 호출이 진짜 비용이다
- 2026.06.11 시트 셀에 JSON·이미지를 통째로 넣으면 터진다
- 2026.06.10 웹앱 배포 설정 하나가 보안 모델을 정한다: Execute as / 접근 권한
- 2026.06.09 GAS 6분 실행 한도: 멈추기 전에 끊어서 재개하라
- 2026.06.08 PropertiesService는 데이터베이스가 아니다: 9KB의 벽
- 2026.06.07 모바일에서 화면을 이미지로 저장할 때 멈추는 진짜 이유
- 2026.06.06 동시에 같은 시트를 쓰면 깨진다: LockService와 30 동시 실행 천장
- 2026.06.05 재배포할 때마다 주소가 바뀌면 안 된다: /exec URL 고정
- 2026.06.04 외부 호출은 가끔 실패한다: 지수 백오프와 재시도 예산
- 2026.06.03 HtmlService 앱은 껍데기 먼저: 비동기 로드와 모바일 뒤로가기
- 2026.06.02 GAS를 졸업해야 할 때: 한계 신호와 외부 DB 이전
- 2026.06.01 GAS에 지능과 엔진룸을 더하기: Vertex AI와 Cloud Run
GAS엔 점진 배포가 없다: '맞는' 검사 하나가 전 사용자를 잠근다
Apps Script는 promote 하는 순간 100% 사용자에게 나간다 — 카나리가 없다. push와 deploy의 차이, 새 검사를 compat → warn → enforce로 내보내는 법, 옛 클라이언트를 세는 텔레메트리, 그리고 사고 시 롤백 명령까지.
GAS엔 점진 배포가 없다. promote 하면 그 순간 100%에게 나간다. 이미 현장 폰에 깔린 클라이언트가 모르는 새 검사를 강제하면, 배포 즉시 전 사용자가 잠긴다. “보안 강화”라고 세션 검사를 넣었다가, 옛 클라이언트가 토큰을 안 보내서 전 현장의 이력 조회가 막힌 적이 있다.

왜 중요한가
카나리가 없으니 “일부에게 먼저 내보고 문제 없으면 확대”가 불가능하다. 코드가 로컬 테스트를 다 통과해도, 필드의 클라이언트는 지난주 번들이다. 서버만 새 계약을 강제하면 그 순간 옛 클라이언트 전부와 계약이 어긋난다. 롤백해도 그 사이 현장은 멈춰 있다.
push, deploy, promote — 뭐가 사용자에게 가나
먼저 배포 모델부터. clasp push는 소스만 올린다. 사용자는 여전히 옛 버전을 받는다. 사용자에게 가는 건 clasp deploy가 새 버전을 자를 때다.
clasp push # 로컬 → Apps Script 소스 (사용자엔 영향 없음)
clasp deploy -i <deploymentId> -d "메모" # 이 배포 ID를 새 버전으로 (사용자에게 반영)
clasp deployments # 배포 ID·버전 목록 확인
하나의 배포 ID를 유지하고 버전만 올려라. 그래야 웹앱 주소가 안 바뀐다(주소 고정). 매번 새 배포를 만들면 /exec 주소가 바뀌어 옛 링크가 죽는다.
정답: 새 검사는 compat → warn → enforce 사다리로
새로 강제하는 게이트를 즉시 enforce로 내보내지 마라. 스크립트 속성으로 모드를 두고, 기본을 안 깨지는 compat로 시작한다. warn 단계에서 옛 클라이언트 히트를 세서, 0이 되면 그때 enforce로 올린다.
function sessionMode_() {
return PropertiesService.getScriptProperties()
.getProperty("APP_SESSION_MODE") || "compat"; // 기본은 안 깨지는 compat
}
function requireApprovedSession_(token) {
const ok = token && isApprovedSession_(token);
const mode = sessionMode_();
if (ok) return;
if (mode === "compat") { recordLegacyHit_(); return; } // compat: 통과 + 카운트
if (mode === "warn") { // warn: 통과 + 기록
recordLegacyHit_();
console.warn("session missing (warn mode)");
return;
}
throw new Error("APP_SESSION_REQUIRED"); // enforce: 이때만 차단
}
// 옛 클라이언트(토큰 없는 요청) 히트를 센다 → 이 값이 0으로 수렴하면 enforce 승격 가능
function recordLegacyHit_() {
const p = PropertiesService.getScriptProperties();
p.setProperty("legacyHits", String(Number(p.getProperty("legacyHits") || 0) + 1));
}
승격 순서: compat로 배포 → 클라이언트 업데이트 배포 → warn으로 며칠 관찰 → legacyHits가 0이면 enforce. 모드 전환은 코드 재배포 없이 스크립트 속성만 바꾸면 된다.
사고 나면: 롤백
같은 배포 ID를 이전 안전 버전으로 다시 deploy 하면 즉시 되돌아간다.
# 이전 안전 버전으로 롤백 (배포 ID 유지, 버전만 이전으로)
clasp deploy -i <deploymentId> -V <이전_버전번호>
그래서 배포 직전에 현재 프로드 버전 번호를 어딘가 적어둬라. 사고는 대개 밤에 나고, 그 번호를 못 찾으면 롤백이 늦어진다.
놓치기 쉬운 것
- 카나리가 없다. 전부거나 전무다. 그래서 배포되는 기본값 자체가 안전해야 한다.
- 이전 버전 번호를 기록. 롤백은 배포 ID의 버전을 되돌리는 것. 그 번호를 미리 알아야 밤에 되돌린다.
- 필드의 클라이언트는 옛 코드다. 서버 변경은 옛 클라이언트와 호환되게. 새 필드·새 헤더를 “반드시 있어야 함”으로 만들지 마라(additive only).
- 모드는 데이터로 판단. “이제 됐겠지”가 아니라
legacyHits같은 실제 카운트가 0인지로 enforce를 결정하라.
깊이: 새 필수 계약은 언제나 사다리로
서버가 먼저 compat로 받아주고 → 클라이언트가 새 토큰을 보내기 시작하고 → 옛 클라이언트 히트가 0으로 수렴하고 → 그제야 enforce. 이 순서를 건너뛰면 필드가 멈춘다. 핵심 한 줄: 새 게이트는 기본을 compat로 두고, 옛 클라이언트가 사라진 걸 데이터로 확인한 뒤에만 enforce하라.
자주 묻는 질문
- Apps Script 배포를 일부 사용자에게만 먼저 내보낼 수 있나요?
- 없습니다. promote는 즉시 전역이라 카나리(일부 선배포)가 불가능합니다. 대신 코드 안에서 스크립트 속성 같은 모드 플래그로 새 규칙을 compat → warn → enforce 단계로 점진 강제하세요.
- clasp push를 했는데 왜 반영이 안 되나요?
- push는 소스만 업로드하고, deploy가 실제 버전을 웹앱으로 자릅니다. push가 'already up to date'라고 해도 사용자는 여전히 옛 버전을 받습니다. 배포는 push와 deploy가 별개 단계입니다.
- 새 배포가 기존 사용자를 잠갔습니다. 어떻게 복구하나요?
- 같은 배포 ID를 이전 안전 버전 번호로 다시 deploy 하면 롤백됩니다. 그다음 새 검사를 enforce가 아니라 compat 기본값으로 다시 내보내, 옛 클라이언트가 사라질 때까지 기다립니다.
- 롤백하려면 이전 버전 번호를 어떻게 알죠?
- clasp deployments로 배포·버전 목록을 볼 수 있지만, 배포 직전에 '지금 안전한 프로드 버전 번호'를 따로 기록해두는 게 확실합니다. 사고는 대개 밤에 나고, 그때 번호를 못 찾으면 롤백이 늦어집니다.
- 2026.07.29 Apps Script 프로젝트가 늘어나면 반드시 망가지는 것들
- 2026.07.16 매일 돌던 Apps Script 트리거가 갑자기 멈추는 이유
- 2026.07.15 브라우저 저장소에 고객정보 넣으면 생기는 문제
- 2026.07.07 브라우저가 내 데이터를 버릴 때: 초록 체크가 거짓말을 한다
- 2026.07.07 설치됨 ≠ 작동함: 백업 트리거가 한 번도 안 돌았다
- 2026.07.07 GAS엔 점진 배포가 없다: '맞는' 검사 하나가 전 사용자를 잠근다 지금 보는 글
- 2026.07.07 시트가 텍스트를 날짜로 바꾼다: '2026-06'이 완료를 영영 막을 때
- 2026.06.18 여러 사람이 한 시트에 쓰는 점검 앱: 동시성·중복·내보내기 설계
- 2026.06.12 셀을 한 칸씩 읽지 마라: 서비스 호출이 진짜 비용이다
- 2026.06.11 시트 셀에 JSON·이미지를 통째로 넣으면 터진다
- 2026.06.10 웹앱 배포 설정 하나가 보안 모델을 정한다: Execute as / 접근 권한
- 2026.06.09 GAS 6분 실행 한도: 멈추기 전에 끊어서 재개하라
- 2026.06.08 PropertiesService는 데이터베이스가 아니다: 9KB의 벽
- 2026.06.07 모바일에서 화면을 이미지로 저장할 때 멈추는 진짜 이유
- 2026.06.06 동시에 같은 시트를 쓰면 깨진다: LockService와 30 동시 실행 천장
- 2026.06.05 재배포할 때마다 주소가 바뀌면 안 된다: /exec URL 고정
- 2026.06.04 외부 호출은 가끔 실패한다: 지수 백오프와 재시도 예산
- 2026.06.03 HtmlService 앱은 껍데기 먼저: 비동기 로드와 모바일 뒤로가기
- 2026.06.02 GAS를 졸업해야 할 때: 한계 신호와 외부 DB 이전
- 2026.06.01 GAS에 지능과 엔진룸을 더하기: Vertex AI와 Cloud Run