공개 광고 라이브러리에서 경쟁사 헤드라인 300개를 모았다면, 이제 궁금한 것은 경쟁사들이 실제로 어떤 메시지를 밀고 있는가일 것이다. 이때 가장 흔한 방법은 300개 문구를 채팅창에 한꺼번에 붙여 넣고 “이 광고들의 소구점을 요약해 줘”라고 묻는 것이다. 답은 대개 빠르게 나온다. 가성비를 강조한다, 사용자 경험을 내세운다, 긴급성을 만든다, 밝고 긍정적인 정서를 쓴다. 광고를 하나도 읽지 않고도 쓸 수 있는 말들이다.
데이터가 문제는 아니다. 플랫폼별 광고 라이브러리와 크리에이티브 센터는 공개되어 있고, 자기 계정으로 정상적으로 조회할 수 있으며, 어떤 접근 제한을 우회하는 것도 아니다. 문제는 질문 방식이다. 모델에 집계된 질문을 던졌으니 집계된 답만 받을 수밖에 없다. 광고 300개가 형용사 네 개로 압축되면 정보는 거의 사라지고, 남는 것은 맞는 말이지만 아무 데도 쓸 수 없는 결론뿐이다.
대량 카피 분석에서 먼저 정해야 할 것은 ‘쓸 수 있는 결과’가 무엇인지다. “가성비를 강조한다”는 실행으로 이어지지 않는다. 반면 “절감 약속에 숫자 근거와 기간 제한을 결합한 방식이 이 표본의 40%에서 나타나며, 가격 민감도가 높은 고객을 겨냥한다”는 바로 다음 소재에서 조절할 변수를 알려 준다. 이런 결과는 한 번의 요청으로 얻을 수 없다. 한 단계를 둘로 나눠야 한다.
“소구점을 요약해 줘”가 늘 뭉개지는 이유
원샷 분석이 실패하는 이유는 프롬프트 문구가 부족해서가 아니라 구조 자체에 있다.
모델에게 성격이 전혀 다른 두 작업을 동시에 시키고 있기 때문이다. 첫째는 광고별 정보 추출이다. 이 광고가 어떤 약속을 했는지, 근거를 제시했는지처럼 답이 대체로 하나로 수렴하는 작업이다. 둘째는 추출한 정보를 분류하는 일이다. 어떤 약속들을 같은 범주로 볼지, 경계를 어디에 둘지는 판단이 필요하다. 이 둘을 한 번의 호출에 몰아넣으면 모델은 지름길을 택한다. 광고별 추출은 건너뛰고 텍스트 덩어리에서 받은 인상만 요약해, 듣기 좋은 긍정적 형용사 묶음을 내놓는다.
원샷 방식에는 중간 산출물도 없다. “가성비를 강조한다”는 결론만 있을 뿐, 어느 광고에서 나왔는지, 비중은 얼마인지, 반례는 없는지 추적할 수 없다. 다른 표본으로 다시 돌리면 결론도 달라진다. 중간 산출물이 없는 분석은 검토도 반복 개선도 할 수 없다.
광고별 필드 추출과 클러스터링을 분리하라
해법은 간단하다. 중간에 구조화 필드 계층을 두고 작업을 두 단계로 분리하는 것이다.
1단계에서는 광고 문구를 하나씩 고정된 스키마에 맞춰 필드로 추출한다. 최소한 핵심 약속, 근거 유형, 긴급성 장치, 암시된 타깃 고객이라는 네 축은 필요하다. 이 단계는 광고 하나만 보고, 설명문 없이 엄격한 JSON만 출력하게 한다.
You will receive one ad headline. Break it apart along the fixed fields below.
Output JSON only, no explanation.
<copy>
{{one piece of copy}}
</copy>
Fields and allowed values (pick only from the given enum; when unsure pick
unknown; do not invent values):
- promise: [save money, save time, look better, get healthier, make money,
learn a skill, belong, identity, unknown]
- evidence: [testimonial, data/numbers, authority, before/after, demo,
none, unknown]
- urgency: [time limit, scarcity, price-rise warning, fear of missing out,
none, unknown]
- audience: a short phrase, inferred from the wording, for who it's talking to
(e.g. "night owls", "moms with kids", "junior designers")
Output:
{"promise":"...","evidence":"...","urgency":"...","audience":"..."}
광고 300개를 처리하고 나면 텍스트 블록 300개 대신 구조화된 레코드 300개를 갖게 된다. 이 시점부터 “소구점을 요약해 줘”는 모호한 의미 분석이 아니라, 수를 세고 묶고 분포를 그릴 수 있는 데이터 작업으로 바뀐다.
2단계는 클러스터링이다. 단, 원문이 아니라 필드를 클러스터링해야 한다. 원문 텍스트를 묶으면 다시 의미적 유사성이라는 안개 속으로 들어간다. 필드를 묶으면 몇 개의 이산 차원 위에서 명확한 경계로 그룹을 만들 수 있다.
클러스터링 결과에는 반드시 반례를 붙여라
클러스터링 프롬프트의 핵심은 “이것들을 그룹화해 줘”가 아니다. 모델이 직접 반례를 꺼내 오게 만드는 데 있다.
Below are N ad headlines already extracted into structured records.
The categories are frozen. Do not add categories.
<records>
{{JSON array, each with promise/evidence/urgency/audience}}
</records>
Output three things in order:
1. Combination clustering
Group by (promise x evidence x urgency), and give each group's record
count and one representative sample.
2. Counterexample check
For the top 3 groups by record count, pick one record per group whose
audience clearly departs from the group's mainstream, and explain why it
got grouped there. Is it really the same selling point, or did step one
extract a field wrong?
3. Gaps
Which combinations that should be common don't appear even once in this
batch? Are those gaps "nobody's doing it" or "my sample didn't cover it"?
이 프롬프트의 가치는 두 번째 항목에서 나온다. 모델은 기본적으로 사용자가 원하는 방향에 맞춰 주려는 경향이 있다. 따로 압박하지 않으면 모델이 만든 모든 클러스터는 지나치게 일관돼 보인다. 방금 만든 그룹에서 잘 맞지 않는 레코드 하나를 직접 찾아내게 하면, 모델은 결론을 냈다고 느낀 뒤에도 계속 검토해야 한다. 그 과정에서 분류 경계가 너무 거칠다는 사실이나 1단계 필드 추출 오류가 드러난다.
모델이 자기 결과를 스스로 파고들게 만드는 방식은 광고 분석에만 쓰이지 않는다. 코드 리버스 엔지니어링에서는 이를 반증 근거 섹션으로 부르기도 한다. 모델이 알고리즘 계열을 알아봤다고 해서 그대로 믿지 않고, “표준 구현과 다른 지점은 어디인가”를 답하게 만드는 것이다. 이는 난독화 코드를 다루거나 알고리즘을 핑거프린팅할 때 모델이 사용자의 가정에 쉽게 동의해 버리는 성향을 막는 원리이기도 하다. 카피 클러스터링에서는 반례 검토가 바로 그 반증 근거 섹션이다.
단계마다 모델을 다르게 써야 하는 이유
두 단계가 모델에 요구하는 역량은 정반대다. 하나의 모델로 모두 처리하는 것은 낭비다.
단계 | 필요한 역량 | 권장 모델 | model id |
|---|---|---|---|
광고별 필드 추출(수백~수천 건, 건별 호출) | 낮은 비용, 높은 동시성, 안정적인 구조화 출력 | Claude Sonnet 5 |
|
카테고리 설정(전체 표본을 한 번 읽고 enum 도출) | 긴 컨텍스트 | Kimi K3 |
|
필드 클러스터링, 반례 검토, 공백 탐색 | 강한 추론 능력, 자기 결과를 다시 검토하는 성향 | Claude Opus 5 |
|
빈도 귀속 판단(높은 빈도가 효과인지, 모방 확산인지) | 중간 수준 추론, 데이터를 근거로 설명하는 능력 | GPT-5.6 Sol |
|
비용이 자릿수 단위로 벌어지는 이유도 이 분리에 있다. 필드 추출만 데이터량에 정비례한다. 광고 300개면 300회 호출이고, 1,000개면 1,000회다. 반면 클러스터링은 배치당 한 번만 실행한다. 한두 번만 돌리는 클러스터링에는 가장 비싼 추론 티어를 쓰고, 수백 번 호출하는 추출에는 가장 저렴한 티어를 써야 총비용이 10배까지 달라진다. 이를 거꾸로 하면 흔히 보는 낭비가 된다. 추출에는 그만큼의 추론이 필요 없는데 비용은 커지고, 정작 저렴한 티어가 약한 클러스터링 단계에서 분석이 무너진다.
티어를 낮췄을 때 실제로 얼마나 절감되는지는 직접 한 번 돌려 보면 된다.
이미 수집한 광고에서 20~30개를 뽑는다.
필드 추출: 같은 배치를
claude-sonnet-5와claude-opus-5로 각각 처리한 뒤, 레코드별 네 필드가 얼마나 일치하는지 비교한다.클러스터링: 동일한 추출 레코드를 두 티어로 클러스터링하고 두 가지를 본다. 반례 검토가 실제로 그룹을 파고드는지, 아니면 후보를 다시 말할 뿐인지. 그리고 공백 항목이 실행 가능한 다음 행동을 가리키는지도 확인한다.
결론은 명확하다. 추출 일치율이 높다면 이 단계는 저렴한 티어로 내려 비용을 줄인다. 반대로 저렴한 티어가 클러스터링에서 유의미한 반례를 만들지 못한다면, 이 단계는 추론 티어에 남겨야 한다.
함정 1: 모델이 카테고리까지 정하게 두는 것
추출 단계에서 enum을 제공하지 않고 모델이 카테고리를 직접 만들게 두는 것이 가장 빠지기 쉬운 함정이다.
한 배치만 보면 괜찮아 보인다. 그럴듯한 카테고리 목록이 나오기 때문이다. 문제는 두 번째 배치부터다. 같은 유형의 카피인데도 카테고리 이름이 달라지고, 분류의 세밀함이 바뀌며, 경계도 움직인다. 두 배치를 나란히 놓고 추세를 보려 하면 서로 맞물리지 않는다. 첫 배치의 “discount”와 두 번째 배치의 “save money”는 같은 것인가? 알 수 없다. 배치마다 카테고리가 흔들리면 배치 간 비교는 전부 가짜가 된다.
해결책은 카테고리를 정하는 일과 사용하는 일을 분리하는 것이다. 먼저 긴 컨텍스트 티어로 큰 표본을 한 번에 읽게 한다. 수백 건씩 읽어 enum을 포괄적이면서도 같은 세밀도로 도출하는 작업은 정확히 긴 컨텍스트가 맡을 일이다. 이후 사람이 직접 검토하고 고정한다. 그다음부터 추출 단계는 고정된 enum에서만 선택하게 하며, 예외를 담을 값으로 unknown을 둔다. 현장에서 새 카테고리를 만들게 해서는 안 된다. 추출 프롬프트의 “pick only from the given enum, do not invent”라는 문구는 바로 이 제약을 명시한 것이다.
한 줄로 정리하면 이렇다. 카테고리는 사람이 정하고, 모델은 정해진 칸만 채운다.
함정 2: 빈도를 효과로 읽는 것
추출과 클러스터링을 마치면, 자연스럽게 레코드 수로 정렬하고 가장 자주 나타난 소구점을 ‘가장 효과적인’ 방식으로 해석하게 된다. 하지만 이 단계에는 놓치기 쉬운 오류가 있다.
빈도가 높다는 말은 모두가 그렇게 쓰고 있다는 뜻일 뿐, 그렇게 쓰는 것이 효과가 있다는 뜻은 아니다. 광고에는 실제로 모방 확산이 존재한다. 한 광고가 반응을 얻으면 일주일 안에 시장 전체가 따라붙고, 크리에이티브 센터에는 비슷한 헤드라인 수십 개가 순식간에 등장한다. 빈도 집계는 이 모방 광고까지 모두 포함한 뒤 “이 소구점이 가장 인기 있다”고 결론 내린다. 하지만 그 광고들이 집단으로 성과가 떨어지고 있는데도 누구도 먼저 멈추지 못하는 상황일 수 있다. 빈도가 측정하는 것은 효과가 아니라 동조다.
빈도와 효과를 분리하려면 각 레코드에 성과 데이터를 붙여야 한다. 공개 광고 라이브러리의 각 소재에는 재생 수, 좋아요, CTR 같은 필드가 있다. 실제로 정렬해야 할 값은 단순 레코드 수가 아니라 “특정 소구점 조합이 높은 성과 백분위에 들어가는 비율”이다. 초 단위 유지율과 CTR 백분위를 어떻게 의미 있는 신호로 읽을지는 다른 글에서 다룬다. 여기서 강조할 점은 하나다. 빈도 표와 효과 표는 반드시 별개의 표여야 한다. 둘을 하나로 합치는 순간 결론이 망가진다.
이 단계에서 모델의 역할은 정렬이 아니라 귀속 판단이다. 빈도가 높은 조합과 성과 분포를 중간 추론 티어에 함께 넣고, “이 빈도는 실제 효과 때문인가, 아니면 서로 베낀 결과인가”를 수치에 근거해 판단하게 한다. 귀속 판단이 틀려도 최종적으로 실제 성과 데이터로 검증하기 때문에 비용은 크지 않다.
함정 3: 잘된 광고만 분석하는 것
세 번째 함정은 데이터 소스에 숨어 있다. 주의하지 않으면 알아차리기도 어렵다. 광고 라이브러리와 크리에이티브 센터는 기본적으로 성과가 좋았던 광고를 보여 준다. “hot ads”나 “Top Ads” 같은 항목은 사실상 플랫폼이 성과 기준으로 미리 걸러 낸 생존자 집합이다.
성공한 광고만 모아 클러스터링하고 “성공 광고의 공통점”을 찾은 다음 따라 하는 것은 전형적인 생존자 편향이다. 실패한 광고에도 똑같은 특성이 있을 가능성이 높기 때문이다. 예를 들어 성공 광고의 90%가 “time limit”을 썼고, 그래서 기간 제한이 효과적이라고 결론 내렸다고 하자. 그러나 실패한 광고의 90%도 기간 제한을 썼다면 이 특성에는 성공과 실패를 가르는 힘이 전혀 없다. 업계의 기본값일 뿐, 성과와는 무관한 것이다.
해결책은 성공 광고에 대조군을 붙이는 것이다. 공개 광고 라이브러리에서는 보통 업종, 마케팅 목표, 기간으로 필터링할 수 있다. 같은 필터를 적용해 노출은 있었지만 참여도가 명확히 낮았던 소재도 찾고, 이들 역시 추출과 클러스터링한 뒤 성공군 옆에 둔다. 중요한 것은 “성공 광고에 있는 것”이 아니라 “성공 광고에는 있고 실패 광고에는 없는 것”이라는 차이 집합이다. 이 차이 집합에 나타나는 특성만 다음 소재에 반영할 가치가 있다.
완전한 실패 광고 표본을 얻을 수는 없고, 그럴 필요도 없다. Top Ads만 보고 내린 결론보다 작은 규모라도 저성과 대조군을 둔 분석이 훨씬 신뢰할 만하다.
최종 산출물은 요약문이 아니라 변수표다
두 단계를 거치고 세 가지 함정을 피했다면, 최종 결과는 “이 표본의 경쟁사는 XX를 강조한다” 같은 문단이어서는 안 된다. 그렇게 하면 다시 뭉개진 요약으로 돌아간다. 결과물은 변수표여야 한다.
각 차원(약속, 근거, 긴급성, 타깃 고객)이 어떤 값을 가질 수 있는지.
어떤 조합이 이미 효과가 검증됐는지(차이 집합에 있고 성과 백분위도 높은 조합).
아직 아무도 시도하지 않은 조합이 무엇인지(공백 항목에서 발굴).
이 표는 곧바로 생성 작업의 입력값으로 쓸 수 있다. 효과가 검증된 조합은 변형안을 대량 제작하는 쪽으로 보내고, 비어 있는 조합은 저비용 탐색 실험으로 보낸다. 카피 차원의 변수는 텍스트 모델이 스크립트를 쓰는 데 쓰고, 시각 요소와 톤 변수는 이미지·영상 모델이 소재를 제작하는 데 쓴다. 이렇게 경쟁사 카피 분석부터 자사 완성 광고까지 이어지는 선이 닫힌다. 이 변수표는 크리에이티브 파이프라인의 브리프 단계가 읽는 바로 그 입력값이다.
클러스터링의 목적은 보기 좋은 분류 차트를 만드는 일이 아니다. 다음 제작 배치를 움직일 변수표를 만드는 데 있다.
네 가지 티어를 하나의 키로 연결하기
이 흐름에는 네 가지 티어가 필요하다. 추출용 저비용·고동시성 티어, 카테고리 설정용 긴 컨텍스트 티어, 클러스터링용 추론 티어, 귀속 판단용 중간 추론 티어다. 이들은 여러 공급사와 여러 SDK, 인증 방식, 오류 형식에 흩어져 있다. 단계마다 모델을 바꾸기 위해 클라이언트를 여러 인터페이스에 연결하는 일은 번거롭다. 그래서 대부분은 결국 하나의 모델로 모든 일을 처리하고, 앞서 말한 낭비에 빠진다. 비싼 티어를 추출에 써서 비용을 태우거나, 저렴한 티어가 클러스터링에서 반례를 만들지 못하는 식이다.
AIReiter는 이 계층을 하나로 평탄화한다. 키 하나와 OpenAI 호환 인터페이스 하나로 네 가지 티어를 모두 쓸 수 있으며, 요청 본문의 model 필드만 바꾸면 된다.
# Extract fields: the cheap high-concurrency tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"messages": [{"role": "user", "content": "<extraction prompt + one piece of copy>"}]
}'
# Cluster: switch to the reasoning tier, leave the rest
# "model": "claude-opus-5"
# Frequency attribution:
# "model": "gpt-5.6-sol"
이미 OpenAI SDK를 쓰고 있다면 base_url을 https://aireiter.com/api/v1로 지정하면 된다. 나머지는 바꿀 필요가 없다. Anthropic SDK에서는 같은 키로 POST /api/v1/messages를 호출한다.
가격 기준으로 Claude 모델은 정가 대비 30% 할인, GPT 모델은 절반 가격이며, 이 할인은 이 흐름에서 가장 비용 비중이 큰 부분에 적용된다. 필드 추출은 데이터량에 선형으로 비례하는 유일한 단계다. 광고 300개면 300회, 1,000개면 1,000회 호출한다. 가장 저렴한 Sonnet 티어에 추가로 30% 할인이 적용되므로, 절감 효과의 대부분이 여기서 나온다. 클러스터링과 귀속 판단은 배치당 몇 번만 실행하므로 추론 티어를 써도 부담이 크지 않다. 카테고리 설정은 같은 키로 이용할 수 있는 긴 컨텍스트 Kimi K3에서 배치당 한 번 실행하면 된다.
가입 없이 사용해 보기: 먼저 광고 몇 개를 손으로 처리하면서 저렴한 티어와 추론 티어의 추출 일치율, 클러스터링 반례를 비교해 본다. 결과가 안정적이면 그때 스크립트로 자동화하면 된다.
마무리
대량 광고 카피 분석이 뭉개지는 이유는 모델의 성능이 부족해서가 아니다. 추출과 분류를 한 번의 호출에 억지로 넣었기 때문이다.
두 단계로 나눠라. 고정된 enum을 기준으로 광고별 구조화 필드를 추출하고, 이 작업은 저렴한 티어에서 수백 번 실행한다. 그다음 필드를 클러스터링하면서 모델이 반례를 내놓도록 강제하고, 이 작업은 추론 티어에서 한 번 실행한다. 세 가지 함정도 계속 경계해야 한다. 모델이 현장에서 카테고리를 만들게 두지 말고, 빈도를 효과로 해석하지 말며, 성공 광고에는 대조군을 붙여야 한다. 마지막에 남아야 할 것은 “가성비를 강조한다”는 문장이 아니라 다음 제작 배치를 움직이는 변수표다.
이 흐름에서 모델은 사실 두 가지 도구다. 저렴한 추출기와, 자기 결과를 다시 파고드는 분류기다. 역할에 맞는 티어에서 둘을 분리해 쓰면 광고 수백 개로도 실행 가능한 결론을 얻을 수 있다. 하나의 모델에 전부 몰아넣으면 결국 형용사만 남는다.