지난 글에서 구글 시트에 줄이 차면 메일이 오게 만들었습니다. 이번에는 그 앞에 구글 폼을 붙였습니다. 홈페이지에 문의 폼을 걸어 두면, 고객이 제출한 것이 시트에 한 줄로 들어오고, 그 줄이 메일이 되는 흐름입니다.
지난 글에서 세 번 막혔던 자리는 이번에는 안 막혔습니다. 설치형 트리거를 처음부터 걸었더니 첫 제출에서 메일이 왔습니다. 대신 시간이 간 곳이 달랐습니다. 폼과 시트 사이 — 폼을 고치면 시트가 어떻게 되는가 — 였고, 다섯 통 중 한 통은 실행 기록이 「완료됨」인데 메일함에 없다가 15~20분쯤 뒤에 나타났습니다.
이 글은 그 순서 그대로입니다.
무엇을 만들었나
폼은 이름 · 연락처 · 문의 내용 세 칸이고 전부 필수입니다. 제출하면 응답 시트에 한 줄이 생기고, 시트에 붙은 Apps Script 가 「양식 제출 시」 트리거로 돌면서 정해진 주소로 메일 한 통을 보냅니다.
만든 순서는 이렇습니다. 순서가 중요해서 적어 둡니다.
- 폼을 만들고 질문 세 개를 넣습니다
- 폼의 「응답」 탭 → 「Sheets에 연결」 로 응답 시트를 만듭니다
- 그 시트에서 확장 프로그램 → Apps Script 를 열고 코드를 넣습니다
- 트리거를 「양식 제출 시」로 겁니다
- 폼을 「게시」 합니다
지난 글은 시트에서 시작했지만, 이번에는 시트를 제가 만들지 않았습니다. 폼이 만듭니다.
지난 글과 다른 것 셋
시트는 폼이 만듭니다
「Sheets에 연결」을 누르면 이 대화상자가 뜹니다.

「새 스프레드시트 만들기」로 두고 「만들기」를 누르면 새 탭에 시트가 열립니다. 저는 27초 걸렸습니다.
여기서 하나. 시트 이름이 「제목 없는 설문지(응답)」 으로 만들어졌습니다. 폼 안의 제목은 이미 바꿔 뒀는데, 시트 이름은 폼의 파일 이름(왼쪽 위 작은 글자)을 따릅니다. 폼 제목과 파일 이름은 따로 갑니다. 시트 이름은 나중에 바꿔도 연결이 끊기지 않았습니다.

열은 타임스탬프 | 이름 | 연락처 | 문의 내용 입니다. 첫 열은 제가 넣은 질문이 아니라 폼이 제출 시각을 적는 칸입니다. 이 시트는 지난 글의 빈 시트와 달리 「표(Form_Responses)」 형식으로 만들어졌고, 열 이름은 폼의 질문 제목 그대로입니다. 코드에서 이 이름으로 값을 읽게 되므로 질문 제목을 나중에 바꾸면 코드도 따라가야 합니다.
트리거는 「양식 제출 시」입니다
시트에서 연 Apps Script 의 트리거 대화상자입니다. 이벤트 소스를 「스프레드시트에서」로 두면 이벤트 유형이 네 개 나옵니다.

기본값은 지난 글과 같이 「열릴 시」 입니다. 「양식 제출 시」 로 바꿔야 합니다. 지난 글에서 본 대로 「수정 시」는 사람이 손으로 고칠 때만 돌고, 폼이 넣는 줄은 모릅니다.
저장하면 권한 창이 뜹니다. 항목이 지난 글보다 하나 많습니다.

