Smart Life US

← 목록 · 2026-08-11 · 엑셀·업무 자동화

구글시트 Apps Script 오류 처리 백업 방법 | LockService·try/catch·DriveApp 백업

구글시트 Apps Script 오류 처리 백업 방법 | LockService·try/catch·DriveApp 백업

도입 – 자동화는 잘 도는데, 한 번 망가지면 끝이라면

창고나 재고 업무를 구글시트와 Apps Script로 자동화하다 보면, 어느 순간부터 “잘 돌아가기만 하면 된다”에서 “망가지지 않게 지키는 것”이 더 중요해지는 시점이 옵니다. 버튼 하나에 입고·출고 수백 줄, 재고 집계, 대시보드 갱신까지 묶여 있다면, 한 번의 오류나 동시 저장 충돌로 데이터가 망가졌을 때 그날 작업 전체를 복구하기 어렵습니다.

Apps Script 오류처리 3단계

이 글에서는 실무에서 실제로 사용했던 구글시트 Apps Script 오류 처리 백업 방법을 정리합니다. 핵심은 세 가지입니다. 첫째, try/catch로 오류를 잡아 별도 ERROR_LOG 시트에 기록합니다. 둘째, LockService로 동시 저장 충돌을 줄입니다. 셋째, DriveApp으로 자동 백업을 만들어 사람이 잘못 건드려도 되돌릴 버전을 확보합니다. 여기에 구글시트 보호 범위(UI)를 이용해 편집 권한을 나누는 방식까지 포함해, 현장에서 바로 적용할 수 있는 수준으로 정리합니다.


ERROR_LOG 시트 만들기 – Apps Script try/catch 오류 로그 시트 기록

자동화를 많이 붙이면 “버튼을 눌렀는데 아무 반응이 없습니다”라는 문의가 자주 생깁니다. 관리자 입장에서는 Apps Script 실행 기록을 보면 원인을 찾을 수 있지만, 현장에서 버튼만 누르는 사람에게 그 과정을 요구하기는 어렵습니다. 그래서 저는 오류를 별도의 ERROR_LOG 시트에 남겨, 누구나 시트만 열어도 “언제 어떤 함수가 왜 실패했는지”를 보게 하는 방식을 사용했습니다.

구조는 단순합니다. 실제 작업을 하는 함수는 그대로 두고, 그 바깥을 감싸는 공용 래퍼를 하나 둡니다. 이 래퍼에서 try 안에서 원래 함수를 실행하고, catch에서 오류를 ERROR_LOG 시트에 추가합니다. 시트에는 시간, 함수명, 메시지, 세부 내용 네 가지 컬럼을 두면 대부분의 장애 분석에 충분합니다.

이때 중요한 점 두 가지가 있습니다. 첫째, 같은 프로젝트에 다른 코드들이 들어올 수 있으므로, 설정 상수와 도우미 함수에는 접두어를 붙여 이름 충돌을 막습니다. 여기서는 WARE_ERROR_ 접두어를 사용했습니다. 둘째, 오류 메시지뿐 아니라 “어떤 상황에서 실행됐는지”를 세부 내용으로 남겨 두면 나중에 재현과 원인 파악이 훨씬 수월합니다.

아래 코드는 Apps Script에서 오류를 받아 ERROR_LOG 시트에 한 줄씩 쌓는 도우미입니다.

이 코드는 “오류를 한 줄로 ERROR_LOG 시트에 남기는 도우미”입니다.

붙여넣을 위치: 구글시트 → 확장 프로그램 → Apps Script → Code.gs 맨 아래.

붙여넣은 뒤 할 일: 저장(⌘S/CTRL+S) 후 testWareErrorLog_()를 한 번 실행해 ERROR_LOG가 생성되는지 확인합니다.

Apps Script (JavaScript)
const WARE_ERROR_CONFIG = {                          // → 이 편 전용 설정
  LOG_SHEET_NAME: 'ERROR_LOG',                      // → 오류 로그 시트 이름
  TIMEZONE: 'Asia/Seoul',                           // → 시간대 설정
};

