Apps Script의 이벤트 객체 오류는 함수 실행 방식부터 확인하면 원인을 좁히기 쉽습니다.
Google Apps Script에서 e is undefined 또는 Cannot read properties of undefined 오류가 나오면 먼저 함수가 어떻게 실행됐는지 확인해야 합니다. e는 함수 안에서 항상 자동으로 만들어지는 값이 아니라, 특정 트리거나 웹 요청으로 실행될 때 전달되는 이벤트 객체입니다.
특히 onEdit(e)를 스크립트 편집기에서 직접 실행했거나, doPost(e)를 실제 HTTP 요청 없이 실행했거나, 시간 기반 트리거에서 e.range를 읽으려 하면 같은 형태의 오류가 날 수 있습니다. 직접 테스트할 때는 실제 이벤트 대신 테스트용 mock event object를 만들어 실행하는 방식이 안전합니다.
📑목차[보기]
- 1) 오류 메시지 원문
- 2) e is undefined 오류가 나는 대표 상황
- 3) 빠른 결론: e는 언제 전달될까
- 4) onEdit, doGet, doPost, 시간 기반 트리거 차이
- 5) onEdit(e)에서 e.range 오류가 날 때
- 6) doPost(e)에서 e.parameter 또는 e.postData가 undefined일 때
- 7) 시간 기반 트리거에서 e 객체를 기대하면 안 되는 이유
- 8) 테스트용 mock event object 코드 예시
- 9) 오류를 줄이는 안전한 분기 처리 코드
- 10) 재발 방지 체크리스트
- 11) 공식 자료로 더 확인하기
- 12) 함께 보면 좋은 글
- 13) 자주 묻는 질문
오류 메시지 원문
이 오류는 코드에서 e.range, e.parameter, e.postData, e.namedValues 같은 값을 읽으려 했지만, 정작 e 자체가 전달되지 않았거나 해당 속성이 없는 실행 방식에서 호출됐을 때 자주 나타납니다.
TypeError: Cannot read properties of undefined
Cannot read properties of undefined (reading 'range')
Cannot read properties of undefined (reading 'parameter')
e is undefined오류 문구만 보면 range나 parameter가 문제처럼 보이지만, 실제 원인은 실행 방식이 맞지 않는 경우가 많습니다. 따라서 변수명부터 고치기보다 “이 함수가 어떤 이벤트로 실행됐는지”를 먼저 확인해야 합니다.
e is undefined 오류가 나는 대표 상황
e 오류는 문법 오류라기보다 실행 환경 차이에서 생기는 경우가 많습니다. 같은 함수라도 시트에서 셀을 수정해 실행했는지, 편집기에서 실행 버튼을 눌렀는지, 웹앱 URL로 요청했는지에 따라 전달되는 값이 달라질 수 있습니다.
onEdit(e)를 Google Sheets에서 셀을 수정하지 않고 스크립트 편집기에서 직접 실행한 경우doPost(e)를 외부 요청 없이 편집기에서 직접 실행한 경우- 시간 기반 트리거에서
e.range처럼 시트 편집 이벤트 전용 값을 읽으려 한 경우 - Google Form 제출 이벤트와 Google Sheets 편집 이벤트를 혼동한 경우
- Web App의
e.parameter와 JSON 요청의e.postData.contents를 구분하지 않은 경우
먼저 볼 기준
e가 비어 있다면 코드 첫 줄에서 값을 억지로 읽기보다 실행 방식을 먼저 확인하는 편이 좋습니다. 수동 실행, 단순 트리거, 설치형 트리거, 웹앱 요청은 전달되는 이벤트 객체의 구조가 서로 다를 수 있습니다.
빠른 결론: e는 언제 전달될까
e는 함수가 특정 이벤트에 의해 호출될 때 Apps Script가 전달하는 객체입니다. 예를 들어 스프레드시트에서 사용자가 셀을 수정하면 onEdit(e)에 편집된 범위 정보가 들어올 수 있고, 웹앱으로 GET 또는 POST 요청이 들어오면 doGet(e), doPost(e)에 요청 정보가 들어올 수 있습니다.
반대로 스크립트 편집기에서 함수 이름을 선택한 뒤 실행 버튼을 누르면 실제 시트 편집이나 웹 요청이 발생한 것이 아닙니다. 이때는 e가 자동으로 채워지지 않으므로 테스트용 객체를 직접 만들어 함수에 넘겨야 합니다.
function onEdit(e) {
if (!e) {
Logger.log('이 함수는 시트 편집 이벤트로 실행해야 합니다.');
return;
}
Logger.log(e.range.getA1Notation());
}onEdit, doGet, doPost, 시간 기반 트리거 차이
이벤트 객체 오류를 줄이려면 함수 이름보다 실행 조건을 먼저 구분해야 합니다. onEdit(e)는 시트 편집, doGet(e)과 doPost(e)는 웹 요청, 시간 기반 트리거는 정해진 시간 실행에 가깝습니다.
| 함수·트리거 | 주요 실행 조건 | 자주 쓰는 e 속성 |
|---|---|---|
onEdit(e) |
스프레드시트 셀 값이 수정될 때 | e.range, e.value, e.oldValue |
doGet(e) |
웹앱 URL로 GET 요청이 들어올 때 | e.parameter, e.parameters |
doPost(e) |
웹앱 URL로 POST 요청이 들어올 때 | e.parameter, e.postData |
| 시간 기반 트리거 | 정해진 시간이나 간격에 따라 실행될 때 | 시트 편집 범위 정보는 기대하기 어려움 |
| 폼 제출 트리거 | Google Form 또는 연결된 Sheet에 제출이 들어올 때 | e.namedValues, e.values 등 실행 위치에 따라 다름 |
트리거 자체가 실행되지 않는 문제라면 이벤트 객체보다 트리거 설정, 권한 승인, 실행 계정을 먼저 봐야 합니다. 자동 실행이 안 되는 경우에는 하단의 트리거 오류 관련 글로 이어서 확인하면 원인을 더 빠르게 좁힐 수 있습니다.
onEdit(e)에서 e.range 오류가 날 때
Cannot read properties of undefined (reading 'range')는 onEdit(e)를 편집기에서 직접 실행했을 때 자주 나옵니다. 실제로 셀을 수정해서 실행된 것이 아니라면 Apps Script가 편집된 셀 정보를 만들 수 없기 때문입니다.
직접 실행한 경우
편집기 상단의 실행 버튼으로 onEdit를 호출하면 e가 전달되지 않을 수 있습니다. 이때 e.range를 바로 읽으면 오류가 납니다. 실제 테스트는 스프레드시트에서 셀을 수정하거나, mock object를 만들어 함수에 전달하는 방식으로 진행합니다.
시트 편집 이벤트가 아닌 경우
시간 기반 트리거나 메뉴 실행 함수 안에서 onEdit(e)의 코드를 재사용할 때도 같은 문제가 생길 수 있습니다. 시트 범위가 필요하다면 SpreadsheetApp.getActiveSheet()나 명시적인 시트·범위 지정 방식으로 별도 함수를 분리하는 편이 좋습니다.
function onEdit(e) {
if (!e || !e.range) {
Logger.log('onEdit 이벤트 객체가 없습니다. 시트에서 셀을 직접 수정해 테스트하세요.');
return;
}
const range = e.range;
const sheet = range.getSheet();
Logger.log('수정된 시트: ' + sheet.getName());
Logger.log('수정된 셀: ' + range.getA1Notation());
}e.range가 아니라 getSheetByName() 결과가 null이라서 이어서 오류가 나는 경우도 있습니다. 시트 이름과 범위 지정 문제가 의심된다면 하단의 getRange 오류 관련 글로 이어서 확인하면 좋습니다.
doPost(e)에서 e.parameter 또는 e.postData가 undefined일 때
doPost(e)는 웹앱으로 POST 요청이 들어올 때 실행되는 함수입니다. 편집기에서 직접 실행하면 실제 요청 본문이 없으므로 e.parameter나 e.postData가 기대한 형태로 들어오지 않을 수 있습니다.
폼 데이터와 JSON 요청을 구분한다
e.parameter는 URL 쿼리나 일반 폼 파라미터를 다룰 때 주로 확인합니다. 반면 JSON 본문을 보내는 요청이라면 e.postData.contents를 읽고 JSON.parse()로 변환하는 흐름이 필요합니다.
function doPost(e) {
if (!e) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: 'event object is missing' }))
.setMimeType(ContentService.MimeType.JSON);
}
Logger.log(JSON.stringify(e, null, 2));
const name = e.parameter ? e.parameter.name : '';
return ContentService
.createTextOutput(JSON.stringify({ ok: true, name: name }))
.setMimeType(ContentService.MimeType.JSON);
}JSON 요청을 받는 doPost 예시
외부 서비스나 자동화 도구에서 JSON을 보내는 구조라면 아래처럼 요청 본문을 먼저 확인한 뒤 파싱하는 방식이 좋습니다. 요청 본문이 없을 수 있으므로 e.postData 존재 여부를 함께 확인합니다.
function doPost(e) {
try {
if (!e || !e.postData || !e.postData.contents) {
return jsonResponse({
ok: false,
error: 'POST body is missing'
});
}
const body = JSON.parse(e.postData.contents);
return jsonResponse({
ok: true,
receivedName: body.name || '',
receivedEmail: body.email || ''
});
} catch (err) {
return jsonResponse({
ok: false,
error: err.message
});
}
}
function jsonResponse(data) {
return ContentService
.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}시간 기반 트리거에서 e 객체를 기대하면 안 되는 이유
시간 기반 트리거는 “몇 시에 실행할지” 또는 “몇 분마다 실행할지”를 기준으로 함수를 호출합니다. 사용자가 어떤 셀을 수정해서 실행되는 구조가 아니므로 e.range 같은 시트 편집 정보가 들어온다고 기대하면 오류가 날 수 있습니다.
시간 기반 자동화에서 시트 데이터를 읽어야 한다면 이벤트 객체 대신 직접 스프레드시트와 시트를 지정하는 방식이 안정적입니다. 예를 들어 특정 시트의 전체 데이터를 주기적으로 처리하려면 SpreadsheetApp.openById(), getSheetByName(), getDataRange()처럼 실행 시점에 필요한 범위를 직접 가져오는 흐름이 맞습니다.
function runByTimeTrigger() {
const spreadsheetId = '스프레드시트_ID';
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('Sheet1');
if (!sheet) {
Logger.log('Sheet1 시트를 찾을 수 없습니다.');
return;
}
const values = sheet.getDataRange().getValues();
Logger.log(values.length + '행을 확인했습니다.');
}테스트용 mock event object 코드 예시
편집기에서 직접 테스트해야 한다면 실제 onEdit(e)나 doPost(e) 함수를 바로 실행하지 말고, 테스트용 함수를 따로 만들어 mock object를 넘기는 편이 좋습니다. 이렇게 하면 실행 버튼으로 테스트하면서도 실제 이벤트 객체와 비슷한 구조를 재현할 수 있습니다.
onEdit 테스트용 mock object
onEdit(e) 테스트에서는 실제 Range 객체가 필요합니다. 테스트 함수에서 시트와 셀을 직접 가져온 뒤 range 속성에 넣어 호출하면 됩니다.
function testOnEdit() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
if (!sheet) {
Logger.log('Sheet1 시트를 찾을 수 없습니다.');
return;
}
const mockEvent = {
range: sheet.getRange('A1'),
value: '테스트 값',
oldValue: '',
source: SpreadsheetApp.getActiveSpreadsheet()
};
onEdit(mockEvent);
}
function onEdit(e) {
if (!e || !e.range) {
Logger.log('onEdit 이벤트 객체가 없습니다.');
return;
}
Logger.log('테스트 셀: ' + e.range.getA1Notation());
Logger.log('새 값: ' + e.value);
}doPost 테스트용 mock object
doPost(e)는 실제 웹 요청이 없어도 아래처럼 parameter 또는 postData.contents 구조를 만들어 테스트할 수 있습니다.
function testDoPostWithParameter() {
const mockEvent = {
parameter: {
name: 'Kim',
email: 'kim@example.com'
}
};
const result = doPost(mockEvent);
Logger.log(result.getContent());
}
function testDoPostWithJson() {
const mockEvent = {
postData: {
type: 'application/json',
contents: JSON.stringify({
name: 'Kim',
email: 'kim@example.com'
})
}
};
const result = doPost(mockEvent);
Logger.log(result.getContent());
}오류를 줄이는 안전한 분기 처리 코드
Apps Script 자동화는 실행 경로가 여러 개로 나뉘기 쉽습니다. 같은 함수가 수동 실행, 트리거 실행, 웹 요청으로 호출될 수 있다면 시작 부분에서 필요한 값이 있는지 확인하고, 없으면 로그를 남긴 뒤 종료하는 구조가 좋습니다.
Logger.log로 e 객체 구조 확인하기
처음부터 속성명을 추측하지 말고, 들어온 e 구조를 로그로 확인하면 오류를 줄일 수 있습니다. 다만 개인정보나 민감한 요청 값이 포함될 수 있는 자동화라면 로그에 남길 항목을 제한하는 편이 좋습니다.
function inspectEventObject(e) {
if (!e) {
Logger.log('event object is undefined');
return;
}
Logger.log(JSON.stringify(e, null, 2));
}실행 방식별로 함수 분리하기
트리거 함수는 얇게 두고, 실제 처리 로직은 별도 함수로 분리하면 테스트가 쉬워집니다. onEdit(e)에서는 편집된 범위만 추출하고, 공통 처리 함수에는 필요한 값만 넘기는 방식입니다.
function onEdit(e) {
if (!e || !e.range) {
Logger.log('시트 편집 이벤트가 아니므로 종료합니다.');
return;
}
handleEditedCell(e.range, e.value);
}
function handleEditedCell(range, value) {
const sheetName = range.getSheet().getName();
const cell = range.getA1Notation();
Logger.log(sheetName + '!' + cell + ' = ' + value);
}재발 방지 체크리스트
e is undefined 오류는 한 번 고쳐도 함수 실행 방식을 바꾸면 다시 나타날 수 있습니다. 아래 기준으로 함수별 이벤트 객체를 분리해두면 재발 가능성을 줄일 수 있습니다.
onEdit(e)는 시트에서 셀을 수정해 테스트한다.- 편집기 실행 테스트가 필요하면 mock object를 만든다.
doPost(e)는 실제 POST 요청 또는 mock object로 테스트한다.e.parameter와e.postData.contents를 요청 형식에 따라 구분한다.- 시간 기반 트리거에서는
e.range를 기대하지 않는다. - 폼 제출 이벤트와 시트 편집 이벤트의 속성명을 혼동하지 않는다.
- 함수 첫 줄에서
e와 필요한 속성 존재 여부를 확인한다. - 공식 문서의 Event Objects 표에서 트리거별 속성을 확인한다.
공식 자료로 더 확인하기
Apps Script의 이벤트 객체는 트리거 종류와 실행 방식에 따라 달라질 수 있습니다. 오류가 반복된다면 코드 예시만 복사하기보다 Google 공식 문서에서 해당 트리거가 어떤 이벤트 객체를 전달하는지 확인하는 것이 좋습니다.
트리거가 실행될 때 전달되는 이벤트 객체의 구조와 속성을 확인할 수 있습니다. e.range, e.namedValues, e.parameter처럼 트리거별로 달라지는 값을 비교할 때 가장 먼저 볼 자료입니다.
onEdit(e), onOpen(e), doGet(e), doPost(e) 같은 단순 트리거와 실행 조건을 확인할 수 있습니다. 수동 실행과 트리거 실행을 구분할 때 도움이 됩니다.
설치형 트리거를 만들고 관리하는 방법을 확인할 수 있습니다. 시간 기반 트리거, 시트 편집 트리거, 폼 제출 트리거처럼 자동 실행 구조를 점검할 때 참고할 수 있습니다.
Apps Script 설치형 트리거 공식 문서 확인하기
doGet(e), doPost(e)로 웹앱을 배포하고 요청 파라미터를 받는 방식을 확인할 수 있습니다. e.parameter와 웹 요청 흐름을 점검할 때 유용합니다.
Apps Script에서 JSON, 텍스트, XML 형태의 응답을 반환하는 방법을 확인할 수 있습니다. doPost(e)에서 JSON 응답을 만들 때 함께 보면 좋습니다.
함께 보면 좋은 글
e.range 문제가 아니라 시트 이름이 틀렸거나 getSheetByName() 결과가 비어 있는 상황일 수 있습니다.
자주 묻는 질문
Google Apps Script의 e is undefined 오류는 함수 이름보다 실행 방식을 먼저 확인하고, 수동 테스트에는 mock event object를 사용하면 원인을 더 안정적으로 좁힐 수 있습니다.
댓글