Google Apps Script로 시트의 연락처를 Brevo에 가져오는 방법

Google Sheet의 연락처를 자동으로 Brevo에 푸시합니다. API 키 저장, 편집 시 실행, 시간 기반 트리거, 사용자 정의 메뉴, 오류 처리가 포함된 완전한 Apps Script. 서버가 필요 없습니다.

google apps script brevo
Google Apps Script로 시트의 연락처를 Brevo에 가져오는 방법?

팀이 이미 Google Sheet에서 살고 있다면(영업 리드, 이벤트 등록, 파트너 연락처 리스트), 그 데이터를 Brevo로 가져오는 것은 CSV를 내보내고 손으로 다시 가져오는 작업이 필요하지 않습니다. Google Apps Script는 시트를 Brevo의 API에 직접 연결할 수 있게 합니다. 스크립트는 Google의 인프라 내부에서 실행되므로 호스팅하거나 배포하거나 돌볼 것이 없습니다.

이 가이드는 작동하는 스크립트를 살펴봅니다. 시트의 사용자 정의 Sync to Brevo 메뉴 항목, 자동 시간별 트리거, 안전한 API 키 저장, 배치 처리, 그리고 무엇이 일어났는지 알 수 있도록 약간의 구조화된 로깅입니다.

필요한 것

  • 연락처가 있는 Google Sheet (행당 하나의 연락처, 헤더 행 먼저)
  • Brevo 계정과 API 키 (Settings → SMTP & API → API Keys)
  • 연락처를 추가할 Brevo 리스트의 숫자 ID

그게 전부입니다. npm 없음, Python 없음, 서버 없음.

시트 레이아웃

이 가이드의 스크립트는 헤더 행 다음에 행당 연락처 하나를 기대합니다. 열은 헤더 이름으로 Brevo 속성에 매핑됩니다.

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email은 필수이며 대소문자를 구분하지 않고 일치합니다. 다른 모든 것은 연락처 속성으로 Brevo에 전송됩니다. 사용자 정의 속성(표준의 FIRSTNAME, LASTNAME 외의 모든 것)은 Brevo 계정에 먼저 존재해야 합니다. Contacts → Settings → Contact attributes 아래에서 정의하거나 Brevo API를 통해 정의하세요.

Apps Script 편집기 열기

시트에서 Extensions → Apps Script 를 선택합니다. 빈 Code.gs로 새 탭이 열립니다. 내용을 아래 스크립트로 교체합니다.

전체 스크립트