See, edit, create, and delete all your Google Sheets spreadsheets
View and manage your forms in Google Drive
Send email as you
「양식 보기·관리」 가 새로 붙었습니다. 코드에 폼을 다루는 줄은 한 줄도 없는데, 「양식 제출 시」 트리거를 거는 것만으로 이 항목이 들어옵니다. 이 계정은 회사 워크스페이스 계정이라 「확인되지 않은 앱」 경고는 이번에도 나오지 않았습니다.
값은 e.values 로 한 번에 옵니다
검색하면 가장 먼저 나오는 형태의 코드를 그대로 넣었습니다.
var TO = 'xxxx@xxxx.xx';
function onFormSubmit(e) {
var v = e.values; // [타임스탬프, 이름, 연락처, 문의 내용]
MailApp.sendEmail(TO,
'[문의 접수] ' + v[1],
'접수 시각: ' + v[0] + '\n이름: ' + v[1] + '\n연락처: ' + v[2] + '\n내용: ' + v[3]
+ '\n\n' + e.range.getSheet().getParent().getUrl());
}
지난 글에서는 「수정 시」가 칸 단위로 발동해서 한 줄에 메일이 세 통 왔고, 그걸 막으려고 D열에 보낸 표시를 남겼습니다. 폼은 줄 전체가 한 번에 들어오고 e.values 에 그 줄이 배열로 담겨 옵니다. 세 통 문제가 없고, 보낸 표시도 필요 없었습니다.
타임스탬프도 지난 글과 다르게 옵니다. getValues() 가 날짜 객체를 주던 것과 달리 e.values[0] 은 이미 글자입니다.
2026. 9. 14 오후 9:35:36
그리고 함수 이름 onFormSubmit 은 onEdit 과 달리 구글이 알아서 부르는 이름이 아닙니다. 지난 글에서 이름이 onEdit 이라 한 번의 편집에 두 번 돌던 일이 이번에는 없었습니다. 실행 기록에 제출 한 건당 한 줄만 찍혔습니다.
게시하지 않으면 응답을 받지 않습니다
응답 탭에는 만들자마자 이렇게 적혀 있었습니다.
응답이 없습니다. 응답을 수락하려면 양식을 게시하세요.
오른쪽 위 「게시」 버튼을 누르면 「게시 양식」 대화상자가 뜹니다. 응답자 기본값은 「링크가 있는 모든 사용자에게 공개」 였고, 그 아래에 회사 이름으로 「(으)로 제한」 하는 선택지가 있었습니다. 홈페이지 문의로 쓰려면 기본값 그대로여야 합니다.
게시하면 버튼이 「게시됨」으로 바뀌고, 응답자 링크를 복사할 수 있습니다. 편집 주소와 다른 주소입니다 — /forms/d/e/…/viewform 형태이고, 고객에게 주는 것은 이쪽입니다.
응답자 화면에서 하나 더 봤습니다. 로그인하지 않은 브라우저로 열었더니 폼 맨 아래에 「이 설문지는 ○○○ 내부에서 생성되었습니다」 라고 회사 이름이 찍혀 있었습니다. 워크스페이스 계정으로 만든 폼은 고객에게 이 문구가 보입니다. 감출 방법은 찾아보지 않았습니다.
첫 제출 — 4~5초
로그인하지 않은 별도 브라우저에서 응답자 링크를 열고 세 칸을 채워 「제출」을 눌렀습니다.
| 시각 | |
|---|---|
| 「제출」 클릭 | 21:35:32.7 |
| 시트 타임스탬프 | 21:35:36 |
| 실행 시작 (실행 기록) | 21:35:37 · 1.77초 |
| 메일 도착 (메일함 표시) | 21:35 |
메일이 왔습니다. 코드는 한 글자도 안 고쳤고, 편집기의 「실행」 버튼은 한 번도 안 눌렀습니다. 지난 글의 첫 에러 Cannot read properties of undefined (reading 'source') 는 「실행」 버튼을 누를 때 나는 것이라, 폼에서 직접 제출하면 나올 일이 없습니다.
이후 네 번 더 제출했습니다. 클릭에서 실행 시작까지 3~5초, 실행 자체는 1.1~1.9초, 코드 안에서 sendEmail 이 돌아온 시각까지 재면 클릭에서 4~5초였습니다.
막힌 지점 1 — 폼 중간에 칸을 넣었더니 시트에서는 맨 끝에 붙습니다
문의 폼은 운영하다 보면 고칩니다. 그래서 「이름」 다음에 「회사명」 을 끼워 넣어 봤습니다.
먼저, 게시된 폼을 고치면 재게시 없이 즉시 반영됐습니다. 응답자 링크를 다시 열었더니 회사명 칸이 이미 보였습니다.
그리고 제출했더니 시트가 이렇게 됐습니다.

