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

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

Featured image for article: 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입니다. 더 큰 작업의 경우 행을 일괄 처리하세요.
Brevo로 무료로 시작하기