콘텐츠 생성부터 단체 발송까지, 구글시트 CRM 자동화 실전 사례

소개

Apps Script 기반 자동화에 대한 이해를 높이기 위해, 스터디장님의 「구글 시트를 DB처럼 활용한 CRM 자동화」 사례를 직접 실습해 보고자 했습니다.
특히 단순한 메일 발송을 넘어, 구글 시트 + Gemini + Apps Script를 결합해 메일 콘텐츠 생성부터 단체 발송까지 자동화하는 흐름을 구현하는 것을 목표로 삼았습니다.

실습 과정에서는 이메일 초안을 사람이 간단히 작성하면, Gemini를 활용해 메일 제목과 본문을 자동 생성하고, 이후 별도의 고객 DB 시트를 참조해 개인화된 단체 메일을 발송하는 구조를 설계했습니다.
이를 통해 Google Sheets를 단순한 문서가 아닌, 경량 CRM 데이터베이스로 활용하는 자동화 가능성을 검증하고자 했습니다.

진행 방법

  1. 메일 내용 작성 자동화

  2. 메일 단체 발송 자동화

1. 메일 내용 작성 자동화

  • Gemini에서 실행

아래는 구글 시트의 내용이야.

c6 내가 대충 쓴 내용
c7 내용 작성 프롬프트(구글 Doc로 연동되어 있어)

c8에는 c6의 내용을 보고 "메일 제목" 작성해줘.(Gemini 사용)
c9는 c6을 c7의 프롬프트 내용으로 "메일 내용" 다시 작성해줘.(Gemini 사용)

"이메일작성" 시트 기준으로 진행할거야. 

GEMINI_API_KEY = 'AIzaSyCVRnLhDOpbuxMa-rNYFIpgirVAjMpGFYo'
gemini 모델은 gemini-2.5-flash를 사용해줘.

Gemini 1.5 Flash 모델 기준으로 Apps Script 예제를 안내받았으나, 실제 적용 과정에서 정상적으로 동작하지 않았습니다.
이후 Gemini 2.0 Flash 모델 기준으로 다시 Apps Script를 시도했지만, 동일하게 문제가 발생했습니다.

또한 Gemini에서 생성한 Python 테스트 코드를 Cursor AI에서 실행하자, 사용된 모델이 맞지 않다는 오류 메시지가 출력되었습니다.
다만 Cursor AI가 해당 오류를 자동으로 감지하고 원인을 분석해 주어, 문제를 파악하는 데에는 도움이 되었습니다.

