Apps Script 자동메일이 두 번 오거나 멈출 때, 트리거 계정과 발송 기록 확인
같은 보고서 메일이 두 번 왔다면 수신함을 정리하기 전에 누가 만든 트리거가 실행됐는지 봐야 해요. Apps Script의 설치형 트리거는 만든 사람의 계정으로 실행되고, 다른 사람이 만든 트리거는 내 목록에 안 보여요. 담당자가 바뀐 뒤 메일이 끊겼을 때도 파일 공유 상태만으로 원인을 판단하기 어려워요.
이 점검은 Google 시트 등의 Apps Script에서 메일을 보내는 업무에 해당해요. Gmail 필터의 자동 분류, 설문지의 기본 응답 알림, Workspace Studio의 플로우는 설정 경로와 실행 방식이 달라요. AI가 작성해 준 스크립트도 트리거와 발송 기록을 확인하는 과정은 같아요.
먼저 실제로 온 메일과 실패한 실행을 맞춰 봐요
중복 메일은 제목만 비교하면 안 돼요. 발신 주소, 수신 주소, 도착 시각, 보고서가 다루는 날짜나 접수번호를 함께 기록해요. 같은 제목으로 다음 날 보고서를 보낸 것인지, 같은 업무 한 건을 다시 보낸 것인지부터 구분해야 해요.
Apps Script 프로젝트를 열고 왼쪽 실행(Executions)에서 해당 시간대의 실행을 찾아요. 실행 함수, 시작 시각, 상태와 오류를 메일 기록 옆에 적어요. 두 메일이 왔다고 함수도 꼭 두 번 실행됐다는 뜻은 아니에요. 한 번 실행된 함수가 반복문에서 같은 주소로 두 번 보냈을 가능성도 코드에서 살펴봐요.
자동 실행 중 오류가 나면 화면에 팝업이 뜨는 대신 실패 알림 메일이 올 수 있어요. 알림의 프로젝트 링크로 들어가 실패한 실행을 열어요. 다만 애드온이 만든 설치형 트리거에는 이 실패 알림이 발송되지 않으므로, 알림이 없다는 이유로 정상이라고 판단하지 않아요.
실행이 완료됐다는 표시와 필요한 메일이 도착했다는 결과는 따로 확인해요. 발송 함수를 건너뛰는 조건, 비어 있는 수신 주소, 오류를 잡은 뒤 그대로 끝나는 코드가 있는지도 봐요. 재실행 버튼을 누르기 전에 이미 받은 사람이 있는지 확인해야 중복을 더 만들지 않아요.
트리거 한 개만 보여도 다른 계정을 확인해요
프로젝트 왼쪽 트리거(Triggers)에서 실행 함수와 이벤트 종류를 확인해요. 같은 보고서를 보내는 시간 기반 트리거가 두 개인지, 시간 기반과 수정 이벤트가 같은 함수로 연결돼 있는지 살펴봐요. 함수 이름만 같다고 모두 불필요한 것은 아니니 대상 업무와 조건도 함께 적어요.
설치형 트리거가 메일을 보내면 기본 실행 주체는 트리거를 만든 계정이에요. 시트를 연 동료의 계정으로 자동 전환되지 않아요. 팀원이 각자 같은 발송 트리거를 만들었다면 각각의 계정에서 메일이 나갈 수 있어요.
내 목록에 하나만 보이더라도 프로젝트를 편집하던 다른 담당자에게 본인 계정의 트리거를 확인해 달라고 요청해요. 다른 계정의 트리거는 내 계정에서 보이지 않지만 내가 문서를 수정하거나 여는 행동으로 작동시킬 수는 있어요. 목록의 개수와 팀 전체의 개수는 달라요.