// → 오류 로그 시트 준비(없으면 생성)
function WARE_getErrorLogSheet_() {                 // → 내부용 도우미
  const ss = SpreadsheetApp.getActiveSpreadsheet(); // → 현재 스프레드시트
  let sheet = ss.getSheetByName(WARE_ERROR_CONFIG.LOG_SHEET_NAME); // → 오류 시트 찾기
  if (!sheet) {                                     // → 없으면
    sheet = ss.insertSheet(WARE_ERROR_CONFIG.LOG_SHEET_NAME); // → 새로 만들기
    sheet.appendRow(['시간', '함수명', '메시지', '세부내용']); // → 헤더 추가
  }
  return sheet;                                     // → 시트 반환
}

// → 예외를 받아 ERROR_LOG에 한 줄 남기기
function WARE_logError_(funcName, error, detail) {  // → 함수명·예외·세부내용
  const sheet = WARE_getErrorLogSheet_();           // → 오류 로그 시트 가져오기
  const now = new Date();                           // → 현재 시각
  const time = Utilities.formatDate(                // → 문자열로 변환
    now,
    WARE_ERROR_CONFIG.TIMEZONE,
    'yyyy-MM-dd HH:mm:ss'
  );
  const message = (error && error.message) ? error.message : String(error); // → 메시지
  const detailText = detail ? String(detail) : '';  // → 세부내용 문자열
  sheet.appendRow([time, funcName, message, detailText]); // → 한 줄 추가
}

// → 이 래퍼로 감싸서 실행하면 오류가 자동 기록
function WARE_runSafely_(func, funcName, detail) {  // → 실행할 함수와 이름
  try {                                             // → 시도
    return func();                                  // → 원래 함수 실행
  } catch (e) {                                     // → 예외 발생 시
    WARE_logError_(funcName, e, detail);           // → ERROR_LOG에 기록
    throw e;                                       // → 다시 던져 사용자에게 알림
  }
}

// → 로그 기능이 제대로 동작하는지 테스트
function testWareErrorLog_() {                      // → 테스트 전용 함수
  try {                                             // → 테스트 실행
    WARE_runSafely_(function () {                   // → 래퍼로 감싸기
      throw new Error('테스트 오류입니다');         // → 강제로 오류 발생
    }, 'testWareErrorLog_', '테스트 실행');         // → 함수명·세부내용
  } catch (e) {                                     // → 예외는 무시
  }
}

편집기에서 testWareErrorLog_()를 실행한 후, 시트 목록에 ERROR_LOG가 새로 생기고 첫 번째 데이터 행이 들어가 있으면 정상입니다. 이 상태가 되면 이후 다른 함수도 WARE_runSafely_로 감싸기만 하면 자동으로 로그가 쌓입니다.


LockService 동시 저장 충돌 방지 – 잠금과 오류 로그를 함께 묶기

여러 작업자가 동시에 같은 저장 버튼을 누르는 환경에서는 동시 저장 충돌이 큰 문제입니다. 한 사용자가 재고를 읽어 합산하는 동안 다른 사용자가 그 데이터를 바꿔 버리면, 순서에 따라 결과가 달라질 수 있습니다. 이를 완전히 없앨 수는 없지만, LockService를 사용하면 “한 번에 한 사람만” 저장 함수 안으로 들어오도록 직렬화해 충돌 확률을 크게 줄일 수 있습니다.

실무에서는 저장과 집계가 1~2초 안에 끝나는 경우가 많아서, 잠금 대기 시간을 30초 정도로 두면 거의 모든 케이스를 커버할 수 있었습니다. 중요한 점은 잠금을 잡는 과정에서도 예외가 발생할 수 있다는 것입니다. 예를 들어 대기 시간 초과가 나면 그 역시 오류로 기록해야 합니다. 그래서 waitLock 호출 자체를 try 안에 넣고, catch에서 WARE_logError_를 부른 뒤 예외를 다시 던지는 구조를 씁니다.

아래 코드는 공용 잠금 래퍼입니다. 동시 저장 충돌 방지와 ERROR_LOG 기록을 한 번에 처리할 수 있도록 WARE_runSafely_를 내부에서 다시 호출합니다.