폼에서는 두 번째 칸인데 시트에서는 E열, 맨 끝입니다. 기존 열은 자리를 지켰고 새 열만 뒤에 붙었습니다. 실행 로그에 찍힌 e.values 원문입니다.
row=5.0 values=["2026. 9. 14 오후 10:22:45","이지연","010-5555-6666","namedValues 버전 첫 제출","사아자상사"]
위 코드는 이 상황에서도 안 깨집니다. v[1] 은 여전히 이름이니까요. 대신 회사명은 메일에 안 실립니다. 반대로 「이름」 앞에 칸을 넣었다면 어떻게 되는지는 시험하지 못했습니다. 이 계정에서 새 열이 늘 끝에 붙는 것만 확인했습니다.
그래서 번호 대신 열 이름으로 읽게 바꿨습니다. e.namedValues 는 { '이름': ['홍길동'], '연락처': ['010-…'], … } 형태라 열 순서와 무관합니다.
var TO = 'xxxx@xxxx.xx';
// '양식 제출 시' 설치형 트리거용
function onFormSubmit(e) {
var nv = e.namedValues; // 열 이름으로 읽습니다 — 열 순서가 바뀌어도 그대로
var get = function (k) { return (nv[k] && nv[k][0]) ? nv[k][0] : ''; };
var row = e.range.getRow();
var subject = '[문의 접수] ' + get('이름') + (get('회사명') ? ' (' + get('회사명') + ')' : '');
var body = '접수 시각: ' + get('타임스탬프')
+ '\n이름: ' + get('이름')
+ '\n회사명: ' + get('회사명')
+ '\n연락처: ' + get('연락처')
+ '\n내용: ' + get('문의 내용')
+ '\n\n' + e.range.getSheet().getParent().getUrl()
+ '#gid=' + e.range.getSheet().getSheetId() + '&range=A' + row;
Logger.log('row=%s values=%s quota=%s', row, JSON.stringify(e.values), MailApp.getRemainingDailyQuota());
MailApp.sendEmail(TO, subject, body);
Logger.log('sent row=%s at %s', row, new Date().toISOString());
}
회사명을 비우고 제출하면 e.values 의 그 자리에는 빈 문자열이 옵니다. 필수가 아닌 칸은 늘 이렇게 오므로 get() 처럼 빈 값을 받아 주는 쪽이 낫습니다.
하나 더. 시험 중에 「첨부파일」 질문을 잠깐 넣었다가 지웠는데, 시트의 열은 그대로 남았습니다. 폼에서 질문을 지워도 시트 열은 안 지워집니다. 열을 지우려면 시트에서 직접 지워야 하는데, 그러면 이후 제출이 어느 열로 들어오는지는 확인하지 못했습니다.
막힌 지점 2 — 실행 기록은 「완료됨」인데 메일이 없습니다
회사명 칸을 넣고 처음 제출한 건 — 다섯 건 중 두 번째 — 입니다. 시트에는 줄이 들어왔고, 실행 기록에는 이렇게 찍혔습니다.
onFormSubmit 실행 2026. 9. 14. 오후 10:11:55 1.751초 완료됨
그런데 메일함에 없었습니다. 받은편지함, 스팸함, 전체보관함을 다 봤습니다. 3분 뒤에도, 8분 뒤에도, 13분 뒤에도 없었습니다. 그 사이에 다시 제출한 세 번째 건은 45초 안에 왔습니다.
그리고 21분 뒤에 다시 열었더니 있었습니다. 메일 목록에는 보낸 시각(오후 10:11) 으로 찍혀 있어서, 목록만 보면 늦게 온 티가 나지 않습니다.
원인은 모릅니다. 코드도, 트리거도, 받는 주소도 앞뒤 건과 같습니다. 이 건만 이랬고, 다섯 건 중 한 건입니다. 지난 글에서 「실행 기록은 완료됨인데 메일이 없는 상태가 제일 찾기 어렵다」고 적었는데, 이번에는 코드 탓이 아닌 경우도 있다는 것을 봤습니다.
그래서 이 흐름에서 원본은 시트이고 메일은 알림입니다. 메일이 안 왔다고 문의가 안 들어온 것은 아니라서, 하루에 한 번은 시트를 직접 보는 편이 안전합니다.
문의 접수로 쓸 때 걸리는 것
5~50인 회사가 홈페이지 문의를 이 폼으로 받는다고 놓고, 실제로 부딪힌 것만 적습니다.
첨부파일을 못 받습니다. 질문 유형을 「파일 업로드」로 바꾸면 이 안내가 뜹니다.

