구글 광고 문구를 작성하는 Claude 스킬
GAQL로 구글 광고 API를 직접 조회하고, CTR이 약하거나 CPA가 높은 반응형 검색 광고를 찾아내고, 구글의 정확한 글자 수 제한(30자, 90자) 안에서 헤드라인과 설명을 다시 쓰는 Claude 스킬을 만들었다. 새 광고는 일시중지 상태로 생성되며, 직접 검토한 뒤 활성화한다. 정말 중요한 부분은 이거다. 구글의 글자 수 제한이 진짜 제약이고, 문구 자체가 아니라는 것. 글자 수를 제대로 세지 못하는 모델은 구글이 조용히 잘라내거나 거부하는 광고를 만들어낸다.
매주 수요일. 28,400명+ 구독자. 핵심만.
✓ 받은편지함을 확인하세요 — 확인 링크를 클릭해 가입을 완료하세요.
✓ 구독이 완료되었습니다!
✓ 이미 목록에 있습니다.
[운영자의 시각] 이미 페이스북 광고를 관리하는 Claude 스킬이 있다 — 그 구축 내용은 여기. 구글 광고는 더 어려운 후속작이었는데, API가 더 나쁘기 때문이 아니라 반응형 검색 광고에는 페이스북에 없는 엄격한 글자 수 제한이 있기 때문이다: 헤드라인당 30자, 설명당 90자. 읽기엔 괜찮지만 게재 시점에 거부되거나 잘려나가는 다시쓰기는 다시쓰기가 아니라 버그다. 이 스킬 작업의 대부분은 모델이 그 제약을 믿고 맡기는 대신 실제로 지키도록 강제하는 데 들어갔다.
Table of contents
Open Table of contents
구글 광고가 페이스북과 다른 구축이 필요했던 이유
페이스북 스킬은 성과 데이터를 읽고, 문구를 다시 쓰고, 새 광고 세트를 게시한다. 구글 광고도 같은 세 가지 일을 하지만 두 가지가 다르다.
- 쿼리 언어가 GAQL이고, REST 필터가 아니다. 구글 광고 API v18은 Google Ads Query Language로 동작한다 — SQL 형태의, 읽기 전용이며, 정확히 이런 종류의 리포트 추출을 위해 만들어졌다.
- 반응형 검색 광고에는 엄격한 제한이 있다. 광고당 최대 15개 헤드라인(각 30자)과 4개 설명(각 90자). 구글은 부드럽게 경고하지 않는다 — 제출 시점에 자산을 거부하거나, 더 나쁘게는 실제 고객에게 잘못 보이는 잘린 버전을 게재한다.
2번 항목이 순진한 구현을 무너뜨리는 지점이다. 모델에게 “더 강렬한 헤드라인을 써줘”라고 하면 기꺼이 34자를 돌려준다. 구글 광고는 그 자산을 거부하거나 단어 중간에서 끊긴 채로 렌더링한다. 나는 “예약은 지금 바로 하세—“로 끝나는 실제 광고를 보기 전까지는 그걸 알아채지 못한다.
API 접근 권한 얻기 (모두가 과소평가하는 부분)
세 가지가 필요하고, 세 번째를 계획하지 않으면 일주일을 날린다.
- OAuth2 자격 증명을 가진 구글 클라우드 프로젝트 — 클라이언트 ID, 클라이언트 시크릿, 그리고
https://www.googleapis.com/auth/adwords범위의 리프레시 토큰. 리프레시 토큰은 OAuth 플레이그라운드나 짧은 로컬 스크립트로 한 번만 생성하면 되고, 그 후로는 로그인 흐름이 아니라 저장된 자격 증명이 된다. - 개발자 토큰 — 구글 광고 관리자(MCC) 계정의 관리 → API 센터에서 신청한다.
- 접근 수준. 새로 발급된 개발자 토큰은 모두 테스트 계정 접근으로 시작한다 — 구글 광고 테스트 계정에만 작동하고 실제 계정에는 작동하지 않는다. Basic Access(하루 최대 15,000건의 작업, 이 용도에는 충분)는 구글이 수동으로 검토하는 양식을 통해 신청한다. 실제 계정에서 가동하려 계획하기 전에 승인까지 며칠의 영업일을 잡아두어야 한다.
GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_CUSTOMER_ID(하이픈 없는 10자리 계정 ID)를 .env에 저장한다. MCC를 통해 계정을 관리한다면 관리자 계정 ID로 설정한 GOOGLE_ADS_LOGIN_CUSTOMER_ID도 필요하다 — 이게 없으면 쿼리가 권한 오류로 거부되는데, 오류 메시지는 이유를 알려주지 않는다.
스킬 파일 구조
.claude/skills/google-ads/
SKILL.md ← Claude가 읽는 지침
index.ts ← 실제 도구 구현
limits.ts ← 글자 수 제한 검증SKILL.md:
# Google Ads Manager Skill
Use this skill when the user says "check my Google Ads", "run ads
report", or "rewrite underperforming RSAs". Never run this without
explicit user instruction — it touches live ad spend.
## What it can do
- Pull campaign and ad group performance via GAQL (last 7 or 30 days)
- Flag responsive search ads with CTR < 3% or CPA above the stored
target as underperformers
- Rewrite headlines (≤30 chars) and descriptions (≤90 chars) for
flagged ads in Ale's voice
- Create new ad variations as PAUSED — never enabled automatically
## What it will NOT do
- Change budgets or bids without explicit confirmation
- Enable a paused ad
- Submit any headline or description that fails the character check“What it will NOT do”의 마지막 줄은 첫 주에 스킬이 32자짜리 헤드라인을 게시한 뒤에 추가한 것이다. 모델 자신의 글자 수 계산이 2자만큼 틀렸다 — 얼핏 보기엔 통과할 만큼 가까웠지만 자산이 거부될 만큼 멀었다. 그 이후로 모든 것은 모델의 자체 보고가 아니라 엄격한 검증기를 통과해야 한다.
핵심 TypeScript 코드
// .claude/skills/google-ads/index.ts
import { GoogleAdsApi } from "google-ads-api";
import Anthropic from "@anthropic-ai/sdk";
const client = new GoogleAdsApi({
client_id: process.env.GOOGLE_ADS_CLIENT_ID!,
client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET!,
developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN!,
});
const customer = client.Customer({
customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID!,
login_customer_id: process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID,
refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN!,
});
interface AdPerformance {
adGroupAdId: string;
campaignName: string;
headlines: string[];
descriptions: string[];
impressions: number;
clicks: number;
costMicros: number;
conversions: number;
ctr: number;
cpa: number;
}
async function getAdPerformance(days = 7): Promise<AdPerformance[]> {
const rows = await customer.query(`
SELECT
ad_group_ad.ad.id,
campaign.name,
ad_group_ad.ad.responsive_search_ad.headlines,
ad_group_ad.ad.responsive_search_ad.descriptions,
metrics.impressions,
metrics.clicks,
metrics.cost_micros,
metrics.conversions
FROM ad_group_ad
WHERE segments.date DURING LAST_${days === 30 ? "30" : "7"}_DAYS
AND ad_group_ad.status = 'ENABLED'
`);
return rows.map((r: any) => {
const impressions = Number(r.metrics.impressions ?? 0);
const clicks = Number(r.metrics.clicks ?? 0);
const cost = Number(r.metrics.cost_micros ?? 0) / 1_000_000;
const conversions = Number(r.metrics.conversions ?? 0);
return {
adGroupAdId: r.ad_group_ad.ad.id,
campaignName: r.campaign.name,
headlines: (r.ad_group_ad.ad.responsive_search_ad?.headlines ?? []).map((h: any) => h.text),
descriptions: (r.ad_group_ad.ad.responsive_search_ad?.descriptions ?? []).map((d: any) => d.text),
impressions,
clicks,
costMicros: Number(r.metrics.cost_micros ?? 0),
conversions,
ctr: impressions > 0 ? (clicks / impressions) * 100 : 0,
cpa: conversions > 0 ? cost / conversions : Infinity,
};
});
}
const HEADLINE_MAX = 30;
const DESCRIPTION_MAX = 90;
function validateAsset(text: string, max: number): boolean {
return text.length <= max;
}
async function rewriteRsa(
ad: AdPerformance,
targetCpa: number,
): Promise<{ headlines: string[]; descriptions: string[] }> {
const anthropic = new Anthropic();
const msg = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{
role: "user",
content: `Rewrite this Google Ads responsive search ad in Alejandro Rioja's voice: direct, operator-focused, specific. It's underperforming — CTR ${ad.ctr.toFixed(2)}%, CPA $${ad.cpa.toFixed(2)} vs a $${targetCpa} target.
Current headlines: ${JSON.stringify(ad.headlines)}
Current descriptions: ${JSON.stringify(ad.descriptions)}
Rules, non-negotiable:
- Each headline: 30 characters MAXIMUM, including spaces and punctuation
- Each description: 90 characters MAXIMUM, including spaces and punctuation
- Return 5 headlines and 4 descriptions
- Return JSON only: {"headlines": ["...", ...], "descriptions": ["...", ...]}`,
}],
});
const text = (msg.content[0] as any).text.replace(/```json\n?/, "").replace(/```/, "").trim();
const parsed = JSON.parse(text);
const headlines = parsed.headlines.filter((h: string) => validateAsset(h, HEADLINE_MAX));
const descriptions = parsed.descriptions.filter((d: string) => validateAsset(d, DESCRIPTION_MAX));
if (headlines.length < 3 || descriptions.length < 2) {
throw new Error("Model returned too many over-limit assets — rerun or revise manually.");
}
return { headlines, descriptions };
}모든 헤드라인과 설명은 생성된 후, API에 닿기 전에 코드 안에서 제한과 대조 검증된다. 모델의 다시쓰기는 초안일 뿐이다. 검증기가 관문이다.
매일 어떻게 쓰는지
> 지난 7일간 구글 광고 확인해줘Claude는 GAQL 쿼리를 실행하고, CTR 3% 미만이거나 저장된 CPA 목표를 초과하는 모든 것을 표시하고, 다시 쓰기 전에 확인을 요청한다. 승인하면 새 헤드라인과 설명을 생성하고, 글자 수 검증기를 통과시키고, 실패한 것을 제외한 뒤, 구글 광고 API의 mutate 작업을 통해 PAUSED 상태의 새 광고를 만든다. 새 문구를 기존 것과 구글 광고 안에서 직접 비교하고, 실제로 쓸 만한 것을 활성화하고, 나머지는 일시중지 상태로 둔다.
페이스북 버전과 같은 상한선이다 — UI에서 반나절을 쓰는 대신 약 20분. 데이터를 추출하고, 엄격한 제약 안에서 작성하고, 변경을 준비하는 기계적인 부분은 정확히 모델이 나를 대신해서 해야 할 일이기 때문이다.
검증기가 잡아내지 못하는 것
글자 수 계산은 기계적이다. 관련성은 그렇지 않다. 헤드라인이 28자이고 모든 검사를 통과해도, 연결된 검색어에 대해서는 여전히 잘못된 메시지일 수 있다. 이 스킬은 성과가 낮은 광고 그룹이 나쁜 제안 때문인지, 랜딩 페이지와의 불일치 때문인지, 정말로 약한 문구 때문인지 알 수 없다 — 알려줄 수 있는 건 주변 광고와 비교했을 때 문구가 약하다는 것뿐이다. 캠페인 내 모든 광고의 CTR이 낮다면 그건 타겟팅이나 랜딩 페이지 문제이고, 잘못 매칭된 검색 의도에 스킬이 더 많은 다시쓰기 사이클을 돌리게 하면 더 순응적이지만 똑같이 무시당하는 문구만 늘어날 뿐이다.
페이스북 쪽에서 하던 것처럼, 다시쓰기 배치를 승인하기 전에 GAQL 출력을 직접 읽는다. 스킬은 타이핑과 글자 수 세기를 없애준다. 실제로 무엇이 고장났는지에 대한 판단은 없애주지 않는다.
이 작업에 쓰는 도구들
Claude — 페이스북용과 이번 것, 두 광고 스킬 모두가 이를 기반으로 돌아간다. 둘은 다른 API와 더 빡빡한 글자 수 예산에 맞서 같은 다시쓰기-검증 패턴을 공유한다.
구글 광고 API (npm 패키지 google-ads-api) — 원시 gRPC/REST API를 감싸는 TypeScript 래퍼. OAuth 리프레시와 GAQL 쿼리 구성을 처리해주므로 protobuf를 직접 다룰 필요가 없다.
자주 묻는 질문
Basic Access가 필요한가, 테스트 계정 접근으로 충분한가?
테스트 계정 접근은 구글 광고 테스트 계정에만 작동한다 — 개발에는 유용하지만 실제 지출이 있는 진짜 계정에는 쓸모없다. 이 스킬이 실제 캠페인을 다루기 전에 Basic Access(관리자 계정의 API 센터에서 신청)가 필요하다. 승인은 수동 검토이며 즉시 이루어지지 않으니, 필요해질 때가 아니라 그 전에 신청해 두어야 한다.
왜 최대치인 15개와 4개가 아니라 헤드라인 5개, 설명 4개인가?
구글은 최대 15개의 헤드라인을 허용하지만, Ad Strength와 실제 테스트 모두 진짜로 구별되는 8~10개의 각도를 넘어서면 수익이 줄어든다는 것을 보여준다 — 대부분의 계정은 숫자만 채우는 거의 중복된 헤드라인으로 끝난다. 모델의 출력을 다시쓰기 배치당 새 헤드라인 5개로 제한해서, 같은 문장의 9가지 변형을 걸러내는 대신 진짜로 구별되는 옵션들을 검토한다.
모델의 다시쓰기가 매 시도마다 글자 수 검사를 통과하지 못하면 어떻게 되나?
검증기는 제한을 초과한 자산을 조용히 자르거나 제출하는 대신 오류를 던진다. 실제로는 프롬프트를 한 번 다시 실행하고, 두 번 실패하면 그 특정 헤드라인을 직접 손으로 쓴다. 설명에서 더 자주 일어난다 — 90자는 보이는 것보다 완전한 문장을 담기에 더 빡빡한 공간이다.
관련 글: 플랫폼마다 전용 스킬을 만드는 대신 GA4, Search Console, Bing, Ahrefs, Core Web Vitals를 하나의 Claude 커넥터에서 읽기 전용으로 리포팅하려면 SiteWide MCP를 쓴다 — Claude를 실제 마케팅 데이터에 연결한다는 같은 발상이지만, 광고 플랫폼 하나보다 범위가 더 넓다.
운영자의 결론
반응형 검색 광고 변형을 손으로 쓰고 직접 글자 수를 세고 있다면, 그건 정확히 모델이 맡아야 할 기계적이고 규칙 기반의 작업이다. 진짜 판단이 필요한 부분 — 문구 문제인지 타겟팅 문제인지 — 은 여전히 내 몫이다. 스킬을 만들고, 구글의 제한에 맞춰 작성하고 검증하게 한 뒤, 진단은 스스로 남겨두라.
매주 수요일. 28,400명+ 구독자. 핵심만.
✓ 받은편지함을 확인하세요 — 확인 링크를 클릭해 가입을 완료하세요.
✓ 구독이 완료되었습니다!
✓ 이미 목록에 있습니다.
관련 게시물
Claude Cowork 예약 작업 설정 가이드
Claude Cowork의 예약 작업은 한 번뿐인 프롬프트를 코드나 서버 없이 스스로 반복 실행되는 업무로 바꿔준다. 제대로 설정하는 방법을 소개한다.
AI Agents2026년 대행사가 판매할 수 있는 AI 에이전트 서비스
내 자신의 비즈니스에서 운영 중인 6가지 AI 에이전트 자동화를, 대행사가 범위를 정하고 가격을 매겨 고객에게 판매할 수 있는 서비스 항목으로 재구성했다.
AI AgentsSaaS를 위한 AI 에이전트: 무엇을 먼저 자동화할까
AI 에이전트 의사결정에 사용하는 단계 및 ROI 프레임워크를, SaaS에서 정말로 먼저 자동화할 가치가 있는 워크플로에 적용해봤다.
AI 플레이북을 받아보세요
매주 수요일. 28,400명+ 구독자. 핵심만.
받은편지함을 확인하세요.
확인 이메일을 보냈습니다 — 링크를 클릭해 구독을 완료하세요. 1분 안에 보이지 않으면 스팸함을 확인하세요.
구독이 완료되었습니다.
환영합니다 — 다음 호가 곧 받은편지함에 도착합니다.
이미 목록에 있습니다 — 매주 수요일에 확인하세요.