마지막 프롬프트에 "gemini 모델은 gemini-2.5-flash를 사용해줘."가 삽입된 이유입니다.

  • 결과

    /**
     * 구글 시트 "이메일작성" 시트의 데이터를 바탕으로
     * C7(구글 문서 연동 프롬프트)을 사용하여 메일을 작성합니다.
     */
    
    const GEMINI_API_KEY = 'AIzaSyCVRnLhDOpbuxMa-rNYFIpgirVAjMpGFYo';
    const MODEL_NAME = 'gemini-2.5-flash'; 
    
    function generateEmailContent() {
      const ss = SpreadsheetApp.getActiveSpreadsheet();
      const sheet = ss.getSheetByName("이메일작성");
      
      // 데이터 읽기
      const rawDraft = sheet.getRange("C6").getValue();      // 사용자가 쓴 원본 내용
      const docInstruction = sheet.getRange("C7").getValue(); // 구글 문서에서 가져온 프롬프트 내용
      
      if (!rawDraft) {
        SpreadsheetApp.getUi().alert("C6 셀에 원본 내용이 없습니다.");
        return;
      }
    
      // 1. 이메일 제목 생성 (C8)
      const subjectPrompt = `다음 내용을 바탕으로 이메일 제목만 딱 한 줄 작성해줘. 다른 설명은 하지 마: "${rawDraft}"`;
      let generatedSubject = callGemini(subjectPrompt);
      // '제목:' 문구가 포함된 경우 제거
      generatedSubject = generatedSubject.replace(/^제목:\s*/, "").replace(/["']/g, ""); 
      sheet.getRange("C8").setValue(generatedSubject);
    
      // 2. 이메일 본문 작성 (C9)
      // C7의 프롬프트 내용을 핵심 지침으로 사용하되, 결과물만 나오도록 제약을 추가합니다.
      const bodyPrompt = `
        [지침]:
        ${docInstruction}
        
        [원본 내용]:
        ${rawDraft}
        
        [출력 규칙]:
        - 위 [지침]에 따라 [원본 내용]을 메일로 작성해라.
        - "네, 작성하겠습니다"와 같은 인사말이나 서론, 결론 설명은 모두 생략해라.
        - 여러 옵션을 주지 말고 단 하나의 완성된 메일 본문 텍스트만 출력해라.
      `;
      
      const generatedBody = callGemini(bodyPrompt);
      sheet.getRange("C9").setValue(generatedBody);
      
      SpreadsheetApp.getUi().alert("이메일 작성이 완료되었습니다!");
    }
    
    /**
     * Gemini API 호출 함수
     */
    function callGemini(prompt) {
      const url = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL_NAME}:generateContent?key=${GEMINI_API_KEY}`;
      
      const payload = {
        "contents": [{
          "parts": [{
            "text": prompt
          }]
        }]
      };
      
      const options = {
        "method": "post",
        "contentType": "application/json",
        "payload": JSON.stringify(payload),
        "muteHttpExceptions": true
      };
      
      try {
        const response = UrlFetchApp.fetch(url, options);
        const json = JSON.parse(response.getContentText());
        
        if (json.candidates && json.candidates[0].content && json.candidates[0].content.parts[0].text) {
          return json.candidates[0].content.parts[0].text.trim();
        } else {
          return "응답 추출 실패: " + response.getContentText();
        }
      } catch (e) {
        return "API 연결 오류: " + e.toString();
      }
    }
  • Apps Script 메뉴 선택

  • "+" 클릭 후 writeMailContent.gs 이름 수정

  • Gemini가 생성한 Apps Script 복사 & 붙여 넣기 > 저장

  • 이미지에 스크립트 할당

  • 함수 이름 붙여 넣기

  • 그림 클릭 후 함수 실행 및 결과 확인

2. 메일 단체 발송 자동화

  • Gemini

c19 발신자 이름
c20 내 고객디비가 들어있는 구글시트
c21에는 고객디비 시트명
c9 메일 내용

그 고객디비에 단체메일을 보내는 앱스스크립트 만들어줘.
이메일은 고객디비의 컬럼명이 이메일
이메일 내용중에서 {{이름}} 이런게 나오면, 닉네임으로 교체해줘.
주소록은 나중에 다른 시트에 참조할수도 있어주소록은 나중에 다른 시트에 참조할수도 있어서 그렇게 요청한거야.서 그렇게 요청한거야.
구글 쉬트에서는 다른 파일을 @를 통해서 참조할 수 있어. @로 참조된 이름을 불러오도록 수정해줘. c20에 구글 쉬트 참조된 파일명이 있어.

팁 : 구글 스프레드시트에서 @는 스마트 칩 으로 파일을 참조 한다고 합니다. 첫 프롬프트에 스마트 칩이라는 단어를 넣는 것을 권장 합니다.

  • Gemini 출력 결과

    function SEND_EMAILS_WITH_SMART_CHIP() {
      const ss = SpreadsheetApp.getActiveSpreadsheet();
      const mainSheet = ss.getActiveSheet(); 
      
      // 1. 설정 정보 읽기
      const senderName = mainSheet.getRange("C19").getValue();    
      const dbSheetName = mainSheet.getRange("C21").getValue();  
      const emailColName = mainSheet.getRange("C22").getValue(); 
      const nameColName = mainSheet.getRange("C23").getValue();  
      const rawContent = mainSheet.getRange("C9").getValue();    
    
      // C20 셀에서 스마트 칩(@파일명)의 URL 추출하기
      const c20Range = mainSheet.getRange("C20");
      const richText = c20Range.getRichTextValue();
      let dbUrl = "";
      
      // 스마트 칩에서 링크(URL) 정보만 뽑아냅니다.
      if (richText && richText.getLinkUrl()) {
        dbUrl = richText.getLinkUrl();
      } else {
        // 만약 스마트 칩이 아니라 그냥 URL을 텍스트로 넣었을 경우를 대비
        dbUrl = c20Range.getValue();
      }
    
      // 제목과 본문 분리 (제목: 으로 시작하는 줄 찾기)
      let emailSubject = "공지사항입니다";
      let emailBody = rawContent;
      if (rawContent.includes("제목:")) {
        const lines = rawContent.split('\n');
        const subLine = lines.find(l => l.includes("제목:"));
        emailSubject = subLine.replace("제목:", "").trim();
        emailBody = lines.filter(l => !l.includes("제목:")).join('\n').trim();
      }
    
      // 2. 주소록(DB) 가져오기
      let dbSheet;
      try {
        if (dbUrl && dbUrl.toString().includes("http")) {
          // @로 참조된 파일 또는 URL을 통해 외부 파일 오픈
          dbSheet = SpreadsheetApp.openByUrl(dbUrl).getSheetByName(dbSheetName);
        } else {
          // C20이 비어있으면 현재 파일에서 찾기
          dbSheet = ss.getSheetByName(dbSheetName);
        }
      } catch (e) {
        SpreadsheetApp.getUi().alert("주소록 파일을 열 수 없습니다. @참조가 올바른지, 혹은 시트명이 정확한지 확인해주세요.");
        return;
      }
    
      const data = dbSheet.getDataRange().getValues();
      const headers = data[0];
      const emailIdx = headers.indexOf(emailColName);
      const nicknameIdx = headers.indexOf(nameColName);
    
      if (emailIdx === -1 || nicknameIdx === -1) {
        SpreadsheetApp.getUi().alert("컬럼명을 찾을 수 없습니다: " + emailColName + ", " + nameColName);
        return;
      }
    
      // 3. 발송 루프
      let sentCount = 0;
      for (let i = 1; i < data.length; i++) {
        const row = data[i];
        const email = row[emailIdx];
        const nickname = row[nicknameIdx];
    
        if (email && email.toString().includes("@")) {
          // {{이름}}을 닉네임으로 교체
          const finalBody = emailBody.replace(/{{이름}}/g, nickname);
    
          MailApp.sendEmail({
            to: email,
            subject: emailSubject,
            body: finalBody,
            name: senderName
          });
          sentCount++;
        }
      }
    
      SpreadsheetApp.getUi().alert(sentCount + "건의 메일 발송이 완료되었습니다.");
    }
  • Apps Script 실행 부터 그림 클릭 후 함수 실행 및 결과 확인 까지 1번 단계와 동일하여 생략합니다.

  • 고객DB 시트 확인

    아래 고객DB 시트를 읽고 이메일 컬럼을 읽고 메일을 전달합니다.

  • Email 결과

    휴대전화에 있는 한국어 페이지의 스크린샷

결과와 배운 점

Gemini 모델을 활용한 메일 제목·본문 자동 생성 로직을 Apps Script로 구현하여, 이메일 작성 과정을 상당 부분 자동화할 수 있었습니다.
초기에는 Gemini 1.5 Flash 및 2.0 Flash 기준 예제로 인해 오류가 발생했으나, 모델을 gemini-2.5-flash로 명시적으로 지정하면서 정상 동작을 확인했습니다.
이 과정에서 모델 버전 명시가 자동화 안정성에 매우 중요하다는 점을 명확히 인식하게 되었습니다.

또한 Google Sheets의 스마트 칩(@파일 참조)을 활용해 외부 고객 DB 시트를 동적으로 불러오고, 컬럼명을 기준으로 이메일·닉네임을 매핑하여 개인화된 단체 메일 발송이 가능함을 확인했습니다.
메일 본문 내 {{이름}}과 같은 플레이스홀더를 실제 고객 정보로 치환하는 방식은, 실무 CRM 자동화에 바로 적용 가능한 구조였습니다.

이번 실습을 통해 Apps Script는 단순한 매크로 수준을 넘어, AI 기반 콘텐츠 생성 + 데이터 연동 + 커뮤니케이션 자동화까지 충분히 확장 가능하다는 점을 체감했습니다.
특히 Google Sheets 하나만으로도, 기획–작성–발송이 연결된 실전형 업무 자동화 파이프라인을 만들 수 있다는 점이 가장 큰 수확이었습니다.

도움 받은 글 (옵션)

https://www.youtube.com/watch?v=IO4Xr_eGWfo

1

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요