이 코드는 “잠금을 잡고, 오류 로그까지 남기면서 특정 함수를 실행하는 공용 래퍼”입니다.

붙여넣을 위치: Apps Script → Code.gs, 앞에서 넣은 오류 로그 코드 바로 아래.

붙여넣은 뒤 할 일: testWareLockedRun_()를 1~2회 실행해 ERROR_LOG에 테스트 로그가 쌓이는지 확인합니다.

Apps Script (JavaScript)
const WARE_LOCK_CONFIG = {                           // → 잠금 설정
  WAIT_SEC: 30,                                     // → 최대 대기 시간(초)
};

// → LockService로 잠금을 잡고, 오류 로그까지 묶어서 실행
function WARE_lockedRun_(func, funcName, detail) {  // → 실행할 함수와 이름
  const lock = LockService.getScriptLock();         // → 스크립트 잠금 객체
  const waitMs = WARE_LOCK_CONFIG.WAIT_SEC * 1000;  // → 밀리초로 변환
  try {                                             // → 잠금 시도부터 감싸기
    lock.waitLock(waitMs);                          // → 잠금 확보까지 대기
    return WARE_runSafely_(func, funcName, detail); // → 오류 로그 포함 실행
  } catch (e) {                                     // → 잠금·실행 중 예외
    WARE_logError_(funcName, e, detail || 'Lock 대기/실행 실패'); // → 로그 기록
    throw e;                                        // → 다시 던져 사용자에게 알림
  } finally {                                       // → 성공·실패와 무관하게
    if (lock.hasLock && lock.hasLock()) {           // → 잠금 보유 시에만
      lock.releaseLock();                           // → 잠금 해제
    }
  }
}

// → 잠금 래퍼 테스트용 함수
function testWareLockedRun_() {                     // → 테스트 함수
  try {
    WARE_lockedRun_(function () {                   // → 잠금 래퍼 호출
      Utilities.sleep(2000);                        // → 2초간 대기(작업 흉내)
      throw new Error('잠금 테스트 오류');          // → 일부러 오류 발생
    }, 'testWareLockedRun_', '테스트 실행');        // → 이름과 세부내용
  } catch (e) {                                     // → 예외는 무시
  }
}

testWareLockedRun_()를 두세 번 연속 실행한 뒤 ERROR_LOG를 보면, testWareLockedRun_ 행이 여러 줄 쌓여 있어야 합니다. 실제 입고·출고 저장 함수는 기존 함수를 그대로 두고, 아래처럼 SAFE 버전 래퍼를 하나 더 만든 다음 버튼이나 메뉴에서 이 래퍼를 호출하도록 연결합니다.

Apps Script (JavaScript)
// → 기존 saveInbound를 잠금+로그 래퍼로 감싸 실행
function saveInboundSafe() {                        // → 메뉴·버튼에서 이 함수 호출
  return WARE_lockedRun_(saveInbound, 'saveInbound', '입고 저장 버튼'); // → 이름 지정
}

이 구조는 LockService가 “동시에 저장 함수 안에 들어오는 것”을 줄여 주는 것이지, 같은 내용을 두 번 저장하는 것을 완전히 막아 주는 것은 아닙니다. 동일 입력의 이중 제출까지 막으려면, 입력값을 조합해 고유 키를 만들고 성공 처리 후에만 “최근 처리된 키”를 기록하는 추가 로직이 필요합니다. 이 부분은 업무마다 구현 방식이 달라 별도 글에서 다루는 것이 적절하며, 여기서는 충돌 방지와 오류 기록에 집중합니다.


DriveApp 자동 백업 설정 – 구글시트 사본을 매일 유지하기

LockService와 오류 로그를 붙였다고 해도, 사람이 직접 셀을 지우거나 수식을 덮어 쓰는 실수까지는 막을 수 없습니다. 또, 잘못된 로직으로 며칠간 누적된 데이터를 되돌려야 하는 상황에서는 버전 기록 메뉴만으로 원하는 시점을 고르기가 쉽지 않습니다. 그래서 저는 중요한 운영 시트에는 “매일 새 파일로 전체 사본을 남겨 두는” 구글시트 DriveApp 자동 백업 설정을 함께 사용했습니다.