Code.gs
const BREVO_API_BASE = 'https://api.brevo.com/v3';
const BREVO_LIST_ID = 42; // <- the Brevo list to import contacts into
const SHEET_NAME = 'Contacts'; // <- name of the sheet tab to read
const BATCH_SIZE = 1000; // contacts per import call
/**
* Adds a "Brevo" menu to the Sheet so users can run the sync from the UI.
* Triggered automatically when the Sheet opens.
*/
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Brevo')
.addItem('Sync sheet to Brevo', 'syncSheetToBrevo')
.addItem('Configure API key', 'configureApiKey')
.addToUi();
}
/**
* Reads every contact row from the sheet, batches them, and sends each batch
* to Brevo's import endpoint. Returns a summary string for logging.
*/
function syncSheetToBrevo() {
const apiKey = getApiKey_();
if (!apiKey) {
SpreadsheetApp.getUi().alert(
'No Brevo API key configured. Run "Configure API key" first.'
);
return;
}
const contacts = readContactsFromSheet_();
if (contacts.length === 0) {
SpreadsheetApp.getUi().alert('No contacts found in the sheet.');
return;
}
const batches = chunk_(contacts, BATCH_SIZE);
const results = [];
for (let i = 0; i < batches.length; i++) {
const result = importBatchToBrevo_(apiKey, batches[i]);
results.push(result);
Logger.log(
`Batch ${i + 1}/${batches.length}: ${result.ok ? 'ok' : 'FAILED'} ` +
`(processId=${result.processId || '-'}, status=${result.status})`
);
}
const summary =
`Sent ${contacts.length} contacts in ${batches.length} batch(es). ` +
`Successful: ${results.filter(r => r.ok).length}/${results.length}.`;
Logger.log(summary);
SpreadsheetApp.getActiveSpreadsheet().toast(summary, 'Brevo sync', 5);
return summary;
}
/**
* Reads the active spreadsheet's "Contacts" tab into an array of
* { email, attributes } objects shaped for Brevo's jsonBody.
*/
function readContactsFromSheet_() {
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName(SHEET_NAME);
if (!sheet) {
throw new Error(`Sheet tab "${SHEET_NAME}" not found`);
}
const range = sheet.getDataRange().getValues();
if (range.length < 2) return [];
const headers = range[0].map(String);
const emailColumn = headers.findIndex(h => h.toLowerCase() === 'email');
if (emailColumn === -1) {
throw new Error('Sheet must have an "email" column');
}
const contacts = [];
for (let i = 1; i < range.length; i++) {
const row = range[i];
const email = String(row[emailColumn] || '').trim().toLowerCase();
if (!email || !email.includes('@')) continue; // skip invalid
const attributes = {};
for (let c = 0; c < headers.length; c++) {
if (c === emailColumn) continue;
const value = row[c];
if (value === '' || value === null) continue;
// Brevo convention: ATTRIBUTES ARE UPPERCASE
attributes[headers[c].toUpperCase()] = value;
}
contacts.push({ email, attributes });
}
return contacts;
}
/**
* POSTs a batch of contacts to Brevo's import endpoint.
* Returns { ok, status, processId, error }.
*/
function importBatchToBrevo_(apiKey, contacts) {
const payload = {
jsonBody: contacts,
listIds: [BREVO_LIST_ID],
updateExistingContacts: true,
emptyContactsAttributes: false,
};
const response = UrlFetchApp.fetch(`${BREVO_API_BASE}/contacts/import`, {
method: 'post',
contentType: 'application/json',
headers: {
'api-key': apiKey,
'accept': 'application/json',
},
payload: JSON.stringify(payload),
muteHttpExceptions: true, // we'll inspect status ourselves
});
const status = response.getResponseCode();
const body = response.getContentText();
if (status === 202) {
const json = JSON.parse(body);
return { ok: true, status, processId: json.processId };
}
return { ok: false, status, error: body };
}
/**
* Stores the Brevo API key in script properties - encrypted at rest by Google
* and not visible in the source code or to viewers of the sheet.
*/
function configureApiKey() {
const ui = SpreadsheetApp.getUi();
const response = ui.prompt(
'Brevo API key',
'Paste your Brevo API key (xkeysib-...). It will be stored in Script Properties.',
ui.ButtonSet.OK_CANCEL
);
if (response.getSelectedButton() !== ui.Button.OK) return;
const key = response.getResponseText().trim();
if (!key.startsWith('xkeysib-')) {
ui.alert('That doesn\'t look like a Brevo API key (should start with xkeysib-).');
return;
}
PropertiesService.getScriptProperties().setProperty('BREVO_API_KEY', key);
ui.alert('API key saved.');
}
function getApiKey_() {
return PropertiesService.getScriptProperties().getProperty('BREVO_API_KEY');
}
function chunk_(arr, size) {
const out = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}

전체가 그것입니다. 저장하고 (⌘S / Ctrl+S), 프로젝트 이름을 “Brevo sync” 같은 것으로 지정하고, 시트로 돌아갑니다.

첫 실행

시트를 다시 로드합니다. 새로운 Brevo 메뉴가 상단에 나타납니다.

  1. Brevo → Configure API key 를 클릭하고 xkeysib-... 키를 붙여넣고 OK를 클릭합니다.
  2. Brevo → Sync sheet to Brevo 를 클릭합니다. Google이 처음으로 권한을 요청합니다.
    • “스프레드시트 보기 및 관리”: 행을 읽는 데 필요
    • “외부 서비스에 연결”: api.brevo.com을 호출하는 데 필요
  3. 승인합니다. 스크립트가 실행됩니다. 오른쪽 하단의 녹색 토스트가 얼마나 많은 연락처가 나갔는지 알려줍니다.

