구글 앱스 스크립트 onEdit 오류 해결: 이벤트 객체·수정 범위·중복 실행 확인

구글 스프레드시트에서 Apps Script의 onEdit(e)를 작성했지만 셀을 수정해도 실행되지 않거나, 한 번 수정했는데 같은 작업이 여러 번 실행되는 경우가 있습니다.

이 문제는 모두 같은 원인으로 발생하지 않습니다. 함수 이름이나 이벤트 객체가 잘못된 경우, 수정 범위를 정확히 제한하지 않은 경우, 단순 트리거와 설치형 트리거가 중복 등록된 경우를 나눠 확인해야 합니다.

특히 onEdit 안에서 setValue()를 사용하면 수정 이벤트가 다시 발생해 무한 반복된다는 설명은 일반적인 Apps Script 동작과 맞지 않습니다. 스크립트 실행이나 API 요청으로 셀 값을 바꿔도 스프레드시트의 onEdit 트리거는 다시 실행되지 않습니다.

이 글은 Google의 Apps Script 단순 트리거, 설치 가능한 트리거, 이벤트 객체와 Spreadsheet Range 공식 문서를 기준으로 작성했습니다. 예제의 시트 이름과 열 번호는 실제 문서 구조에 맞게 변경해야 합니다.

구글 스프레드시트 onEdit 이벤트가 수정된 셀 범위와 실행 조건을 확인하는 대표 개념 이미지
onEdit는 사용자의 셀 값 수정으로 실행되며 이벤트 객체의 range를 기준으로 처리할 시트·행·열을 제한해야 합니다. 위 이미지는 onEdit 오류 진단 구조를 설명하기 위한 대표 개념 이미지입니다.


onEdit는 사용자가 셀 값을 수정할 때 실행됩니다

단순 수정 트리거는 스프레드시트에 연결된 Apps Script 프로젝트에서 예약된 함수 이름인 onEdit를 사용하면 별도 트리거 등록 없이 실행됩니다.

function onEdit(e) {
  console.log(e.range.getA1Notation());
}

사용자가 셀 값을 입력하거나 수정하면 Apps Script가 이벤트 객체 e를 함수에 전달합니다. 이벤트 객체의 range에는 실제로 수정된 셀 또는 셀 범위가 들어 있습니다.

onEdit가 실행되는 기본 조건

스크립트가 해당 스프레드시트에 연결돼 있어야 합니다.
사용자가 수정 권한으로 셀 값을 변경해야 합니다.
함수 이름이 정확히 onEdit여야 합니다.
단순 트리거를 사용할 때는 별도의 트리거 등록이 필요하지 않습니다.

setValue가 onEdit를 다시 실행해 무한 반복되는 것은 아닙니다

다음 코드는 A열을 수정하면 같은 행의 B열에 시간을 기록합니다.

function onEdit(e) {
  const range = e.range;

  if (range.getColumn() !== 1) {
    return;
  }

  range
    .getSheet()
    .getRange(range.getRow(), 2)
    .setValue(new Date());
}

B열에 값을 기록하는 setValue()는 스크립트가 실행한 변경이므로 새로운 onEdit 이벤트를 발생시키지 않습니다.

같은 작업이 반복된다면 다음 원인을 먼저 확인해야 합니다.

  • 단순 onEdit(e)와 설치형 수정 트리거가 모두 실행 중임
  • 동일한 설치형 트리거가 두 번 이상 등록됨
  • 다른 사용자의 계정에서도 같은 설치형 트리거를 등록함
  • 함수 안에서 처리 함수를 직접 다시 호출함
  • 여러 사용자가 짧은 시간에 같은 범위를 연속으로 수정함
직접 재귀 호출은 별도 문제입니다

함수 안에서 다시 onEdit(e)를 호출하거나 처리 함수가 자기 자신을 조건 없이 호출하면 실제 재귀가 발생할 수 있습니다. 이것은 셀 쓰기가 트리거를 다시 실행한 것이 아니라 코드가 함수를 직접 다시 호출한 것입니다.

실행 버튼으로 onEdit를 실행하면 이벤트 객체가 없습니다

Apps Script 편집기에서 onEdit를 선택하고 실행 버튼을 누르면 실제 스프레드시트 수정 이벤트가 발생하지 않습니다.