방법은 단순합니다. 현재 스프레드시트의 파일 ID를 가져와 DriveApp.getFileById로 파일 객체를 얻고, makeCopy로 사본을 하나 만드는 것입니다. 사본 이름에는 [백업] 같은 접두어와 날짜를 붙여 두면 나중에 목록에서 찾기 쉽습니다. 예시로는 [백업] 재고관리 2026-08-12처럼 저장하는 형태가 관리에 무리가 없었습니다.

아래 코드는 하루 1회 실행을 전제로 하는 백업 함수입니다. 추후 필요에 따라 일주일에 한 번 등으로 바꿔도 됩니다.

이 코드는 “현재 스프레드시트를 날짜가 붙은 이름으로 Google Drive에 사본 저장”합니다.

붙여넣을 위치: Apps Script → Code.gs, 앞 코드들 아래.

붙여넣은 뒤 할 일: WARE_backupDaily()를 수동으로 한 번 실행해 권한을 허용한 뒤, 시간 기반 트리거를 설정합니다.

Apps Script (JavaScript)
const WARE_BACKUP_CONFIG = {                        // → 백업 설정
  PREFIX: '[백업] ',                                // → 파일명 앞에 붙일 말
  TIMEZONE: 'Asia/Seoul',                           // → 시간대
};

// → 현재 스프레드시트를 날짜와 함께 백업
function WARE_backupDaily() {                       // → 하루 1회 실행용
  const ss = SpreadsheetApp.getActiveSpreadsheet(); // → 현재 스프레드시트
  const file = DriveApp.getFileById(ss.getId());    // → 드라이브 파일 객체
  const baseName = ss.getName();                    // → 원본 파일명
  const today = Utilities.formatDate(               // → 오늘 날짜 문자열
    new Date(),
    WARE_BACKUP_CONFIG.TIMEZONE,
    'yyyy-MM-dd'
  );
  const backupName = WARE_BACKUP_CONFIG.PREFIX + baseName + ' ' + today; // → 백업명
  file.makeCopy(backupName);                        // → 사본 생성
}

편집기에서 WARE_backupDaily()를 한 번 실행하면 Google Drive 접근 권한 허용 화면이 나타납니다. 허용 후에는 Apps Script 편집기 오른쪽의 시계 아이콘(트리거)에서 WARE_backupDaily를 선택해 “시간 기반” → “일별” → 원하는 시각(예: 새벽 시간대)을 지정하면 됩니다. 다음날 드라이브 파일 목록에서 [백업]으로 시작하는 새 구글시트가 보이면 정상 동작입니다.


실무 팁 – 숫자 검증, 보호 범위, 실행 로그 확인 절차

코드만 붙인다고 운영이 자동으로 안정되는 것은 아닙니다. 실제로는 입력값 검증, 구글시트 보호 범위 설정, Apps Script 실행 로그 확인까지 함께 관리해야 사고를 줄일 수 있습니다. 제가 운영하면서 효과를 본 부분을 정리하면 다음과 같습니다.

첫째, 수량·금액처럼 숫자로만 의미가 있는 칸은 Apps Script에서 한 번 더 검증하는 것이 좋습니다. 예를 들어 저장 함수에서 행을 읽어 올 때는 Number(row[qtyColIndex])로 변환한 뒤 Number.isFinite(qty)로 검사합니다. 유효한 숫자가 아니면 그 행은 건너뛰고, WARE_logError_를 이용해 “몇 행 수량 오류” 같은 메시지를 남깁니다. 이렇게 하면 잘못된 입력이 전체 합계를 NaN으로 만드는 상황을 줄일 수 있습니다.