실패하면 Extensions → Apps Script → View → Logs 를 클릭하여 Brevo의 배치별 상태 코드를 확인하세요. 가장 일반적인 실패는 누락된 사용자 정의 속성으로 인한 400입니다. 아래의 문제 해결 섹션을 참조하세요.

스케줄로 실행

Apps Script 편집기에서: Triggers (왼쪽 사이드바의 시계 아이콘) → Add Trigger.

  • 함수 선택: syncSheetToBrevo
  • 이벤트 소스: 시간 기반(Time-driven)
  • 유형: 시간 타이머(또는 하루에 한 번 동기화하는 일 타이머)
  • 간격: 매시간(또는 적절한 것)

저장합니다. Google은 그 빈도로 영원히 함수를 실행하며, 서버 없음, cron 없음, 유지 관리 없음입니다.

모든 셀 변경이 동기화를 트리거하길 원한다면 From spreadsheet → On edit 를 사용할 수도 있습니다. 그것에 주의하세요. 외관상의 편집조차 트리거를 발동시키며, 바쁜 시트에서 Apps Script의 일일 할당량에 빠르게 도달할 수 있습니다. 시간별 시간 트리거가 거의 항상 정답입니다.

알아둘 Apps Script 할당량

무료 Apps Script 계층에는 존중할 가치가 있는 제한이 있습니다.

제한값 (무료 계층)
하루 총 실행 시간90분
단일 실행 시간6분
하루 UrlFetchApp 호출20,000
UrlFetchApp 페이로드 크기50 MB
UrlFetchApp 헤더 크기8 KB
사용자별 스크립트별 트리거20

일반적인 연락처 동기화의 경우(몇 천 개의 연락처, 시간별), 이 중 어느 것에도 가깝지 않습니다. 주의해야 할 유일한 것은 6분 단일 실행 입니다. 한 번에 수십만 개의 연락처를 동기화하는 경우, 더 작은 청크로 일괄 처리하세요(위 스크립트는 이미 BATCH_SIZE를 통해 이를 수행합니다).

가져오기 비동기적으로 처리

Brevo의 가져오기 엔드포인트는 비동기적입니다. 즉시 processId를 받고, 실제 가져오기는 서버 측에서 실행됩니다. 대부분의 시트 동기화의 경우 이것으로 충분합니다. 발사하고 잊으면 되며, 각 배치가 완료될 때 Brevo가 요약을 이메일로 보냅니다.

가져오기가 정말로 완료될 때까지 블로킹 하고 싶다면, 프로세스 상태 엔드포인트를 폴링하세요.

function waitForImport_(apiKey, processId, timeoutMs = 5 * 60 * 1000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const resp = UrlFetchApp.fetch(`${BREVO_API_BASE}/processes/${processId}`, {
headers: { 'api-key': apiKey },
muteHttpExceptions: true,
});
if (resp.getResponseCode() === 200) {
const status = JSON.parse(resp.getContentText()).status;
if (status === 'completed' || status === 'failed') return status;
}
Utilities.sleep(5000); // 5s between checks
}
return 'timeout';
}

Utilities.sleep은 블로킹 대기의 Apps Script 동등 솔루션입니다. 너무 오래 자지 마세요. 실행당 총 6분이 있습니다.

알림 웹훅 추가

폴링보다 깔끔한 패턴: Apps Script를 Web App 으로 배포하고 그 URL을 notifyUrl로 전달합니다. 가져오기가 완료되면 Brevo가 거기로 POST합니다.

// Add to Code.gs
function doPost(e) {
const payload = JSON.parse(e.postData.contents);
Logger.log(`Brevo import ${payload.processId} finished: ${payload.status}`);
// optionally: write the result back to a "Sync log" tab in the sheet
return ContentService.createTextOutput('ok');
}

배포: Deploy → New deployment → Web app, “Who has access”를 Anyone 으로 설정하고, 결과 URL을 복사하고, 가져오기 페이로드에서 notifyUrl로 전달합니다.

payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';

이제 Brevo는 결과를 시트의 자체 스크립트로 다시 게시합니다. 외부 인프라 없이 루프를 닫습니다.

문제 해결