getProjectTriggers의 조회 범위
Google Developers 문서 일부 재현 · Google · CC BY 4.0 https://creativecommons.org/licenses/by/4.0/
원문 화면 잘라냄: https://developers.google.com/apps-script/reference/script/script-app?hl=ko
코드로 점검할 때도 ScriptApp.getProjectTriggers()는 현재 프로젝트에서 현재 사용자가 만든 설치형 트리거를 돌려줘요. 결과가 하나라고 팀 전체에 하나만 있다고 단정할 수 없어요. 계정별 점검 결과를 모아 생성자, 함수, 이벤트 종류를 맞춰 봐요.
중복을 찾았으면 남길 담당 계정과 실행 조건을 정한 뒤, 불필요한 트리거의 생성자가 해당 항목을 제거하는 순서로 처리해요. 프로젝트의 모든 트리거를 한꺼번에 지우는 코드를 붙여 넣으면 다른 정상 업무까지 멈출 수 있어요. 제거 전 설정과 마지막 정상 발송 시각을 기록해 두어요.
담당자 교체는 파일 공유와 트리거 인계를 따로 해요
시트나 스크립트를 공유받았다는 사실만으로 기존 트리거의 실행 계정이 바뀌지는 않아요. 새 담당자가 앞으로 사용할 계정, 접근해야 할 시트와 파일, 메일을 보내는 함수를 확인해요. 기존 계정의 자동 발송과 새 계정의 자동 발송이 겹치지 않도록 전환 시점도 정해요.
기존 담당자가 마지막으로 처리한 업무 번호와 발송 결과를 남기고, 새 담당자는 그 다음 작업부터 이어받는 방식으로 계획해요. 날짜만 경계로 쓰면 자정을 걸친 처리나 지연 실행을 놓칠 수 있으니 실제 완료한 업무 단위로 맞춰요. 계정 접근이 불가능하면 관리자의 계정·파일 권한 확인을 먼저 요청해요.
새 트리거를 만들기 전에는 필요한 권한을 해당 생성자 계정에서 승인해야 해요. 코드 변경으로 새 서비스를 쓰게 되면 추가 권한이 필요할 수 있고, 자동 실행 중에는 승인을 묻는 창을 띄워 처리할 수 없어요. 권한 오류가 났다면 생성자 계정에서 편집기의 승인 과정을 확인해요.
이때 기존 발송 함수를 무심코 수동 실행하지 않아요. 권한을 확인하는 실행도 함수 내용에 따라 실제 메일을 보낼 수 있어요. 개발 담당자와 함께 발송을 막은 시험용 함수 또는 시험용 사본에서 필요한 서비스 접근을 확인하고, 본인 테스트 주소에만 보내도록 범위를 좁혀요.
수신자에게 보이는 이름과 실제 실행 계정도 구분해요. MailApp의 name은 발신자 표시 이름이고 replyTo는 답장받을 주소예요. 이름을 팀 이름으로 바꾸거나 답장 주소를 새 담당자로 지정하는 것만으로 트리거 생성자까지 바뀌지는 않아요.
코드가 바꾼 셀과 권한 부족을 다른 문제로 봐요
사람이 시트를 수정할 때는 메일이 오는데 자동 입력 뒤에는 오지 않는다면, 수정 이벤트를 누가 일으켰는지 봐요. 스크립트 실행이나 API 요청은 설치형 트리거를 일반적으로 발생시키지 않아요. 다른 코드가 셀을 바꿨다는 이유만으로 수정 트리거가 이어서 실행된다고 기대하면 안 돼요.
단순 onEdit 함수에 메일 발송을 넣은 경우도 점검해요. 단순 트리거는 승인이 필요한 서비스에 접근할 수 없어서 메일 발송용으로 쓸 수 없어요. 설치형 트리거와 이름이 비슷해도 권한 조건은 다르니, 이름만 onEdit로 고쳐 해결하려고 하지 않아요.
실패한 실행에 Authorization required 같은 권한 오류가 있으면 생성자의 승인 상태와 코드에 추가된 서비스를 봐요. 서비스 사용량이나 시간 제한 오류가 있으면 사용량 쪽을 확인해요. 권한을 다시 승인한다고 소진된 발송 한도가 복구되는 것은 아니에요.
MailApp.getRemainingDailyQuota()는 그 실행에서 남아 있는 일일 수신자 수를 알려줘요. 메일 제목의 개수나 프로젝트별 성공 횟수를 뜻하지 않으며, 실행이 달라지면 값도 달라질 수 있어요. 여러 수신자와 참조·숨은참조를 쓰는 업무는 단순히 보고서가 몇 통인지로 한도를 계산하지 않아요.