따라서 Apps Script가 이벤트 객체 e를 전달하지 않으며 다음과 같은 코드에서 오류가 발생할 수 있습니다.

function onEdit(e) {
  const range = e.range;
}

이 코드는 스프레드시트에서 실제 셀 값을 수정할 때 실행해야 합니다. 이벤트 없이 직접 실행되는 상황을 구분하려면 다음처럼 확인 문구를 추가할 수 있습니다.

function onEdit(e) {
  if (!e || !e.range) {
    throw new Error(
      'onEdit(e)는 스프레드시트의 셀 수정 이벤트로 실행해야 합니다.'
    );
  }

  const range = e.range;

  console.log(range.getA1Notation());
}

이 검사는 이벤트 객체 없이 함수를 실행했을 때 원인을 알기 어려운 일반적인 속성 오류 대신 구체적인 설명을 표시합니다.

getActiveSheet보다 e.range의 시트를 기준으로 처리합니다

onEdit에서 어느 시트가 수정됐는지 확인할 때는 이벤트 객체가 전달한 범위를 직접 사용하는 편이 처리 대상을 명확하게 보여줍니다.

활성 시트를 기준으로 작성한 코드:

function onEdit(e) {
  const sheet =
    SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

  sheet.getRange('D2').setValue(new Date());
}

수정된 범위의 시트를 직접 사용하는 코드:

function onEdit(e) {
  if (!e || !e.range) {
    throw new Error(
      '수정 이벤트 정보가 없습니다.'
    );
  }

  const range = e.range;
  const sheet = range.getSheet();

  sheet.getRange('D2').setValue(new Date());
}

두 번째 구조는 어떤 셀의 수정으로 함수가 실행됐는지를 이벤트 객체에서 바로 확인합니다.

시트·행·열 조건을 먼저 확인하고 종료합니다

onEdit 안에서 매번 전체 시트를 읽거나 모든 업무 로직을 실행하면 관련 없는 셀을 수정했을 때도 같은 코드가 실행됩니다.

처리할 시트와 열이 정해져 있다면 함수 앞부분에서 조건을 확인하고 대상이 아니면 바로 종료합니다.

다음 코드는 “업무” 시트의 C열에서 상태가 “완료”로 바뀌면 같은 행의 D열에 시간을 기록합니다.

const ON_EDIT_CONFIG = Object.freeze({
  sheetName: '업무',
  headerRows: 1,
  watchedColumn: 3,
  outputColumn: 4,
  completedValue: '완료'
});

function onEdit(e) {
  processStatusEdit_(e);
}

function processStatusEdit_(e) {
  if (!e || !e.range) {
    throw new Error(
      'onEdit(e)는 스프레드시트의 셀 수정 이벤트로 실행해야 합니다.'
    );
  }

  const editedRange = e.range;
  const sheet = editedRange.getSheet();

  if (
    sheet.getName() !==
    ON_EDIT_CONFIG.sheetName
  ) {
    return;
  }

  const firstEditedColumn =
    editedRange.getColumn();

  const lastEditedColumn =
    editedRange.getLastColumn();

  if (
    ON_EDIT_CONFIG.watchedColumn <
    firstEditedColumn
  ) {
    return;
  }

  if (
    ON_EDIT_CONFIG.watchedColumn >
    lastEditedColumn
  ) {
    return;
  }

  const firstDataRow =
    Math.max(
      editedRange.getRow(),
      ON_EDIT_CONFIG.headerRows + 1
    );

  const lastEditedRow =
    editedRange.getLastRow();

  if (firstDataRow > lastEditedRow) {
    return;
  }

  const rowCount =
    lastEditedRow - firstDataRow + 1;

  const statusValues =
    sheet
      .getRange(
        firstDataRow,
        ON_EDIT_CONFIG.watchedColumn,
        rowCount,
        1
      )
      .getValues();

  const editedAt =
    new Date();

  const outputValues =
    statusValues.map(function (row) {
      if (
        row[0] ===
        ON_EDIT_CONFIG.completedValue
      ) {
        return [editedAt];
      }

      return [''];
    });

  sheet
    .getRange(
      firstDataRow,
      ON_EDIT_CONFIG.outputColumn,
      rowCount,
      1
    )
    .setValues(outputValues);
}