400 Bad Request with error: "Attribute X not found": 시트의 열이 Brevo가 모르는 속성에 매핑됩니다. 시트 열 이름을 기존 속성과 일치하도록 변경하거나 Brevo에서 속성을 만드세요(Contacts → Settings → Contact attributes).

401 Unauthorized: API 키가 잘못되었거나 만료되었습니다. Configure API key 를 다시 실행하고 Brevo의 대시보드에서 새 키를 붙여넣으세요.

429 Too Many Requests: Brevo의 속도 제한에 도달했습니다. 가져오기 엔드포인트는 분당 약 30회 호출을 허용합니다. 적극적으로 일괄 처리하는 경우 루프의 배치 사이에 Utilities.sleep(2000)을 추가하세요.

스크립트가 스케줄로 조용히 실행되지 않습니다: Apps Script 편집기에서 Triggers 를 확인하세요. 트리거가 반복적으로 실패하면 Google이 비활성화합니다. 트리거를 클릭하여 실패 이유를 확인하세요. 보통 다시 권한을 부여할 수 있는 권한 문제입니다.

Brevo 메뉴가 나타나지 않음: onOpen은 시트를 처음부터 (다시) 열 때만 실행됩니다. 브라우저 탭을 다시 로드하세요.

권한 팝업이 계속 다시 나타남: 스크립트의 범위를 편집했을 가능성이 있습니다(새 Google 서비스 추가). Apps Script는 필요한 권한이 변경될 때마다 권한 부여를 다시 요청합니다. 편집기에서 함수를 한 번 실행하여 프롬프트를 트리거하고 승인하세요.

이것이 Zapier와 친구들을 이기는 이유

Apps Script는 무료이고, Google의 인프라 내부에 살며, 시트의 데이터에 직접 접근할 수 있습니다. 행별 이벤트 발생 없음, 작업당 가격 없음, Google의 할당량 외의 속도 제한 없음(이런 종류의 작업에는 충분합니다). 단점: 작은 코드 조각을 작성하고 유지하는 데 전념하고 있습니다. 연락처 동기화의 경우 약 100줄이며 기본적으로 진행 중인 유지 관리가 없습니다.

이것을 일별 트리거와 영업팀이 이미 업데이트하는 시트와 짝지으면, 반복 작업이 없는 Brevo로의 연락처 파이프라인이 있습니다.

더 읽기

Frequently Asked Questions

Google Sheet를 Brevo에 동기화하는 데 서버나 인프라가 필요합니까?
아니요. Google Apps Script는 Google Sheets 자체 내부에서 실행됩니다. 함수를 작성하고, 프로젝트를 저장하고, Google이 호스팅하고 실행합니다. 스크립트는 UrlFetchApp을 사용하여 Brevo의 API에 직접 도달할 수 있습니다.
Brevo API 키는 어디에 저장합니까?
PropertiesService.getScriptProperties()를 사용하세요. Apps Script 프로젝트로 범위가 지정된 Google이 관리하는 키/값 저장소입니다. 소스에 키를 하드코딩하지 마세요. 시트의 협업자가 그것을 볼 수 있게 됩니다.
이것을 매일 자동으로 실행하려면 어떻게 합니까?
Apps Script → Triggers → Add Trigger를 엽니다. syncSheetToBrevo 함수를 선택하고, '시간 기반(Time-driven)'을 선택하고, 일별/시간별 빈도를 설정하세요. Google의 무료 계층 할당량은 하루 90분의 총 Apps Script 실행 시간이며, 연락처 동기화에는 충분합니다.
행 제한이 있습니까?
Brevo의 가져오기 엔드포인트는 약 10MB의 인라인 JSON을 받아들입니다. 각 연락처에 얼마나 많은 속성이 있는지에 따라 약 30,000-50,000개의 연락처입니다. Apps Script의 UrlFetchApp은 50MB 페이로드를 보낼 수 있으므로 병목은 Apps Script가 아니라 Brevo입니다. 더 큰 작업의 경우 행을 일괄 처리하세요.

Subscribe to updates

blog-updates

Drop your email or phone number — we'll send you what matters next.

auto-detect
Brevo 받기