남은 수신자 수와 현재 실행의 관계
Google Developers 문서 일부 재현 · Google · CC BY 4.0 https://creativecommons.org/licenses/by/4.0/
원문 화면 잘라냄: https://developers.google.com/apps-script/reference/mail/mail-app?hl=ko
계정 종류에 따라 한도가 다르고 Google이 예고 없이 한도를 바꿀 수 있으므로 고정 숫자를 코드에 영구 기준으로 두지 않아요. 실패한 날짜의 사용량과 공식 한도표를 함께 확인해요. 한도 초과에 같은 작업을 계속 재시도하면 해결되지 않고 다른 업무의 실행도 늘어날 수 있어요.
같은 업무를 다시 보내지 않는 기록을 따로 남겨요
트리거를 하나로 정리해도 사람이 다시 실행하거나 두 실행이 겹치는 상황은 남아요. 개발 담당자에게는 “같은 업무 번호는 다시 실행해도 이미 발송한 것을 건너뛰도록 해 달라”고 요청해요. 이런 성질을 멱등성이라고 해요. 버튼을 한 번만 누르는 주의사항으로 대신할 수 없어요.
발송 대상마다 고정된 식별키를 정해요. 접수번호와 안내 종류, 보고서 기준일과 수신 대상 구분처럼 업무를 식별하는 조합이에요. 실행 시각을 매번 새 키로 만들면 같은 업무를 다시 실행해도 다른 작업으로 보여요. 시트 정렬로 바뀌는 행 번호만 키로 쓰는 것도 피하는 편이 안전해요.
기록에는 키, 처리 상태, 시도 시각, 완료 시각을 남기는 구성을 검토해요. 작은 업무는 PropertiesService를 저장소 후보로 볼 수 있어요.
Script Properties는 같은 스크립트의 사용자들이 공유하고 User Properties는 현재 사용자에게 나뉘어요. 계정을 바꿨을 때도 같은 완료 기록을 읽어야 한다면 저장 범위를 먼저 정해야 해요.
Properties는 서로 다른 스크립트끼리 공유되지 않아요. 시험 사본이나 다른 프로젝트에서 같은 업무를 발송한다면 각자 기록을 갖게 되므로, 한쪽의 완료 기록으로 다른 쪽까지 자동 차단된다고 생각하지 않아요. 개인정보와 메일 본문을 기록에 통째로 넣기보다 점검에 필요한 식별 정보만 남겨요.
동시에 들어온 두 실행이 모두 “미처리”를 읽는 틈도 막아야 해요. 같은 프로젝트에서 여러 사용자의 실행을 막는 범위는 LockService.getScriptLock()이에요.
잠금 객체를 얻는 것만으로 잠기지는 않고 tryLock이나 waitLock으로 실제 획득해야 해요. 획득 실패 시 발송을 진행하지 않는 분기와 마지막 해제를 개발 담당자가 함께 설계해요.
사용자별 getUserLock은 다른 사용자들의 동시 실행까지 막아 주지 않아요. 프로젝트가 다른 경우에는 스크립트 잠금도 각자의 범위에 머물러요. 기록 저장소와 잠금의 적용 범위를 다르게 두면 겉으로는 중복 방지 코드가 있어도 같은 업무가 다시 나갈 수 있어요.
발송 뒤 기록이 실패했다면 바로 재전송하지 않아요
완료 기록을 발송 전에 써 버리면 뒤의 발송 실패로 메일이 누락될 수 있어요. 발송 후에 쓰면 메일을 보냈지만 기록 저장이 실패하는 경우를 따로 다뤄야 해요. 메일 전송과 자체 완료 기록을 한 번에 확정되는 작업처럼 취급하지 않아요.
MailApp.sendEmail의 반환형은 void예요. 이 함수의 반환값에서 메일 ID나 수신자의 도착 확인을 얻는 방식으로 설계할 수는 없어요. 발송 호출이 끝났다는 기록과 실제 수신 여부를 구분하고, 결과가 애매한 작업은 별도 확인 대상으로 남겨요.

발송 기록에 따른 재실행 분기
예를 들어 접수 확인 메일을 보낸 직후 완료 기록 저장에서 오류가 났다면, 그 접수번호를 무조건 “미발송”으로 되돌리지 않아요. 실행 기록과 수신 확인 등 이용 가능한 근거를 대조해 발송 여부를 먼저 정해요. 불확실 상태의 자동 재시도를 멈추는 설계는 중복을 줄이는 대신 확인 업무가 생기는 선택이에요.
전환 전 시험에는 새 업무 한 건, 같은 키 재실행, 같은 키 동시 실행, 권한 오류, 발송 후 기록 저장 실패를 따로 넣어요. 시험용 주소에서 받은 메일 수와 완료 기록을 맞춰 보고, 불확실 작업이 자동 재전송되지 않는지 확인해요. 실무로 옮길 때는 마지막 완료 키와 새 담당자의 다음 처리 범위를 인계 기록에 남겨요.



