Claude Code로 스프레드시트 자동화하기: CSV, Google Sheets API, Apps Script
Claude Code로 CSV 집계, Google Sheets API 기록, Apps Script 리드 관리를 구현하는 실무 가이드.
스프레드시트 자동화는 작아 보이지만 실제로는 매출, 문의, 광고비, 청구, 콘텐츠 KPI가 모이는 작업입니다. 가져오기 규칙이 한 번 틀리면 팀은 잘못된 매출, 잘못된 우선순위, 잘못된 캠페인 판단을 보게 됩니다.
Claude Code를 쓰는 이유는 AI에게 표 계산을 맡기기 위해서가 아닙니다. 저장소 안에서 규칙을 읽고, CSV 집계 스크립트, Google Sheets API 기록 스크립트, Apps Script 분류 로직을 만들고, 마지막에 변경 파일과 검증 결과를 남기게 할 수 있기 때문입니다.
이 글은 로컬 CSV 집계, Sheets API로 문의 행 추가, Apps Script로 리드 분류까지 한 흐름으로 보여줍니다. 기본 운용은 Claude Code 생산성 팁과 검증 레시트 워크플로를 함께 보면 좋습니다. 공식 문서는 Claude Code docs, Claude Code CLI usage, Google Sheets API Node.js quickstart, Apps Script Sheets guide, SheetJS docs를 기준으로 확인하세요.
먼저 데이터 경계를 정하기
가장 먼저 정할 것은 라이브러리가 아니라 사실의 원천입니다. 같은 숫자를 CSV, Google Sheets, CRM, 회계 도구에서 모두 고칠 수 있으면 자동화는 속도만 빠른 혼란이 됩니다. Claude Code에 구현을 맡기기 전에 입력, 정리된 중간 데이터, 사람이 보는 보고서를 분리합니다.
| 계층 | 역할 | 예시 | Claude Code에 맡길 일 |
|---|---|---|---|
| Raw | 사람이 직접 고치지 않는 원본 | 폼 제출, 결제 CSV, 광고 export | 가져오기, 검증, 오류 행 분리 |
| Clean | 타입과 이름을 맞춘 중간표 | 날짜, 금액, 상태명 정규화 | 정규화, 중복 제거, 필수 컬럼 확인 |
| Report | 사람이 읽는 출력 | 월 매출, 리드 우선순위, KPI 표 | 집계, 그래프용 CSV, 알림 |
초보자가 자주 하는 실수는 API로 Report 탭에 바로 쓰는 것입니다. Report 탭에는 색, 수식, 메모, 고정 행, 사람이 보기 위한 정렬이 들어갑니다. 기계는 안정적인 헤더와 append 전용 행을 좋아합니다. 프로그램은 Raw나 Clean에만 쓰고, Report는 수식, 피벗, Looker Studio, 별도 스크립트로 만드는 편이 안전합니다.
예제1: 매출 CSV를 월별로 집계하기
먼저 Google 인증 없이 실행되는 스크립트를 만듭니다. 이렇게 하면 Claude Code가 만든 코드를 권한 문제 없이 로컬에서 검증할 수 있습니다.
data/sales.csv를 만듭니다.
date,channel,product,amount,status
2026-05-01,organic,Claude Code Cheatsheet,0,won
2026-05-02,gumroad,Prompt Template Pack,2980,won
2026-05-08,consultation,Team Workshop,120000,won
2026-05-11,gumroad,Prompt Template Pack,2980,refunded
2026-06-01,organic,Claude Code Cheatsheet,0,won
2026-06-02,consultation,Implementation Review,80000,won
scripts/summarize-sales.mjs를 저장합니다.
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const inputPath = process.argv[2] ?? "data/sales.csv";
const outputPath = process.argv[3] ?? "out/monthly-summary.csv";
function parseCsvLine(line) {
const cells = [];
let current = "";
let inQuotes = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
const next = line[index + 1];
if (char === '"' && inQuotes && next === '"') {
current += '"';
index += 1;
continue;
}
if (char === '"') {
inQuotes = !inQuotes;
continue;
}
if (char === "," && !inQuotes) {
cells.push(current.trim());
current = "";
continue;
}
current += char;
}
cells.push(current.trim());
return cells;
}
function parseCsv(source) {
const lines = source.trim().split(/\r?\n/).filter(Boolean);
const headers = parseCsvLine(lines[0]);
return lines.slice(1).map((line) => {
const cells = parseCsvLine(line);
return Object.fromEntries(headers.map((header, index) => [header, cells[index] ?? ""]));
});
}
function toMonth(dateValue) {
const date = new Date(`${dateValue}T00:00:00Z`);
if (Number.isNaN(date.getTime())) {
throw new Error(`Invalid date: ${dateValue}`);
}
return dateValue.slice(0, 7);
}
const rows = parseCsv(await readFile(inputPath, "utf8"));
const summary = new Map();
for (const row of rows) {
if (row.status !== "won") continue;
const amount = Number(row.amount);
if (!Number.isFinite(amount)) {
throw new Error(`Invalid amount: ${JSON.stringify(row)}`);
}
const key = `${toMonth(row.date)},${row.channel}`;
const current = summary.get(key) ?? { month: toMonth(row.date), channel: row.channel, deals: 0, revenue: 0 };
current.deals += 1;
current.revenue += amount;
summary.set(key, current);
}
const output = [
"month,channel,deals,revenue",
...[...summary.values()]
.sort((a, b) => `${a.month}:${a.channel}`.localeCompare(`${b.month}:${b.channel}`))
.map((row) => `${row.month},${row.channel},${row.deals},${row.revenue}`),
].join("\n");
await mkdir(path.dirname(outputPath), { recursive: true });
await writeFile(outputPath, `${output}\n`, "utf8");
console.log(`Wrote ${outputPath} (${summary.size} groups)`);
실행합니다.
mkdir -p data out scripts
node scripts/summarize-sales.mjs data/sales.csv out/monthly-summary.csv
cat out/monthly-summary.csv
핵심은 잘못된 데이터에서 조용히 계속하지 않는 것입니다. 금액이 비었거나 날짜가 깨졌거나 상태명이 바뀌면 바로 실패해야 합니다. 다음 단계로 Claude Code에 행 번호가 포함된 오류 메시지, 거절 행 CSV, 환불 제외 테스트를 추가하라고 요청할 수 있습니다.
예제2: Google Sheets API로 문의 행 추가하기
팀 운용에서는 개인 OAuth보다 서비스 계정이 감사하기 쉽습니다. Google Cloud에서 Sheets API를 활성화하고 서비스 계정 JSON을 만들고, 해당 서비스 계정 이메일을 대상 스프레드시트에 편집자로 공유합니다. 시트에는 Raw 탭을 만들고 첫 행에 createdAt,source,subject,amount,status를 둡니다.
npm install googleapis
export GOOGLE_APPLICATION_CREDENTIALS="$PWD/service-account.json"
export SHEET_ID="your-google-sheet-id"
scripts/append-lead-to-sheet.mjs를 만듭니다.
import { google } from "googleapis";
const { GOOGLE_APPLICATION_CREDENTIALS, SHEET_ID } = process.env;
if (!GOOGLE_APPLICATION_CREDENTIALS) {
throw new Error("GOOGLE_APPLICATION_CREDENTIALS is required");
}
if (!SHEET_ID) {
throw new Error("SHEET_ID is required");
}
const auth = new google.auth.GoogleAuth({
keyFile: GOOGLE_APPLICATION_CREDENTIALS,
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const sheets = google.sheets({ version: "v4", auth });
const source = process.argv[2] ?? "web";
const subject = process.argv[3] ?? "Claude Code consultation";
const amount = Number(process.argv[4] ?? 0);
if (!Number.isFinite(amount)) {
throw new Error(`Invalid amount: ${process.argv[4]}`);
}
await sheets.spreadsheets.values.append({
spreadsheetId: SHEET_ID,
range: "Raw!A:E",
valueInputOption: "USER_ENTERED",
insertDataOption: "INSERT_ROWS",
requestBody: {
values: [[new Date().toISOString(), source, subject, amount, "new"]],
},
});
console.log("Appended lead row");
실행 예시는 다음과 같습니다.
node scripts/append-lead-to-sheet.mjs newsletter "Spreadsheet automation review" 50000
Claude Code에 수정시킬 때는 환경 변수명, 쓰기 범위, 대상 탭, 컬럼 순서, credential 금지 규칙을 같이 적어야 합니다. service-account.json은 Git, 이슈, 문서, 프롬프트에 들어가면 안 됩니다.
예제3: Apps Script로 매출과 문의 분류하기
업무가 Google Workspace 안에 있다면 Apps Script가 편합니다. 폼 제출을 받아 Sheets에 쓰고 이메일을 보낼 수 있습니다. 대신 실행 시간, 트리거, 권한, 메일 발송량 제한이 있으므로 고빈도 작업 전에는 Apps Script quotas를 확인해야 합니다.
아래 코드를 Apps Script 편집기에 붙여넣고 onFormSubmit에 설치형 폼 제출 트리거를 설정합니다.
const SETTINGS = {
sheetName: "Leads",
notifyTo: "sales@example.com",
minAmountForHighPriority: 100000,
};
function onOpen() {
SpreadsheetApp.getUi()
.createMenu("Lead Ops")
.addItem("Rebuild lead status", "rebuildLeadStatus")
.addToUi();
}
function onFormSubmit(event) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getSheetByName(SETTINGS.sheetName) || spreadsheet.insertSheet(SETTINGS.sheetName);
ensureHeader_(sheet);
const values = event && event.namedValues ? event.namedValues : {};
const company = first_(values, "Company");
const email = first_(values, "Email");
const plan = first_(values, "Plan");
const budget = Number(first_(values, "Budget") || 0);
const priority = classifyLead_(plan, budget);
sheet.appendRow([new Date(), company, email, plan, budget, priority, "new"]);
if (priority === "high") {
MailApp.sendEmail({
to: SETTINGS.notifyTo,
subject: `High priority lead: ${company}`,
body: `Company: ${company}\nEmail: ${email}\nPlan: ${plan}\nBudget: ${budget}`,
});
}
}
function rebuildLeadStatus() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SETTINGS.sheetName);
if (!sheet) throw new Error(`Sheet not found: ${SETTINGS.sheetName}`);
ensureHeader_(sheet);
const values = sheet.getDataRange().getValues();
for (let rowIndex = 1; rowIndex < values.length; rowIndex += 1) {
const row = values[rowIndex];
const plan = String(row[3] || "");
const budget = Number(row[4] || 0);
const priority = classifyLead_(plan, budget);
sheet.getRange(rowIndex + 1, 6).setValue(priority);
}
}
function ensureHeader_(sheet) {
const header = ["createdAt", "company", "email", "plan", "budget", "priority", "status"];
const current = sheet.getRange(1, 1, 1, header.length).getValues()[0];
if (current.join("") === "") {
sheet.getRange(1, 1, 1, header.length).setValues([header]);
sheet.setFrozenRows(1);
}
}
function classifyLead_(plan, budget) {
const normalizedPlan = String(plan).toLowerCase();
if (budget >= SETTINGS.minAmountForHighPriority || normalizedPlan.includes("team")) {
return "high";
}
if (budget >= 30000) {
return "medium";
}
return "low";
}
function first_(namedValues, key) {
const value = namedValues[key];
return Array.isArray(value) ? value[0] || "" : "";
}
폼 항목명이 한국어라면 Company, Budget 같은 키를 실제 항목명으로 바꿉니다. Claude Code에 요청할 때 실제 항목명, 알림 조건, 개인 정보 로그 금지를 함께 전달하세요.
Claude Code 요청 템플릿
좋은 요청은 좁고 검증 가능합니다. 입력, 출력, 수정 범위, 금지 사항, 확인 명령을 한 번에 적습니다.
You are working on spreadsheet automation for this repository.
Goal:
- Import sales CSV rows from data/sales.csv.
- Write a monthly summary to out/monthly-summary.csv.
- Add a Google Sheets append script for the Raw tab.
Scope:
- You may edit scripts/summarize-sales.mjs and scripts/append-lead-to-sheet.mjs.
- You may add small tests or sample CSV files if needed.
- Do not edit content files, product links, analytics, or deployment settings.
Rules:
- Do not commit credentials.
- Use environment variables for SHEET_ID and GOOGLE_APPLICATION_CREDENTIALS.
- Fail loudly on invalid dates, invalid amounts, and missing required columns.
- Keep the code copy-paste runnable with Node.js 20 or later.
Verification:
- Run node --check on every script you edit.
- Run the CSV summary against data/sales.csv.
- For Google Sheets API, verify syntax locally and list the manual credential checks.
- Return changed files, commands run, output summary, and remaining risks.
검증 명령은 다음처럼 고정합니다.
node --check scripts/summarize-sales.mjs
node scripts/summarize-sales.mjs data/sales.csv out/monthly-summary.csv
node --check scripts/append-lead-to-sheet.mjs
git diff -- scripts/summarize-sales.mjs scripts/append-lead-to-sheet.mjs
팀에서 반복한다면 이 내용을 CLAUDE.md에 넣고 Claude Code 권한 가이드와 연결해 실행 가능한 명령과 수정 가능한 파일을 제한하세요.
실무 유스케이스
첫 번째는 월 매출 리포트입니다. Gumroad, Stripe, 수동 청구, 무료 자료 등록을 CSV로 모으고, 환불은 제외하며 무료 리드는 건수만 셉니다. 차트보다 중요한 것은 상태별 규칙입니다.
두 번째는 문의 우선순위입니다. 예산, 팀 규모, 플랜, 기존 고객 도메인으로 분류할 수 있습니다. 규칙은 설명 가능해야 합니다. “예산 100000엔 이상 또는 team 플랜”은 검토할 수 있지만 “좋아 보임”은 검토할 수 없습니다.
세 번째는 기사와 광고 KPI입니다. slug, 공개일, 검색 클릭, CTA 클릭, 상품 클릭, 상담 폼 도달을 한 시트에 모으면 개선할 글이 보입니다. 이벤트 이름은 Claude Code 분석 구현과 맞추면 안전합니다.
네 번째는 청구 전 검산입니다. 납품 로그와 청구 CSV를 비교하고, 불일치 행만 검토 시트에 씁니다. 첫날부터 자동 발송까지 만들지 말고 차이를 보이게 만드는 단계부터 시작하세요.
피해야 할 함정
가장 큰 함정은 컬럼명이 흔들리는 것입니다. Amount, amount, 매출이 섞이면 스크립트가 성공해도 누락이 생깁니다. 필수 헤더가 없으면 종료하게 만들어야 합니다.
두 번째는 Sheets를 데이터베이스처럼 쓰는 것입니다. Sheets는 협업과 검토에 좋지만 트랜잭션, 잠금, 대량 쓰기, 권한의 원본에는 맞지 않습니다. 결제와 접근 권한은 실제 애플리케이션 DB에 남겨야 합니다.
세 번째는 credential 유출입니다. 서비스 계정 JSON은 Git, 문서, 이슈, 프롬프트에 들어가면 안 됩니다. Claude Code에 비밀 정보를 읽지 말고 출력하지 말고 커밋하지 말라고 명시합니다.
네 번째는 Apps Script 트리거를 잊는 것입니다. 코드를 붙여넣어도 폼 제출 시 실행되지 않을 수 있습니다. 설치형 트리거, 실행 사용자, 최초 승인, 메일 한도, 오류 알림을 확인하세요.
다섯 번째는 최종 합계만 보는 것입니다. 리포트에는 읽은 행 수, 제외한 행 수, 오류 수, 마지막 업데이트 시각이 필요합니다. 이 숫자가 없으면 언제부터 자동화가 깨졌는지 알 수 없습니다.
CTA: 스크립트를 운영으로 만들기
스프레드시트 자동화는 스크립트보다 반복 가능한 요청, 권한, 검증에서 안정됩니다. 먼저 무료 Claude Code 치트시트로 매일 쓰는 명령과 리뷰 습관을 고정하세요. 재사용할 프롬프트와 CLAUDE.md 템플릿은 ClaudeCodeLab 제품에서 확인할 수 있습니다.
팀이 매출, 리드, 광고, 청구 시트를 운영한다면 자동화를 늘리기 전에 credential 경계와 승인 절차를 정해야 합니다. 그 설계까지 함께 잡고 싶다면 Claude Code 교육 및 상담을 활용하세요.
실제로 시도한 결과
Masa의 운용에서 가장 효과가 컸던 것은 Google Sheets API부터 붙이는 일이 아니라 CSV 헤더, 제외 규칙, 실패 방식을 먼저 고정하는 일이었습니다. 그러자 Claude Code 요청이 구체적이 되었고 리뷰 시간이 줄었습니다. Apps Script는 리드 분류에는 편했지만 알림 조건이 모호하면 메일이 너무 많이 왔습니다. 안정적인 접근은 Raw, Clean, Report의 경계를 정한 뒤 Claude Code에 작은 구현과 검증을 맡기는 방식이었습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.