이 코드의 처리 순서는 다음과 같습니다.

  1. 이벤트 객체와 수정 범위가 있는지 확인합니다.
  2. 수정된 시트가 “업무”인지 확인합니다.
  3. 수정 범위에 C열이 포함돼 있는지 확인합니다.
  4. 머리글을 제외한 실제 데이터 행만 계산합니다.
  5. C열 상태를 한 번에 읽습니다.
  6. 같은 크기의 2차원 배열을 만들어 D열에 한 번에 기록합니다.

한 셀 수정뿐 아니라 C열의 여러 행을 복사해 붙여 넣은 경우도 처리할 수 있도록 작성했습니다.

e.value와 e.oldValue는 단일 셀 수정에서만 사용합니다

수정 이벤트 객체에는 새 값인 e.value와 이전 값인 e.oldValue가 포함될 수 있습니다.

하지만 두 속성은 수정 범위가 단일 셀인 경우에만 사용할 수 있습니다. 여러 셀을 한 번에 붙여 넣거나 삭제하면 값이 제공되지 않을 수 있습니다.

단일 셀 수정만 허용하는 코드라면 먼저 범위 크기를 확인합니다.

function onEdit(e) {
  if (!e || !e.range) {
    return;
  }

  const range = e.range;

  if (range.getNumRows() !== 1) {
    return;
  }

  if (range.getNumColumns() !== 1) {
    return;
  }

  console.log('이전 값: ' + String(e.oldValue));
  console.log('새 값: ' + String(e.value));
}

여러 셀 수정까지 처리하려면 e.value에 의존하지 말고 e.range.getValues() 또는 필요한 실제 범위를 다시 읽는 방식을 사용합니다.

단순 onEdit와 설치형 수정 트리거 중 하나를 선택합니다

단순 onEdit(e)는 함수 이름만 맞으면 자동으로 실행되지만, 사용자 승인이 필요한 서비스에는 접근할 수 없습니다.

구분 단순 onEdit 설치형 수정 트리거
등록 방법 함수 이름을 onEdit로 작성 Apps Script 트리거 화면에서 등록
승인 서비스 사용할 수 없음 승인된 범위 안에서 사용 가능
다른 파일 접근 승인 제한 때문에 사용할 수 없음 트리거 생성 계정의 권한으로 접근
실행 계정 보안 제한에 따라 사용자 정보가 제한될 수 있음 트리거를 만든 계정

같은 스프레드시트 안에서 셀 값을 읽고 쓰는 정도라면 단순 onEdit로 처리할 수 있습니다.

이메일 발송, 다른 스프레드시트 접근 등 승인이 필요한 서비스가 포함된다면 설치형 수정 트리거를 검토합니다.

설치형 트리거에서는 onEdit와 다른 함수 이름을 사용합니다

설치형 수정 트리거를 사용하려면 onEdit와 다른 함수 이름을 사용하는 편이 중복 등록 여부를 구분하기 쉽습니다.

function handleStatusEdit(e) {
  processStatusEdit_(e);
}

설치형 트리거는 다음 순서로 등록합니다.

  1. 스프레드시트에서 확장 프로그램 → Apps Script를 엽니다.
  2. 왼쪽에서 트리거를 선택합니다.
  3. 트리거 추가를 선택합니다.
  4. 실행할 함수로 handleStatusEdit를 선택합니다.
  5. 이벤트 소스에서 스프레드시트에서를 선택합니다.
  6. 이벤트 유형에서 수정 시를 선택합니다.
  7. 저장한 뒤 필요한 권한을 승인합니다.
같은 처리 로직을 두 번 연결하지 마세요

설치형 handleStatusEdit를 등록했다면 같은 작업을 수행하는 단순 onEdit(e)를 함께 유지하지 않습니다. 두 진입점이 같은 처리 함수를 호출하면 사용자 수정 한 번에 로직이 두 번 실행될 수 있습니다.

여러 번 실행될 때 트리거 목록을 확인합니다

같은 수정에 대해 기록이 두 번 생기거나 알림이 두 번 전송된다면 Apps Script 왼쪽의 트리거 화면을 확인합니다.

다음 항목을 확인합니다.

  • 같은 함수의 수정 트리거가 두 개 이상 등록돼 있는지
  • 이전 함수명과 새 함수명이 모두 같은 처리 함수를 호출하는지
  • 단순 onEdit와 설치형 트리거를 동시에 사용하고 있는지
  • 다른 공동 작업자 계정에서도 설치형 트리거를 만들었는지

