이 페이지에서
행사 신청자 열두 명을 한 번 내보내는 일은 쉽습니다. 두 번째 가져오기에서는 같은 이메일이 중복되고 다른 사람이 Brevo에서 수신 거부했을 수 있습니다. 반복 가능한 가져오기는 두 경우를 확인해야 합니다.
스크립트는 Brevo 연락처 조회, 생성, 갱신 엔드포인트를 사용합니다. 작은 예약 가져오기용이며 대량 로더나 기본 Google Sheets 커넥터가 아닙니다. 이메일은 발송하지 않습니다. 동의 증거와 운영은 사용자가 책임집니다.
Sheet와 목록 준비
Contacts 탭에 다음 열 이름을 정확히 만드세요. consent에는 체크박스를 사용하고 해당 마케팅 목적에 동의했을 때만 TRUE로 두세요.
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
Brevo 목록을 만들거나 선택하고 숫자 ID를 LIST_ID에 넣으세요. Brevo에 FNAME과 LNAME 속성이 있는지 확인하세요. 예제는 이 두 항목만 매핑합니다. Sheet가 동의 시간과 방식을 증명하지 못하면 별도 원본 기록을 보관하세요. TRUE 셀 하나만으로는 증거가 되지 않습니다.
Extensions → Apps Script → Project Settings → Script Properties에 BREVO_API_KEY를 추가하세요. 속성은 스크립트 사용자와 공유되므로 편집 권한을 제한하세요. 키를 셀이나 소스 코드에 넣지 마세요. Google의 접근 규칙을 참고하세요.
스크립트
Code.gs를 다음 예제로 바꾸고 첫 실행 전에 LIST_ID를 설정하세요.
const LIST_ID = 42; // Replace with your existing Brevo list ID.const TAB = 'Contacts';const BASE = 'https://api.brevo.com/v3';
function syncSheetToBrevo() { const key = PropertiesService.getScriptProperties().getProperty('BREVO_API_KEY'); if (!key) throw new Error('Set BREVO_API_KEY in Script Properties first'); const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TAB); if (!sheet) throw new Error('Contacts tab missing'); const values = sheet.getDataRange().getValues(); const headers = values.shift().map(h => String(h).trim()); const columns = ['email', 'firstName', 'lastName', 'consent'] .map(name => headers.indexOf(name)); if (columns.includes(-1)) throw new Error('Required header missing'); const [emailCol, firstCol, lastCol, consentCol] = columns; const seen = new Set(); const result = { written: 0, skipped: 0 };
for (const row of values) { const email = String(row[emailCol] || '').trim().toLowerCase(); if (row[consentCol] !== true || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || seen.has(email)) { result.skipped++; continue; } seen.add(email); const path = '/contacts/' + encodeURIComponent(email); const current = brevo_(key, path, 'get'); if (current.code !== 200 && current.code !== 404) { throw new Error(`Contact read failed (${current.code}) for row ${result.written + result.skipped + 2}`); } if (current.code === 200) { const contact = JSON.parse(current.text); if (contact.emailBlacklisted || (contact.listUnsubscribed || []).includes(LIST_ID)) { result.skipped++; continue; } } const attributes = {}; if (row[firstCol] !== '') attributes.FNAME = String(row[firstCol]); if (row[lastCol] !== '') attributes.LNAME = String(row[lastCol]); const body = { attributes, listIds: [LIST_ID] }; const saved = current.code === 404 ? brevo_(key, '/contacts', 'post', { email, ...body }) : brevo_(key, path, 'put', body); if (![201, 204].includes(saved.code)) { throw new Error(`Contact write failed (${saved.code}) for ${email}: ${saved.text.slice(0, 200)}`); } result.written++; } Logger.log(JSON.stringify(result)); return result;}
function brevo_(key, path, method, body) { for (let attempt = 0; attempt < 3; attempt++) { const response = UrlFetchApp.fetch(BASE + path, { method, contentType: 'application/json', headers: { 'api-key': key }, payload: body ? JSON.stringify(body) : undefined, muteHttpExceptions: true, }); const code = response.getResponseCode(); if (code !== 429) return { code, text: response.getContentText() }; if (attempt === 2) return { code, text: response.getContentText() }; const reset = Object.entries(response.getHeaders()).find( ([name]) => name.toLowerCase() === 'x-sib-ratelimit-reset' ); const raw = Number(reset && reset[1]); const seconds = Number.isFinite(raw) && raw > 0 ? raw : 2 ** attempt; const waitSeconds = seconds + 2 ** attempt; if (waitSeconds > 60) return { code, text: response.getContentText() }; Utilities.sleep(waitSeconds * 1000); }}저장하고 syncSheetToBrevo를 선택해 Run을 누르세요. Google이 Sheets와 외부 요청 권한을 요구합니다. Executions에서 {written, skipped} 로그를 확인하세요. 조회가 404이면 생성, 200이면 갱신합니다. 다른 오류는 실행을 중단합니다. 연락처 쓰기가 성공해도 캠페인이 발송된 것은 아닙니다.
한 번의 실행에서는 중복 이메일을 제거하고 다음 실행에서는 Brevo를 다시 조회합니다. emailBlacklisted를 false로 설정하거나 주소를 바꾸지 않습니다. Brevo가 전체 차단 또는 대상 목록 수신 거부를 보고하면 Sheet에 TRUE가 있어도 건너뜁니다. 수신 거부를 취소하는 용도로 쓰지 마세요. Brevo는 차단된 연락처의 이메일 변경이 차단을 해제할 수 있다고 경고합니다.
예약 전에 한 번 검사
직접 관리하는 테스트 목록과 연락처를 사용하세요. TRUE 행 하나, FALSE 행 하나, 중복 이메일 하나를 실행하면 쓰기 한 건과 건너뛰기 두 건이 예상됩니다. 테스트 연락처를 Brevo에서 수신 거부하고 재실행해 그 연락처에 쓰기가 없는지 보세요. 오류 테스트에는 일시적으로 잘못된 LIST_ID를 넣고 Executions의 HTTP 오류를 확인한 후 값을 복원해 다시 실행하세요. 실제 고객 데이터로 테스트하지 마세요. 예제 코드는 모의 Brevo 응답으로 로컬 검증했으며 실제 계정에 쓰지 않았습니다.
Apps Script → Triggers → Add Trigger에서 syncSheetToBrevo와 시간 간격을 선택하세요. 트리거는 생성자의 계정으로 실행됩니다. 그 계정이 Sheet 접근권을 유지해야 합니다. 작은 Sheet는 하루 한 번부터 시작하고 실패를 확인하세요. Google은 실행 시간과 URL Fetch 사용량을 제한합니다. 큰 원본은 제한된 작업으로 나누세요.
429이면 x-sib-ratelimit-reset을 읽고 최대 두 번만 재시도합니다. 필요한 대기가 60초를 넘으면 일찍 시도하지 않고 429를 반환해 다음 실행으로 넘깁니다. 다른 오류도 멈추며 다음 실행은 처음부터 시작합니다. 조회와 쓰기 사이에 수신 거부가 생길 수 있습니다. 이 짧은 경쟁 구간을 허용할 수 있을 때만 예제를 사용하세요. 더 엄격한 보호에는 이벤트 대조, 신뢰할 동의 저장소, 발송 직전의 최종 검사가 필요합니다.
큰 Sheet에는 Brevo 비동기 가져오기가 processId를 반환합니다. 이는 작업 접수이지 완료가 아닙니다. 결과를 확인하세요. 이 예제의 연락처별 처리는 실패한 행을 보여줍니다.
자주 묻는 질문
기본 Google Sheets 커넥터가 필요한가요?
아니요. Sheet에 연결한 Apps Script 프로젝트가 Brevo 연락처 API를 호출합니다. 스크립트, API 키, 예약 트리거는 직접 관리합니다.
어떤 행이 Brevo에 전송되나요?
consent가 TRUE이고 이메일이 유효한 행만 처리합니다. 매 실행마다 중복을 제거하고 쓰기 전에 Brevo를 확인합니다.
재실행하면 수신 거부한 사람이 다시 구독되나요?
Brevo가 emailBlacklisted로 표시했거나 대상 목록에서 수신 거부한 연락처는 건너뜁니다. emailBlacklisted: false를 보내거나 이메일을 변경하지 않습니다. 실제 운영 전 다른 동의 및 차단 출처도 확인하세요.
실패한 실행은 어떻게 다시 하나요?
Apps Script의 실행 오류를 읽고 원인을 해결한 뒤 같은 함수를 재실행하세요. 동일한 이메일과 목록 ID를 사용하므로 성공한 행이 두 번째 연락처를 만들지 않습니다.