Claude Code로 경쟁사 X 분석 자동화하기: 루프 엔지니어링으로 만드는 AI 스페셜리스트 팀
목차
매주 나가는 경쟁사 X(구 트위터) 동향 리포트를, Claude Code로 "데이터 수집 → 변화 분석 → 검증 → PowerPoint 생성"까지 반자동으로 돌리고 있어요. 설계의 밑그림은 루프 엔지니어링(Addy Osmani, Loop Engineering)이에요. "설계의 대상을 '1회의 동작'에서 '계속 돌아가는 구조 전체'로 끌어올린다"는 사고방식이죠.
다만 소재가 **"수치 KPI"가 아니라 "정성적인 경쟁 분석"**이었던 탓에, 정석대로는 굴러가지 않았어요. 이 글은 이 구조를 어떻게 설계했는지를 실제 코드로 구체화한 기록이에요. 결론부터 말하면 ——
정성 리포트에서는 결정론 체크(A)의 사정거리가 놀랄 만큼 좁고, 주역은 다른 모델의 평가 담당(B)이 된다.
이 한 가지를 중심으로, 루프의 골격과 각 파츠의 구현을 차례로 살펴볼게요.
소재는 실재하는 경쟁사 X 계정(공개 정보)이지만, 평가 기준·자사 액션·실제 수치는 전부 가공의 샘플로 바꿔 놨어요. 기법만 가져가 주세요(정보는 2026/06 말 기준).
대상 독자
- 정례 "조사·분석 리포트"를 매주 손으로 만들고 있고, AI에게 맡기고 싶지만 품질이 불안한 사람
- 단발성 AI 자동화는 해 봤지만, "계속 돌아가는 구조"로 만들고 싶은 사람
- 출력이 숫자가 아니라 문장·판단의 비중이 높은 리포트를 자동화하고 싶은 사람 (여기가 이 글의 주안점)
이 글에서 얻을 수 있는 것
- "생성 담당과 평가 담당을 나눈다"를, 재계산할 수 없는 정성 리포트에서 어떻게 구현하는가
- 정말 막아야 할 사고는 "레이아웃 깨짐"이 아니라 **"빈 데이터에서의 날조"**라는 관점과, 그 잡는 법
- 회사 이름을 몰라도 브랜드 컬러 규칙을 강제하는 결정론 체크의 잔기술
- 전부 들어 있는 검증 스크립트 (표준 라이브러리 + python-pptx뿐, 그대로 갖다 써도 됨)
소재: 경쟁사 X 주간 리포트 파이프라인
하는 일은 단순해요.
- 수집: 경쟁 6개사 공식 X 계정의 타임라인을, 로그인된 브라우저 너머로 스크레이프 (게시글 본문·일시·views/RP)
- 분석: 전주 리포트(
latest.md)와의 차이를 뽑아서, "이번 주 무엇이 달라졌나·전략의 의도·자사에 주는 시사점"을 Markdown으로 생성 - 생성: 고정 템플릿 PowerPoint에 텍스트만 갈아 끼워 경영진용 슬라이드화
- 검증 → 저장
흔한 수치 집계 리포트(KPI·장애 건수 등)와의 결정적인 차이는, **성과물의 내용이 "숫자"가 아니라 "소견"**이라는 점이에요. 건수 집계라면 원시값에서 다시 합산해 대조할 수 있지만, 경쟁 분석의 내용은 "어느 경쟁사는 할인 정책을 '간판 이벤트화'했다(관찰)", "노림수는 휴면 사용자를 다시 불러들이는 것일 터(추측)" 같은 판단이에요. 인용하는 82,000 views 같은 숫자도 X의 자릿수를 뭉뚱그린 표시의 개산이라, 산술적으로 다시 끌어낼 수 없어요.
이 "재계산 불가"를 전제로, 루프는 이렇게 짰어요.
전체 구성: 오케스트레이션 프롬프트가 "루프 본체"
실제 기동은, "순서"와 "중지 조건"만 적은 한 장의 프롬프트에서 시작해요. 이게 루프 본체이고, 실작업은 2개의 대본에 통째로 위임해요. Boris Cherny의 *"I just write loops"*를 정례 리포트로 옮기면 이렇게 돼요.
다음을 순서대로 실행해 주세요.
1. prompt/weekly_x_competitor_prompt.md 를 읽어 들여, 지시에 따라 실행한다
(리포트 생성·저장까지)
2. latest.md 의 작성일이 오늘 날짜로 갱신되어 있는지 확인한 뒤,
prompt/competitor_slides_prompt.md 를 읽어 들여, 지시에 따라 실행한다
(pptx 생성·저장까지)
스텝 1이 실패한 경우(latest.md 가 갱신되지 않은 경우)에는,
스텝 2로 넘어가지 말고 이유를 보고하고 종료해 주세요.
이 프롬프트는 아무것도 생성하지 않아요. 하는 일은 ① step1 → step2의 순서, ② **스텝 사이의 게이트(latest.md 작성일이 오늘인가)**뿐. 핵심은, 다음 공정으로 넘어갈 판단을 **생성 담당의 "다 했습니다"라는 자기 신고가 아니라 파일의 상태(작성일)**로 정한다는 점이에요. "완료는 본인이 선언하게 두지 않는다"를 검증만이 아니라 오케스트레이션 층에도 적용한 거죠.
구성은 3층이에요.
| 층 | 역할 | 내용(개요) |
|---|---|---|
| 오케스트레이션 | 루프 본체 = 순서 + 중지 조건 | 위 프롬프트. step1 → (작성일 게이트) → step2 |
| 대본①: 리포트 | 데이터 → Markdown | 조사 대상(A/B/C그룹) / 저장 요건(latest.md + 원시값 아카이브) / 분석 절차(차이·이상값·전략 추측) / 출력 형식 / 데이터 충분성 게이트 / 검증 2단(A·B) |
| 대본②: 슬라이드 | 리포트 → PowerPoint | 템플릿 방식의 절대 규칙 / 7장 슬라이드 갈아 끼우기 대응 / 브랜드 컬러 팔레트 / 작업 절차 / 결정론 체크(verify_deck) |
오케스트레이션이 위임하는 2개 대본의 내용을 발췌로 보여 드릴게요 (실명·실수치는 가려 놨어요). 이걸 보면 한 장의 오케스트레이션 프롬프트가 "무엇을 맡기고 있는지"가 구체적으로 잡혀요.
대본①: 리포트 (weekly_x_competitor_prompt.md / 발췌)
# 역할
경쟁 전자책 앱의 X 계정에서 "이번 주의 변화"를 추출하고, 지난 리포트와의 차이를 낸다.
# 브라우저 제약 (엄수)
- 기존 로그인된 브라우저(127.0.0.1:9222)에만 접속한다. 신규/자동 조작 전용 브라우저는 켜지 않는다.
- 접속할 수 없으면 대체 수단을 쓰지 말고 정지하고 보고한다.
# 조사 대상
- A. 직접 경쟁(4개사) … 게시 유형·소구·캠페인·유입 동선까지 상세하게 차이를 뽑는다
- B. 시장 트렌드 그룹(2개사) … 새 정책/새 소구/고반응 게시글의 유무만 가볍게 확인
- C. 월간 그룹(1개사) … 실행일이 그달의 첫째 주일 때만 확인
# 저장 요건
- latest.md 에 덮어쓰기 + weekly_x_competitor_report_YYYY-MM-DD_wXX.md
- 스크레이프 원시값을 raw/YYYY-MM-DD_wXX/ 에 아카이브 (삭제하지 않는다)
# 출력 규칙 (달라진 것만 · 사실/관찰/추측을 나눈다 · 추측은 명기 · 미확인은 "확인 불가")
# 출력 형식
1.이번 주 요점(3개) / 2.직접 경쟁의 변화 / 3.시장 트렌드 / 4.시장 전체 /
5.이상값 / 6.전략 변화의 해석(추측) / 7.자사 액션(우선도 A/B/C) / 8.다음 주 주목 / 9.저장 결과
# 데이터 충분성 게이트 (생성 전에 판단)
- A그룹에서 2개사 이상 수집 불가라면, 생성하지 말고 중지하고 이유를 보고한다
# 검증 (평가 담당 · 2단)
(A) verify_report.py (결정론 · 최대 3회) → (B) 다른 모델로 원시값과 대조 (최대 2회)
대본②: 슬라이드 (competitor_slides_prompt.md / 발췌)
# 목적
경쟁 X 분석을, 비기술자도 "무엇을 의사결정해야 하는지" 알 수 있는 형태로 슬라이드화한다.
# 템플릿 방식의 절대 규칙
- python-pptx 로 템플릿을 열고, 텍스트 갈아 끼우기만 한다.
- 도형·카드·띠의 추가/이동/크기 변경/색 변경/폰트 변경은 금지.
- 갈아 끼우기는 run 단위로 원래 서식을 유지한다. 템플릿 본체는 바꾸지 말고 다른 이름으로 저장한다.
# 슬라이드 구성 (전 7장)
1.타이틀 / 2.이번 주 결론(번호 카드 3) / 3.시장의 변화 / 4.경쟁의 움직임(수치 카드 3) /
5.이상값 / 6.자사에 주는 시사(키워드 3) / 7.액션 플랜(A-1/A-2)
# 브랜드 컬러 (색 변경 금지의 "유일한 예외")
- 각사 지정 색을 사명 부분에 적용한다. 밝은 칠 위의 글자는 반드시 진한 색 (흰 글자는 금지).
# 검증 (평가 담당 · 결정론)
- verify_deck.py 를 실행한다. 푸터/주차/날짜 일치·갈아 끼우기 누락·글자 수 초과·팔레트 규칙을 체크 (최대 3회 수정).
여기서 효과를 내는 건, 대본 쪽에도 "멈추는 조건"이 적혀 있다는 점이에요. 대본①의 "접속할 수 없으면 정지", "A그룹 2개사 이상 빠지면 중지", 대본②의 "텍스트 갈아 끼우기만·도형과 색은 건드리지 마라". 오케스트레이션이 순서와 게이트를, 대본이 현장의 금지 사항과 검증을 쥔다. 이 이중 구조가 효과를 내요.
전체 흐름을 그림으로 그리면 이래요. 중지 조건 = 게이트를 마름모로 명시한 게 포인트예요 (여기서 멈출 수 있다는 것이 루프의 안전장치예요).
"순서와 게이트만 적는다", "실작업과 검증은 대본에 위임한다". 이 분리가, 매주 같은 품질로 루프를 돌릴 수 있는 토대가 되고 있어요. 참고로 역할의 주고받기(생성 담당 → 다른 모델의 평가 담당)를 주제로 한 2단 검증은, 다음 절에서 시퀀스 도표로 보여 드릴게요.
루프의 골격: 6파츠 할당
Addy는 루프에 필요한 도구 일습을 6파츠로 정리하고 있어요. 이 파이프라인에서는 Claude Code와 주변 기능을 이렇게 할당했어요.
| 파츠 | 역할 | 이 파이프라인에서의 구현 |
|---|---|---|
| Automations | 기동 | 주간 트리거 (수동 기동도 가능. X 로그인 상태에 의존하므로 현재는 반자동) |
| Connectors(MCP) | 외부 접속 | 브라우저 조작 MCP로 로그인된 X의 타임라인을 수집 |
| Skills / 대본 | 절차의 고정 | weekly_x_competitor_prompt.md / competitor_slides_prompt.md 에 검증 절차까지 명문화 |
| Sub-agents | 생성 담당과 평가 담당의 분리 | 생성은 본체 / 검증은 다른 모델(예: Haiku)의 서브에이전트 |
| Memory | 상태의 영속화 | latest.md (전주 비교의 기준) + raw/ 원시값 아카이브 |
| 결정론 체크 | 토큰 불요 게이트 | verify_report.py / verify_deck.py (뒤에서 설명) |
이 중에서, 정성 리포트이기에 더 무겁게 효과를 낸 것이 **Sub-agents(평가 담당의 분리)**와 그것을 대본에 고정하는 것, 그리고 폭주를 멈추는 하드 상한이에요. 순서대로 구현을 볼게요.
설계①: 생성 담당과 평가 담당을 나눈다 — 다만 (A)의 사정거리는 좁다
검증은 2단. 여기가 이 글의 핵심이에요. Addy의 주장은 "코드를 쓴 본인은 자기 작업에 무르다(A model grading its own output is too generous)". 그래서 생성 담당과 평가 담당을 나누고, 평가 담당은 가능하면 다른 모델로 둬요.
(A) 결정론 체크: 토큰 없이 걸러 낼 수 있는 것만
수치 집계 리포트라면 "원시값에서 다시 합산해 대조"가 주역이 돼요. 정성 리포트에서 결정론으로 걸러 낼 수 있는 건 이것뿐이에요.
- 작성일이 당일인가
- 비교 대상(전주 파일명)이 명기되어 있는가
- 수집 상황(수집 불가의 유무)이 명기되어 있는가
- 리포트 속 쉼표 구분 숫자가, 스크레이프 원시값 어딘가에 존재하는가 (= 옮겨 적기 실수 검출. 산술적인 재계산은 못 하지만, "원시값에 없는 숫자 = 어딘가에서 가져왔다는 의심"은 기계적으로 말할 수 있다)
4번만이 "숫자의 검증"이고, 그마저 재계산이 아니라 존재 체크까지 약해져 있다는 게 포인트예요.
# scripts/verify_report.py (발췌)
# 쉼표 구분 숫자(4자리 이상)를 추출해, 원시값 아카이브에 존재하는지 확인
figs = sorted(set(re.findall(r"\b\d{1,3}(?:,\d{3})+\b", md)))
unmatched = []
for fig in figs:
plain = fig.replace(",", "")
if (fig in blob) or (plain in blob): # blob = raw/*.json 을 결합한 원시값
ok(f"숫자 {fig} 는 원시값에 존재")
else:
unmatched.append(fig) # 파생값 or 옮겨 적기 실수 의심 → (B)에서 확인
"82,000(원시값의 표시값)은 원시값에 있다 = OK", "8.2만 view(내가 본문용으로 뭉뚱그린 파생값)는 원시값에 없다 = WARN으로 (B)에 보낸다". 결정론으로 말할 수 있는 건 여기까지, 라고 선을 그어요.
(B) 다른 모델의 평가 담당: 여기가 주역
정성 리포트의 타당성 —— 사실/관찰/추측의 구분, 다른 주·다른 회사에서의 뒤바뀜, 수집 불가 계정을 추측으로 메우지 않았는가 —— 는 사람과 마찬가지로 "읽고 판단"하지 않으면 알 수 없어요. 그래서 (A)가 PASS하면, 생성 담당과는 다른 모델(예: Haiku)의 서브에이전트에게 latest.md와 원시값 raw/*.json을 읽혀서 대조시켜요.
대본(프롬프트)에는 이렇게 고정해 뒀어요.
## (B) 검증 서브에이전트 (다른 모델로 문장을 점검)
- (A) 가 PASS 하면, 다른 모델(예: Haiku)에게 latest.md 와 raw/<...>/*.json 을 읽혀서,
- 각 "변화·관찰"이 원시값(실제 게시글)에 뒷받침되는가 (날조·뒤바뀜이 없는가)
- "사실 / 관찰 / 추측"의 구분이 적절한가
- "수집 불가"를 추측으로 메우지 않았는가
를 점검시킨다. 수정이 필요하면 고치고 (A) 부터 다시 한다. 왕복은 최대 2회.
수치 집계 리포트에서는 (A)가 주역·(B)가 재확인 정도가 되기 쉽지만, 정성 리포트에서는 비중이 역전돼요. 이게 "숫자를 재계산할 수 없는 리포트"에서의 최대 설계 판단이었어요.
설계②: 데이터 충분성 게이트 — 진짜 사고는 "빈 데이터에서의 날조"
평가 담당을 넣어도, 애초에 입력 데이터가 비어 있으면 날조를 사후에 검출할 뿐이에요. 그래서 생성 전에 멈추는 결정론 게이트를 둬요.
# 데이터 충분성 게이트 (생성 전에 반드시 판단)
- A그룹(직접 경쟁 4개사)에서 최근 7일의 새 게시글이 1건도 안 잡힌 계정은 "수집 불가"로 명기한다.
- A그룹에서 2개사 이상 수집 불가라면, 차이 분석의 토대가 무너지므로 리포트를 생성하지 말고 중지하고 이유를 보고한다.
"못 잡았으면 추측으로 메운다"가 아니라 "못 잡았으면 멈춘다/명기한다". 당연한 말이지만, AI에게 맡기면 제일 먼저 무너지는 규율이라 코드와 대본 양쪽에 적어 놨어요. X는 로그인 요구로 게시글이 안 잡힐 때가 있고, 안 잡혔는데도 전주의 기억이나 "그럴듯한 추측"으로 메워진 리포트가 겉모습만 깔끔하게 나와 버리는 것이 제일 무서운 사고거든요.
설계③: 원시값 아카이브 — 지연과 "나중에 검증"에 대비한다
루프 엔지니어링에서 제일 AI답다고들 하는 함정이 "지난주 값이 다음 날에는 바뀌어 있었다(지연 집계)"예요. 경쟁 X에서도 views/팔로워는 시간에 따라 움직여요. 그래서 스크레이프 원시값을 매주 raw/YYYY-MM-DD_wXX/에 저장하고, 지우지 않아요.
- 저장 위치: raw/YYYY-MM-DD_wXX/<account>.json (followers, 각 게시글의 dt/txt/aria 포함)
- 이 아카이브는 삭제하지 않는다 (대조 가능한 원본으로 남긴다)
이걸로 "전주부터 계속" 같은 변화 주장을, 나중에 원본에 대 보고 검증할 수 있어요. (A)의 숫자 추적 가능성도 이 원본이 있어야 비로소 성립해요.
설계④: 슬라이드는 고정 템플릿 + 텍스트만 + "회사 이름을 모르는" 색 체크
슬라이드 생성의 원칙은 단순해요. 색·레이아웃·폰트는 AI가 못 건드리게 하고, 텍스트 문구만 갈아 끼운다 (python-pptx로 run 단위 치환). 겉모습을 고정하면 매주의 변화가 "숫자와 소견"만 남아서, 리뷰가 순식간에 끝나요.
재미있는 건 그 결정론 체크 쪽의 잔기술이에요. 슬라이드 4의 각사 헤더 바는 주에 따라 실리는 회사가 바뀌어요 (이번 주는 A사/B사/C사, 다음 주는 다른 3개사일지도 몰라요). 그래서 "A사는 노란색"이라고 못 박아 검사하면 금방 부서져요. 대신 **회사 이름을 몰라도 성립하는 "규칙 그 자체"**를 검사해요.
# slides/verify_deck.py (발췌): 팔레트 규칙을 회사 비의존으로 강제한다
BRAND_FILL = {"F6AB00","E60012","0D2F54","2D5FAA","FFD400","06C755"} # 6개사의 칠
BRIGHT_FILL = {"F6AB00","FFD400"} # 밝은 칠 → 글자는 반드시 #2B2924
for bar, txt, card in [("Shape 5","Text 6","S4 카드1"),
("Shape 11","Text 12","S4 카드2"),
("Shape 17","Text 18","S4 카드3")]:
bf = fill_hex(find(slide4, bar))
if bf not in BRAND_FILL: # 칠이 6개사 브랜드 색인가
fail(f"{card}: 헤더 바 칠={bf} 이 브랜드 색이 아님"); continue
want = "2B2924" if bf in BRIGHT_FILL else "FFFFFF" # 명/암으로 글자색이 정해진다
if run_color(find(slide4, txt), 0, 0) != want:
fail(f"{card}: 사명 글자색이 칠 {bf} 위의 규칙에 어긋남")
"어느 회사인가"가 아니라 "밝은 칠 위에 흰 글자를 올리지 않았는가"를 본다. 이러면 매주 어느 3개사가 와도 안 떨어져요. 갈아 끼우기 누락(해당 주 이외의 주차 토큰 잔존)과 전각 글자 수 초과도 여기서 같이 걸러 내요. soffice가 없는 환경에서도 돌아가니까, 이미지화해서 눈으로 보는 데 의존하지 않아요.
대본에 고정한다 & 하드 상한
정말 중요한 건, 이것들을 그때그때 떠오르는 대로 하지 않는 것. 검증 절차를 2개의 프롬프트(weekly_x_competitor_prompt.md / competitor_slides_prompt.md)에 절로 적어 내려서, 매주 반드시 같은 순서로 돌게 해 뒀어요.
그리고 상한을 끊어요. Addy가 말하는 "A loop running unattended is also a loop making mistakes unattended."(아무도 안 보는 루프는, 아무도 안 보는 채로 계속 틀린다)에 대한 대책이에요.
- (A)의 수정 루프: 최대 3회
- (B)의 왕복: 최대 2회
- 넘으면 미해결 사항을 남기고 정지 (토큰 폭주 방지)
여기를 안 끊으면, 로그인이 끊겨 있거나 게시글이 안 잡히는 주에 루프가 "고쳐 보겠다"며 하염없이 토큰을 태워요. 상한은 품질을 위해서라기보다, 지갑을 지키기 위한 안전장치예요.
돌려 보고 효과를 본 점 / 주의점
| 함정 | 효과를 본 막는 법 |
|---|---|
| 검증 미루기 누적 | 생성 담당과 다른 평가 담당((A)결정론 + (B)다른 모델)을 대본에 고정 |
| 빈 데이터에서의 날조 | 데이터 충분성 게이트로 생성 전에 중지 (사후 검출보다 앞에서 멈춘다) |
| 뒤바뀜 (지난주/타사 소견의 유용) | (B)에게 원시값 대조를 명시적으로 시킨다 |
| 색·갈아 끼우기 누락 | 회사 비의존 팔레트 규칙 체크 + 주차 토큰 잔존 스캔 |
| 토큰 폭주 | 수정 루프·왕복 횟수에 하드 상한 |
화려해 보이는 건 (B)의 다른 모델 평가 담당뿐이고, 나머지는 **"원시값을 남긴다", "템플릿을 고정한다", "결정론으로 걸러 낼 수 있는 것만 걸러 낸다"**는 수수한 배선이에요. 그런데 정성 리포트에서 품질을 안정시킨 건, 이 수수한 배선과 **"자기가 자기를 채점하지 않는다"**는 한 가지였어요.
정리: 그릇은 도구가 마련한다, 잣대는 사람이 쥔다
이 설계를 한마디로 정리하면 이렇게 돼요.
수치 리포트에서는 "원시값에서 재계산해 대조"가 잣대가 된다. 정성 리포트에서는, 재계산할 수 있는 숫자가 거의 없다. 그래서 잣대는 "사실과 추측을 나누고 있는가", "못 잡은 것을 잡은 것처럼 만들지 않았는가"라는 판단 기준이 되고, 그것을 다른 모델과 대본에 적어 내리는 수밖에 없다.
도구가 마련해 주는 건 그릇(스케줄·템플릿·서브에이전트·원시값 둘 곳)까지예요. **"좋은 경쟁 주간 리포트란 무엇인가"**라는 잣대는, 결국 해 온 사람 안에만 있었어요.
관심이 생긴 분은, 손에 있는 정례 리포트를 하나 골라서 우선 "원시값 저장 → 고정 템플릿 → 결정론으로 걸러 낼 수 있는 것만 걸러 내기 → 나머지는 다른 모델에게 읽히기"로 분해해 보세요. 출력이 숫자 쪽인지 판단 쪽인지에 따라, (A)와 (B) 중 어느 쪽에 무게를 실어야 하는지가 금방 와닿을 거예요.
참고
- Addy Osmani, Loop Engineering (2026/6/7) — 개념의 첫 등장
이 글이 도움이 됐다면 추천해 주세요