이번 실습에서는 외부 웹사이트에서 고객 리드 데이터를 수집하고, Google Apps Script를 API 엔드포인트로 활용하여 데이터를 수신한 뒤 Google Sheets에 자동으로 정리·저장하는 시스템을 구축합니다.
전체 흐름: 외부 웹사이트(HTML 폼) → fetch()로 POST 전송 → Google Apps Script(doPost) → Google Sheets 저장
사용 도구: Google Apps Script, Google Sheets API, Google Cloud Platform
- 프로젝트 생성:
- Google Cloud Console에 접속하여 새로운 프로젝트를 생성합니다.
- 프로젝트 이름 예시:
Web Lead Collection
-
API 활성화:
- 생성한 프로젝트에서 **'API 및 서비스' > '라이브러리'**로 이동합니다.
- 검색창에
Google Sheets API를 입력하고 나타나는 결과를 선택한 후 **'사용 설정'**을 클릭합니다.
-
OAuth 동의 화면 설정:
- **'API 및 서비스' > 'OAuth 동의 화면'**으로 이동합니다.
- 사용자 유형을 **'외부'**로 선택하고, 앱 이름과 필수 정보를 입력합니다.
- 범위(Scopes)에서
https://www.googleapis.com/auth/spreadsheets를 추가합니다.
-
스프레드시트 생성:
- Google Sheets에 접속하여 새로운 스프레드시트를 만듭니다.
- 예시:
고객 리드 관리
-
시트 구성:
- 첫 번째 행에 다음 헤더를 추가합니다:
A B C D E F G 수집일시 이름 이메일 전화번호 회사명 유입경로 관심제품 -
스프레드시트 ID 확인:
- 스프레드시트 URL에서 ID를 확인합니다.
- 예:
https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit에서SPREADSHEET_ID부분이 ID입니다.
-
프로젝트 생성:
- Google Apps Script에 접속하여 **'새 프로젝트'**를 클릭합니다.
- 프로젝트 이름 예시:
Lead Collection Web App
-
Google Cloud 프로젝트 연결:
- Apps Script 편집기에서 '프로젝트 설정'(톱니바퀴 아이콘)을 클릭합니다.
- 'Google Cloud Platform(GCP) 프로젝트' 섹션에서 **'프로젝트 변경'**을 클릭하고, 앞서 생성한 GCP 프로젝트 번호를 입력하여 연결합니다.
-
appsscript.json 수정:
- 편집기 왼쪽의 **'프로젝트 설정'**에서 **'appsscript.json 매니페스트 파일을 편집기에 표시'**를 체크합니다.
appsscript.json파일을 열어 다음과 같이 수정합니다:
{ "timeZone": "Asia/Seoul", "dependencies": {}, "exceptionLogging": "STACKDRIVER", "webapp": { "executeAs": "USER_DEPLOYING", "access": "ANYONE_ANONYMOUS" }, "oauthScopes": [ "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/script.external_request" ] }
외부 웹사이트에서 전송한 리드 데이터를 수신하여 Google Sheets에 저장하는 API 엔드포인트를 작성합니다.
Code.gs 파일에 다음과 같이 작성합니다:
// 스프레드시트 ID (본인의 스프레드시트 ID로 교체)
var SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
var SHEET_NAME = 'Sheet1';
/**
* 외부 웹사이트에서 POST 요청으로 전송된 리드 데이터를 수신하는 함수
* @param {Object} e - POST 요청 이벤트 객체
* @returns {TextOutput} - JSON 응답
*/
function doPost(e) {
try {
var leadData = JSON.parse(e.postData.contents);
// 필수 필드 검증
if (!leadData.name || !leadData.email || !leadData.phone) {
return ContentService.createTextOutput(
JSON.stringify({ status: 'error', message: '필수 필드(name, email, phone)가 누락되었습니다.' })
).setMimeType(ContentService.MimeType.JSON);
}
// Google Sheets에 데이터 저장
var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
var timestamp = Utilities.formatDate(new Date(), 'Asia/Seoul', 'yyyy-MM-dd HH:mm:ss');
sheet.appendRow([
timestamp,
leadData.name,
leadData.email,
leadData.phone,
leadData.company || '',
leadData.source || '',
leadData.product || ''
]);
Logger.log('리드 데이터 저장 완료: ' + leadData.name);
return ContentService.createTextOutput(
JSON.stringify({ status: 'success', message: '리드 데이터가 저장되었습니다.' })
).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
Logger.log('doPost 오류: ' + error.message);
return ContentService.createTextOutput(
JSON.stringify({ status: 'error', message: error.message })
).setMimeType(ContentService.MimeType.JSON);
}
}
/**
* GET 요청 시 API 상태를 확인하는 함수
*/
function doGet() {
return ContentService.createTextOutput(
JSON.stringify({ status: 'ok', message: 'Lead Collection API is running.' })
).setMimeType(ContentService.MimeType.JSON);
}
YOUR_SPREADSHEET_ID를 앞서 확인한 실제 스프레드시트 ID로 교체해야 합니다.
-
배포 설정:
- Apps Script 편집기에서 **'배포' > '새 배포'**를 클릭합니다.
- 왼쪽 톱니바퀴 아이콘에서 **'웹 앱'**을 선택합니다.
-
배포 옵션 설정:
- 설명:
고객 리드 수집 API v1 - 실행 사용자:
나(본인 계정) - 액세스 권한:
모든 사용자(외부 웹사이트에서 접근해야 하므로)
- 설명:
-
배포 완료:
- '배포' 버튼을 클릭합니다.
- 권한 요청 창이 나타나면 지시에 따라 권한을 승인합니다.
- 생성된 웹 앱 URL을 복사합니다. (예:
https://script.google.com/macros/s/AKfycbx.../exec) - 이 URL이 외부 웹사이트에서 데이터를 전송할 API 엔드포인트가 됩니다.
-
API 동작 확인:
- 브라우저에서 웹 앱 URL에 접속하면 다음과 같은 JSON 응답이 표시됩니다:
{ "status": "ok", "message": "Lead Collection API is running." }
외부 웹사이트에 리드 수집 폼을 작성하고, fetch()를 사용하여 Apps Script API로 데이터를 전송합니다.
외부 웹 서버 또는 로컬 환경에 다음 HTML 파일을 생성합니다:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>제품 문의 및 상담 신청</title>
<style>
body {
font-family: 'Arial', sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
background-color: #f9f9f9;
}
h2 {
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input, select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.success-message {
display: none;
text-align: center;
color: #4CAF50;
font-size: 18px;
margin-top: 20px;
}
.error-message {
display: none;
text-align: center;
color: #f44336;
font-size: 14px;
margin-top: 10px;
}
</style>
</head>
<body>
<h2>제품 문의 및 상담 신청</h2>
<form id="leadForm">
<div class="form-group">
<label for="name">이름 *</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">이메일 *</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="phone">전화번호 *</label>
<input type="tel" id="phone" name="phone" required>
</div>
<div class="form-group">
<label for="company">회사명</label>
<input type="text" id="company" name="company">
</div>
<div class="form-group">
<label for="source">유입경로</label>
<select id="source" name="source">
<option value="">선택해주세요</option>
<option value="검색엔진">검색엔진</option>
<option value="SNS">SNS</option>
<option value="블로그">블로그</option>
<option value="지인추천">지인추천</option>
<option value="광고">광고</option>
<option value="기타">기타</option>
</select>
</div>
<div class="form-group">
<label for="product">관심 제품</label>
<select id="product" name="product">
<option value="">선택해주세요</option>
<option value="제품A">제품 A</option>
<option value="제품B">제품 B</option>
<option value="제품C">제품 C</option>
<option value="전체">전체 제품</option>
</select>
</div>
<button type="button" id="submitBtn" onclick="submitLead()">상담 신청하기</button>
</form>
<div id="successMessage" class="success-message">
상담 신청이 완료되었습니다. 감사합니다!
</div>
<div id="errorMessage" class="error-message"></div>
<script>
// Apps Script 웹 앱 URL (배포 시 생성된 URL로 교체)
var APPS_SCRIPT_URL = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec';
function submitLead() {
var name = document.getElementById('name').value.trim();
var email = document.getElementById('email').value.trim();
var phone = document.getElementById('phone').value.trim();
// 필수 필드 검증
if (!name || !email || !phone) {
alert('이름, 이메일, 전화번호는 필수 입력 항목입니다.');
return;
}
// 이메일 형식 검증
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert('올바른 이메일 형식을 입력해주세요.');
return;
}
var leadData = {
name: name,
email: email,
phone: phone,
company: document.getElementById('company').value.trim(),
source: document.getElementById('source').value,
product: document.getElementById('product').value
};
// 버튼 비활성화 (중복 전송 방지)
var submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '전송 중...';
// Apps Script API로 POST 전송
fetch(APPS_SCRIPT_URL, {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(leadData)
})
.then(function() {
// no-cors 모드에서는 응답을 읽을 수 없으므로 성공으로 처리
document.getElementById('leadForm').style.display = 'none';
document.getElementById('successMessage').style.display = 'block';
})
.catch(function(error) {
document.getElementById('errorMessage').textContent = '제출 중 오류가 발생했습니다. 다시 시도해주세요.';
document.getElementById('errorMessage').style.display = 'block';
submitBtn.disabled = false;
submitBtn.textContent = '상담 신청하기';
});
}
</script>
</body>
</html>
YOUR_DEPLOYMENT_ID를 5단계에서 복사한 실제 배포 URL로 교체해야 합니다.
- Apps Script 웹 앱은 CORS 헤더를 직접 제어할 수 없으므로,
mode: 'no-cors'를 사용합니다. no-cors모드에서는 응답 본문을 읽을 수 없지만, 데이터 전송 자체는 정상적으로 이루어집니다.- 응답 확인이 필요한 경우, Apps Script를 JSONP 방식으로 구현하거나 Google Sheets에서 직접 저장 여부를 확인합니다.
터미널에서 다음 명령어로 Apps Script API가 정상 동작하는지 먼저 확인합니다:
curl -L -X POST \
'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec' \
-H 'Content-Type: application/json' \
-d '{
"name": "테스트",
"email": "test@example.com",
"phone": "010-0000-0000",
"company": "테스트회사",
"source": "검색엔진",
"product": "제품A"
}'정상 응답 예시:
{ "status": "success", "message": "리드 데이터가 저장되었습니다." }-
HTML 파일 실행:
index.html파일을 브라우저에서 열거나, 로컬 웹 서버로 실행합니다.- 예:
npx serve .또는 VS Code의 Live Server 확장 활용
-
테스트 데이터 입력:
- 다음과 같이 테스트 데이터를 입력하고 '상담 신청하기' 버튼을 클릭합니다:
- 이름:
홍길동 - 이메일:
hong@example.com - 전화번호:
010-1234-5678 - 회사명:
테스트 주식회사 - 유입경로:
검색엔진 - 관심 제품:
제품 A
- 이름:
- 다음과 같이 테스트 데이터를 입력하고 '상담 신청하기' 버튼을 클릭합니다:
-
데이터 확인:
- Google Sheets를 열어 입력한 데이터가 정상적으로 저장되었는지 확인합니다.
수집일시 이름 이메일 전화번호 회사명 유입경로 관심제품 2025-01-15 14:30:00 홍길동 hong@example.com 010-1234-5678 테스트 주식회사 검색엔진 제품A
수집된 리드 데이터에서 회사명, 관심 제품 등의 정보를 기반으로 고객 등급(예: VIP, 일반, 잠재)을 자동으로 분류하여 시트에 태깅합니다.
- 기존 시트의 H열에
고객등급헤더를 추가합니다.
Code.gs에 다음 함수를 추가합니다:
/**
* 리드 데이터를 기반으로 고객 등급을 분류하는 함수
* @param {Object} leadData - 리드 데이터
* @returns {string} - 고객 등급 (VIP / 일반 / 잠재)
*/
function classifyLead(leadData) {
// VIP: 회사명이 있고, 전체 제품에 관심이 있는 경우
if (leadData.company && leadData.product === '전체') {
return 'VIP';
}
// 일반: 회사명이 있는 경우
if (leadData.company) {
return '일반';
}
// 잠재: 그 외
return '잠재';
}기존 doPost 함수를 수정하여 등급 분류를 추가합니다:
function doPost(e) {
try {
var leadData = JSON.parse(e.postData.contents);
if (!leadData.name || !leadData.email || !leadData.phone) {
return ContentService.createTextOutput(
JSON.stringify({ status: 'error', message: '필수 필드가 누락되었습니다.' })
).setMimeType(ContentService.MimeType.JSON);
}
var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
var timestamp = Utilities.formatDate(new Date(), 'Asia/Seoul', 'yyyy-MM-dd HH:mm:ss');
// 고객 등급 분류
var grade = classifyLead(leadData);
sheet.appendRow([
timestamp,
leadData.name,
leadData.email,
leadData.phone,
leadData.company || '',
leadData.source || '',
leadData.product || '',
grade // 고객 등급 추가
]);
Logger.log('리드 저장 완료 - 이름: ' + leadData.name + ', 등급: ' + grade);
return ContentService.createTextOutput(
JSON.stringify({ status: 'success', message: '리드 데이터가 저장되었습니다.', grade: grade })
).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(
JSON.stringify({ status: 'error', message: error.message })
).setMimeType(ContentService.MimeType.JSON);
}
}신규 리드가 유입되면 영업팀에 Slack 메시지 또는 이메일로 알림을 자동 발송합니다.
- Slack API에서 새로운 앱을 생성합니다.
- Incoming Webhooks를 활성화하고, 알림을 받을 채널을 선택합니다.
- 생성된 Webhook URL을 복사합니다.
Code.gs에 다음 함수를 추가합니다:
var SLACK_WEBHOOK_URL = 'YOUR_SLACK_WEBHOOK_URL';
var NOTIFY_EMAIL = 'sales@example.com'; // 영업팀 이메일
/**
* Slack으로 신규 리드 알림을 발송하는 함수
* @param {Object} leadData - 리드 데이터
* @param {string} grade - 고객 등급
*/
function sendSlackNotification(leadData, grade) {
var message = {
text: ':tada: *신규 리드 유입!*',
attachments: [{
color: grade === 'VIP' ? '#ff0000' : '#36a64f',
fields: [
{ title: '이름', value: leadData.name, short: true },
{ title: '고객등급', value: grade, short: true },
{ title: '이메일', value: leadData.email, short: true },
{ title: '전화번호', value: leadData.phone, short: true },
{ title: '회사명', value: leadData.company || '미입력', short: true },
{ title: '관심제품', value: leadData.product || '미선택', short: true },
{ title: '유입경로', value: leadData.source || '미선택', short: true }
]
}]
};
var options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(message)
};
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
}
/**
* 이메일로 신규 리드 알림을 발송하는 함수
* @param {Object} leadData - 리드 데이터
* @param {string} grade - 고객 등급
*/
function sendEmailNotification(leadData, grade) {
var subject = '[신규 리드] ' + leadData.name + ' (' + grade + ')';
var htmlBody =
'<h3>신규 리드가 유입되었습니다</h3>' +
'<table border="1" cellpadding="8" cellspacing="0">' +
'<tr><td><b>이름</b></td><td>' + leadData.name + '</td></tr>' +
'<tr><td><b>이메일</b></td><td>' + leadData.email + '</td></tr>' +
'<tr><td><b>전화번호</b></td><td>' + leadData.phone + '</td></tr>' +
'<tr><td><b>회사명</b></td><td>' + (leadData.company || '미입력') + '</td></tr>' +
'<tr><td><b>유입경로</b></td><td>' + (leadData.source || '미선택') + '</td></tr>' +
'<tr><td><b>관심제품</b></td><td>' + (leadData.product || '미선택') + '</td></tr>' +
'<tr><td><b>고객등급</b></td><td>' + grade + '</td></tr>' +
'</table>';
MailApp.sendEmail({
to: NOTIFY_EMAIL,
subject: subject,
htmlBody: htmlBody
});
}function doPost(e) {
try {
var leadData = JSON.parse(e.postData.contents);
if (!leadData.name || !leadData.email || !leadData.phone) {
return ContentService.createTextOutput(
JSON.stringify({ status: 'error', message: '필수 필드가 누락되었습니다.' })
).setMimeType(ContentService.MimeType.JSON);
}
var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
var timestamp = Utilities.formatDate(new Date(), 'Asia/Seoul', 'yyyy-MM-dd HH:mm:ss');
var grade = classifyLead(leadData);
sheet.appendRow([
timestamp,
leadData.name,
leadData.email,
leadData.phone,
leadData.company || '',
leadData.source || '',
leadData.product || '',
grade
]);
// Slack 알림 발송
sendSlackNotification(leadData, grade);
// 이메일 알림 발송
sendEmailNotification(leadData, grade);
Logger.log('리드 저장 및 알림 발송 완료: ' + leadData.name);
return ContentService.createTextOutput(
JSON.stringify({ status: 'success', message: '리드 데이터가 저장되었습니다.', grade: grade })
).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(
JSON.stringify({ status: 'error', message: error.message })
).setMimeType(ContentService.MimeType.JSON);
}
}
YOUR_SLACK_WEBHOOK_URL과NOTIFY_EMAIL을 실제 값으로 교체해야 합니다.
외부 웹사이트(예: 채용 플랫폼, 비즈니스 디렉토리)에서 잠재 고객 정보를 주기적으로 수집하여 Google Sheets에 저장합니다.
Code.gs에 다음 함수를 추가합니다:
/**
* 외부 웹 API에서 리드 데이터를 수집하는 함수
* 예시: 공개 API에서 비즈니스 정보를 가져옴
*/
function fetchExternalLeads() {
var apiUrl = 'https://api.example.com/leads'; // 실제 API URL로 교체
var options = {
method: 'get',
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
};
try {
var response = UrlFetchApp.fetch(apiUrl, options);
var data = JSON.parse(response.getContentText());
var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
data.forEach(function(lead) {
// 중복 검사: 이메일 기준
if (!isDuplicate(sheet, lead.email)) {
var timestamp = Utilities.formatDate(new Date(), 'Asia/Seoul', 'yyyy-MM-dd HH:mm:ss');
var grade = classifyLead(lead);
sheet.appendRow([
timestamp,
lead.name,
lead.email,
lead.phone,
lead.company,
'자동수집',
lead.product || '',
grade
]);
}
});
Logger.log('외부 리드 수집 완료: ' + data.length + '건 처리');
} catch (error) {
Logger.log('외부 리드 수집 오류: ' + error.message);
}
}
/**
* 이메일 기준으로 중복 데이터를 검사하는 함수
* @param {Sheet} sheet - 대상 시트
* @param {string} email - 검사할 이메일 주소
* @returns {boolean} - 중복 여부
*/
function isDuplicate(sheet, email) {
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][2] === email) { // C열(이메일)
return true;
}
}
return false;
}-
트리거 추가:
- Apps Script 편집기에서 왼쪽 메뉴의 시계 아이콘(트리거)을 클릭합니다.
- '트리거 추가' 버튼을 클릭합니다.
-
트리거 설정:
- 실행할 함수:
fetchExternalLeads - 이벤트 소스:
시간 기반 - 시간 기반 트리거 유형:
시간 간격 타이머 - 간격 선택:
매 1시간마다(필요에 따라 조정)
- 실행할 함수:
-
권한 부여:
- 권한 요청 창이 나타나면 지시에 따라 권한을 승인합니다.
-
API 키 보안:
SPREADSHEET_ID,SLACK_WEBHOOK_URL, API 키 등 민감한 정보는 코드에 직접 작성하지 않고 Apps Script의 스크립트 속성(프로젝트 설정 > 스크립트 속성)에 저장하여 관리하는 것을 권장합니다.// 스크립트 속성에서 값 가져오기 var SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
-
API 호출 한도: Google Apps Script는 일일 실행 시간 및 API 호출 횟수에 제한이 있습니다. 할당량 문서를 확인하세요.
-
개인정보 보호: 고객의 개인정보를 수집·저장할 때는 개인정보 보호법을 준수해야 하며, 수집 목적과 이용 범위를 명확히 고지해야 합니다.
이 실습을 통해 웹에서 고객 리드 데이터를 수집하고 Google Sheets에 자동으로 저장하며, 등급 분류와 알림 발송까지 자동화하는 시스템을 구축할 수 있습니다. 이는 영업팀의 리드 관리 효율을 크게 높이는 데 활용될 수 있습니다.