설치형 트리거는 이를 만든 계정의 권한으로 실행됩니다. 다른 사용자가 자신의 계정으로 만든 트리거는 현재 계정의 트리거 목록에 보이지 않을 수 있습니다.

여러 계정에서 같은 알림 트리거를 만들면 각 계정의 트리거가 별도로 실행될 수 있으므로 어떤 계정이 트리거를 소유할지 정하는 편이 좋습니다.

셀 값 수정과 시트 구조 변경은 다른 이벤트입니다

onEdit는 사용자가 셀 값을 수정할 때 실행됩니다. 행·열 또는 시트를 추가하거나 삭제하는 구조 변경을 처리하려면 설치형 변경 트리거를 사용해야 합니다.

사용자 작업 검토할 트리거
셀에 값 입력·수정 단순 또는 설치형 수정 트리거
행이나 열 추가·삭제 설치형 변경 트리거
새 시트 추가·시트 삭제 설치형 변경 트리거
양식 응답 제출 설치형 양식 제출 트리거

구조 변경을 onEdit로 처리하려고 하면 셀 값 수정이 없기 때문에 기대한 시점에 함수가 실행되지 않을 수 있습니다.

빠른 연속 수정에서는 모든 이벤트가 무제한으로 대기하지 않습니다

Google 공식 문서에 따르면 단순 onEdit는 최대 두 개의 이벤트만 대기열에 추가합니다. 사용자가 짧은 시간에 여러 셀을 계속 수정하는 구조라면 모든 수정 이벤트가 무제한으로 쌓인다고 가정하면 안 됩니다.

단순 트리거는 30초를 넘겨 실행할 수도 없습니다. 따라서 onEdit 안에서 관련 없는 전체 시트를 반복해서 읽거나, 행마다 셀을 한 개씩 쓰거나, 긴 외부 작업을 수행하는 구조는 피하는 편이 적절합니다.

앞의 최종 예제는 다음 방식으로 실행 범위를 줄였습니다.

  • 대상 시트가 아니면 즉시 종료
  • 수정 범위에 감시 열이 없으면 즉시 종료
  • 머리글 행은 제외
  • 여러 행의 값을 getValues()로 한 번에 읽기
  • 결과를 setValues()로 한 번에 쓰기

여러 실행이 같은 공유값을 동시에 읽고 수정하는 문제는 onEdit 범위 조건만으로 해결되지 않습니다. 공용 번호나 마지막 행을 함께 수정한다면 LockService와 잠금 범위를 별도로 확인해야 합니다.

onEdit 오류 유형별 확인 위치

e.range를 읽을 수 없다는 오류
Apps Script 편집기의 실행 버튼으로 onEdit를 직접 실행했는지 확인합니다. 실제 셀 수정 이벤트에는 e.range가 전달됩니다.
특정 시트에서만 작동하지 않음
e.range.getSheet().getName()과 코드에 입력한 시트 이름이 정확히 같은지 확인합니다.
여러 셀을 붙여 넣으면 값이 undefined로 표시됨
e.value와 e.oldValue는 단일 셀 수정에서만 제공됩니다. e.range와 getValues를 기준으로 처리합니다.
같은 기록이나 알림이 두 번 생성됨
단순 onEdit와 설치형 수정 트리거가 함께 있는지, 동일한 설치형 트리거가 중복 등록됐는지 확인합니다.
setValue 때문에 무한 반복된다고 의심됨
Apps Script의 셀 쓰기는 onEdit를 다시 실행하지 않습니다. 함수의 직접 재귀 호출과 중복 트리거를 확인합니다.
행·열을 추가해도 실행되지 않음
셀 수정 트리거가 아니라 설치형 변경 트리거가 필요한 작업인지 확인합니다.
이메일이나 다른 파일 접근에서 권한 오류 발생
단순 onEdit의 승인 제한에 해당하는지 확인하고 설치형 수정 트리거를 검토합니다.
빠른 연속 수정 중 일부 작업이 누락됨
단순 onEdit의 대기열과 실행 시간 제한을 확인하고 처리 범위를 줄이거나 여러 셀을 한 번에 읽고 씁니다.

내용 확인에 사용한 Google 공식 자료

공식 문서 확인일: 2026년 7월 29일

Post a Comment

다음 이전