파일이 설문지 소유자의 Google Drive에 업로드됩니다.
파일 업로드 질문이 추가된 설문지에 응답하려면 사용자가 Google에 로그인해야 합니다.
신뢰하는 사용자에게만 이 설문지를 공유해야 합니다.
고객에게 구글 로그인을 요구하게 됩니다. 견적서나 도면을 받아야 하는 문의라면 이 폼으로는 어렵고, 저는 「계속」을 누르지 않고 취소했습니다.
같은 사람이 몇 번이든 보낼 수 있습니다. 설정 → 응답에 「응답 횟수 1회로 제한」이 있는데, 「로그인 필요」 아래에 있습니다. 로그인 없이 받는 폼에서는 못 씁니다. 이번 시험에서 로그인 없는 브라우저로 다섯 번 제출했고 다섯 번 다 들어왔습니다.
스팸 방지 장치가 없습니다. 폼에는 자동입력 방지 문자 같은 것이 없고, 링크가 공개되면 누구나 제출할 수 있습니다. 다만 이번 시험에서 실제로 스팸을 받지는 않았으므로, 얼마나 들어오는지는 말 못 합니다.
메일 한도가 있습니다. 실행 로그에 남은 한도를 찍어 봤더니 이 계정은 1,440~1,455통이었습니다(워크스페이스 계정). 하루 수십 건 문의라면 문제 없고, 수백 건이면 지난 글에 적은 대로 메일이 아닌 채널로 가야 합니다.
회사 이름이 보입니다. 위에 적은 대로, 로그인하지 않은 고객의 폼 화면 아래에 조직 이름이 찍힙니다.
요약
| 시트를 먼저 만들지 마십시오 | 폼의 「응답」 탭 → 「Sheets에 연결」 이 시트를 만듭니다 |
| 시트 이름이 「제목 없는 설문지(응답)」 | 폼의 파일 이름을 따릅니다. 폼 안의 제목과 별개입니다 |
| 트리거 기본값 「열릴 시」 | 「양식 제출 시」 로 바꾸십시오. 「수정 시」는 폼이 넣는 줄을 모릅니다 |
| 권한 창에 「양식 보기·관리」 | 「양식 제출 시」 트리거를 걸면 붙습니다. 코드와 무관합니다 |
e.values |
줄 전체가 배열로 옵니다. 타임스탬프는 글자입니다 |
| 「응답을 수락하려면 양식을 게시하세요」 | 「게시」 를 눌러야 받습니다. 응답자 링크는 /d/e/…/viewform |
| 폼 중간에 칸 추가 | 시트에서는 맨 끝 열에 붙습니다. e.namedValues 로 이름으로 읽으십시오 |
| 폼에서 질문 삭제 | 시트 열은 남습니다 |
| 「완료됨」인데 메일 없음 | 한 건이 13~21분 뒤에 왔습니다. 원인 모름. 시트가 원본입니다 |
| 파일 업로드 | 고객에게 구글 로그인을 요구합니다 |
| 응답 1회 제한 | 「로그인 필요」 아래에 있습니다 |
코드는 지난 글보다 짧게 끝났습니다. 시간이 간 곳은 코드가 아니라 폼과 시트 사이의 약속 — 누가 시트를 만들고, 열이 어디에 붙고, 언제 받기 시작하는가 — 였습니다.
참고
– Google Apps Script 공식 문서 — 이벤트 객체 (Event Objects: Google Sheets form submit — values, namedValues, range)
– Google Apps Script 공식 문서 — 설치형 트리거 (Installable Triggers)
– Google Forms 고객센터 — 파일 업로드 질문 · 양식 게시