둘째, 구글시트 보호 범위 편집 권한 설정입니다. Apps Script 코드만으로도 Protect/Unprotect를 다룰 수 있지만, 저처럼 운영자가 여러 명인 환경에서는 기본 설정은 시트 UI로 관리하고, 코드에서는 주로 ERROR_LOG나 집계 시트에 대한 보호만 간단히 점검하는 정도로 두는 것이 편했습니다. 기본적인 편집 권한 나누기는 다음처럼 구성했습니다.

  • 작업자: 입·출고 요청 입력 시트, 스캔 입력 시트의 데이터 입력 구간만 편집 가능
  • 관리자: 집계 시트, 대시보드 시트, 마스터 설정 시트 편집 가능
  • ERROR_LOG 시트: 관리자만 편집 가능, 현장 리더는 읽기 전용

보호 범위 설정은 시트 탭을 우클릭해 “보호”를 선택한 뒤, 해당 범위와 편집 가능한 사용자만 지정하면 됩니다. 특히 ERROR_LOG와 백업 설정용 시트는 실수로 지우지 않도록 보호해 두는 것이 안전합니다.

셋째, Apps Script 실행 로그와 실행 기록 확인입니다. ERROR_LOG 시트만으로는 충분하지 않을 때가 있습니다. 이런 경우를 위해 테스트 함수 안에 Logger.log를 추가해 두고, 편집기 상단 메뉴의 “실행” → “실행 기록” 또는 “실행 로그”를 열어 세부 내용을 확인합니다. 절차는 다음과 같습니다.

  1. Apps Script 편집기에서 testWareErrorLog_testWareLockedRun_를 선택해 실행합니다.
  2. 상단 메뉴에서 “실행 기록”을 열어 방금 실행된 항목을 클릭합니다.
  3. 오류가 났다면 어떤 예외가 났는지, 소요 시간이 얼마였는지 확인합니다.
  4. 동시에 스프레드시트로 돌아가 ERROR_LOG 시트에 같은 시각의 로그가 쌓였는지 확인합니다.

만약 함수 실행이 전혀 되지 않는다면, 권한 허용이 되지 않았거나, 트리거가 잘못 연결되었을 가능성이 있습니다. 이때는 편집기에서 직접 함수를 실행해 권한 요청을 한 번 더 확인하고, 트리거 화면에서 대상 함수 이름과 실행 주기를 다시 점검합니다.

자주 발생하는 오류 예시는 다음과 같습니다.

  • Exception: Service invoked too many times처럼 호출 한도를 초과한 경우 → 실행 기록에서 시간대와 호출 횟수를 확인한 뒤, 트리거 주기나 데이터 범위를 조정합니다.
  • Authorization is required처럼 권한 오류가 나는 경우 → 편집기에서 해당 함수를 수동 실행해 권한 승인 과정을 다시 밟습니다.

이 과정을 정리해 두면, 현장에서 “버튼이 안 됩니다”라는 이야기가 나왔을 때 ERROR_LOG와 실행 기록, 두 군데만 확인해도 대부분의 문제를 빠르게 찾을 수 있습니다.


맺음말 – 오늘은 ERROR_LOG부터 붙이고, 내일은 백업을 예약하기

정리하면, 구글시트 Apps Script로 자동화를 어느 정도까지 확장했다면 이제는 안정성이 더 중요해집니다. try/catchERROR_LOG 시트로 오류를 눈에 보이게 만들고, LockService로 동시 저장 충돌을 줄이며, DriveApp으로 일일 자동 백업을 남기는 조합은 실무에서 무리 없이 운영할 수 있었던 수준의 방어선입니다. 여기에 숫자 검증과 보호 범위 설정, 실행 로그 확인 습관을 더하면, 운영 중 데이터가 한 번에 날아갈 위험을 상당히 줄일 수 있습니다.

바로 할 수 있는 행동 하나를 꼽자면, 지금 사용하는 구글시트 Apps Script 프로젝트에 이 글의 WARE_ERROR_CONFIG, WARE_logError_, WARE_runSafely_를 먼저 붙여 보고, 가장 자주 사용하는 저장 함수를 하나 골라 WARE_runSafely_ 또는 WARE_lockedRun_으로 감싸 보는 것입니다. ERROR_LOG 시트에 첫 번째 로그가 쌓이는 순간부터, 그 자동화는 단순히 “돌아가는 코드”에서 “문제가 생겨도 추적할 수 있는 시스템”으로 한 단계 올라가게 됩니다.