하룻밤 사이에 25개의 Claude Code 서브에이전트를 생성했습니다. 제가 배운 점은 다음과 같습니다.
요약
작성자는 24시간 동안 1,000달러의 예산으로 25개의 Claude Code 서브에이전트를 활용해 37개의 Apify Actor를 구축하는 실험을 진행했습니다. 이 과정에서 단순한 생산성 향상을 넘어, 인간이 루프 내에서 품질을 유지하며 다수의 백그라운드 프로세스를 제어하는 방법과 제약 조건이 명확한 프롬프트의 중요성을 분석했습니다.
핵심 포인트
- 모호함을 배제하고 명시적이며 협상 불가능한 제약 조건을 포함한 프롬프트가 필수적임
- 다수의 서브에이전트가 생성하는 결과물이 저품질로 흐르지 않도록 하는 'Human in the loop' 전략이 핵심임
- Claude Code를 활용해 단시간 내에 대량의 Apify Actor(코드, 스키마, README 포함)를 병렬로 생성 가능함
- 실제 구현 시 ESM 사용 및 특정 패턴(apify-client .init())과 같은 기술적 제약 명시가 유효함
저는 무언가를 라이브로 출시하기 위해 스스로에게 1,000달러와 24시간을 부여했습니다. 8시간째에 저는 약 25개의 Claude Code 서브에이전트 (subagents)를 병렬로 생성했고, 37개의 Apify Actor를 구축했으며, 그 모두를 Apify의 게시 파이프라인 (publish pipeline)에 밀어 넣었습니다. 오늘 아침 기준으로 5개는 Apify Store에 라이브 (LIVE) 상태로 올라와 있으며, 나머지 31개는 Apify의 '하루 5개 Actor 게시' 할당량에 따라 다음 한 주 동안 순차적으로 배포되기를 기다리며 대기열에 머물러 있습니다. 이것은 사후 분석 (postmortem)입니다. 실제 수치, 실제 프롬프트 (prompts), 실제 실패 사례를 다룹니다. "10배 생산성" 같은 프레이밍 없이, 무엇이 작동했고 무엇이 작동하지 않았는지만 다룹니다.
무엇이 구축되었나
총 37개의 Apify Actor가 구축되었습니다. 각 Actor는 하나의 src/main.js, 하나의 .actor/input_schema.json, 하나의 .actor/dataset_schema.json, 하나의 actor.json, 하나의 README, 그리고 플랫폼으로의 apify push로 구성됩니다. 이 글을 쓰는 시점에 5개가 라이브 (LIVE) 상태입니다 ( apify.com/ianymu ): llms-txt-converter, claudemd-security-auditor, gh-issue-to-claude-prompts, mcp-server-catalog, claudemd-generator. 31개는 구축되었으나 아직 공개되지 않았습니다 — published-state가 private로 설정되어 데몬 (daemon) 대기열에서 할당량 창이 열리기를 기다리고 있습니다. 약 8시간 동안 약 25개의 서브에이전트 (subagent) 프로세스가 생성되었으며, 대부분 백그라운드에서 한 번에 4개씩 실행되었습니다.
Actor 자체는 흥미로운 부분이 아닙니다. 흥미로운 부분은 루프 안의 단 한 명의 인간 (human in the loop)이 어떻게 25개의 백그라운드 프로세스가 쓰레기 같은 결과물로 흘러가지 않도록 유지할 수 있는가 하는 점입니다.
작동했던 네 가지
- 모호한 것이 아닌 제약 조건이 있는 프롬프트 (Constrained prompts)
모든 서브에이전트 (subagent)는 명시적이고 협상 불가능한 제약 조건을 포함한 약 200~300단어 분량의 프롬프트를 받았습니다. 제가 실제로 보냈던 내용의 편집된 골격은 다음과 같습니다 (이것은mcp-server-catalogActor를 위한 것이었습니다):
당신은 하나의 Apify Actor를 구축하고 있습니다 : mcp-server-catalog .
제약 조건 :
-
src/main.js는apify-clientActor의.init()패턴을 사용해야 합니다. -
ESM (ECMAScript Modules)을 사용해야 합니다.
-
Input schema (입력 스키마): { maxServers : integer 1 - 100 , default 20 ; keywordFilter : string optional } - Output to default dataset (기본 데이터셋으로 출력), 한 서버당 하나의 행(row)을 포함하며, 반드시 다음 형식을 따를 것: { fullName : string , stars : int , qualityScore : int ( 0 - 100 ), license : string | null , language : string | null , description : string , scoreBreakdown : { stars : int , recency : int , license : int , description : int , docs : int , activity : int } } - Sources to merge (병합할 소스): punkpeye / awesome - mcp - servers , modelcontextprotocol / servers , wong2 / awesome - mcp - servers . fullName 기준으로 중복 제거 (Dedupe). - README.md에 반드시 포함할 내용: 1줄 목적, 입력 예시, 실제 데이터 한 행을 포함한 출력 예시, "Try it" 링크 플레이스홀더. - 마지막에
apify push를 실행할 것.apify call은 실행하지 말 것 (비용 발생). - 테스트, CI, TypeScript, eslint 또는 요청하지 않은 추가 파일을 절대 추가하지 말 것. - 완료 조건 = Actor 페이지 URL과 샘플 데이터셋 행 하나를 보여줄 것. 이전 서브에이전트들이 제약 사항을 어겼을 때, 제가 하나씩 글로 정리하며 배운 제약 사항들입니다: "TypeScript를 추가하지 말 것" — 한 에이전트가tsconfig.json과 절반만 변환된.ts파일을 만들어냈습니다. 이를 정리하는 데 20분이 소요되었습니다. "apify call을 실행하지 말 것" — 한 에이전트가 "작동 여부를 확인하기 위해" 자신의 Actor를 실행하여 플랫폼 크레딧 약 $0.30를 기분 좋게 태워버렸습니다. 작동은 잘 되었습니다. 하지만 그게 핵심이 아니었습니다. "정확한 데이터셋 형태" — 세 개의 Actor가 자신만의 키(name vs fullName, score vs qualityScore)를 만들어냈습니다. 리팩토링하기 전까지 다운스트림(downstream) 비교 스프레드시트를 무용지물로 만들었습니다. 모호한 프롬프트는 매번 모호한 결과물을 만들어냅니다. 해석의 자유가 주어진 서브에이전트는 자신이 더 빨리 작업을 끝낼 수 있는 방향으로 해석할 것입니다. 2.run_in_background: true가 해답이었습니다. Agent 도구의 기본값은 포그라운드(foreground) 방식입니다. 즉, 다음 도구 호출이 반환되기 전까지 서브에이전트가 끝날 때까지 기다려야 합니다.run_in_background: true를 사용하면, 서브에이전트를 생성(spawn)하고 프로세스 핸들을 반환받은 즉시 다음 서브에이전트를 생성할 수 있습니다. 네 개의 Actor를 병렬로 구축하는 것은 하나씩 구축할 때보다 처리량(throughput)이 대략 4배 높았습니다.
8개를 병렬로 실행하는 것은 8배의 성능을 내지 못했습니다. 모델이 돌아오는 출력물을 검토하는 데 사용할 수 있는 주의력(attention)이 한정되어 있어서, 제가 읽을 수 있는 속도보다 출력물이 더 빠르게 도착하기 시작했기 때문이라고 생각합니다. 이번 실행에서 가장 적절한 지점(sweet spot)은 4개의 병렬 서브에이전트(subagents)였습니다. 그 이상으로 넘어가자 드리프트 신호(drift signals)를 놓치기 시작했습니다. 3. 부모가 검토할 때의 자기 수정(Self-correction): 몇몇 서브에이전트들이 명세(spec)와 일치하지 않는 결과물을 반환했습니다. 잘못된 데이터셋 형태(dataset shape), 생성하지 말라고 지시했던 추가적인 tests/ 디렉토리, 제가 나열하지 않은 의존성(dependencies)이 포함된 package.json 등이 있었습니다. 모든 경우에 대해, 한 줄의 부연 설명(
제가 나열하지 않은 의존성(dependencies)이 포함된 package.json 등이 있었습니다. 모든 경우에 대해, 한 줄의 부연 설명(
get(" isPublic " ) is True : return " already_public " body = { " isPublic " : True , " categories " : [ " AI " , " DEVELOPER_TOOLS " ], ** derive_seo ( actor ), } status , payload = put_actor ( actor_id , body , token ) if 200 <= status < 300 : return " published " blob = json.dumps(payload).lower() if any(m in blob for m in QUOTA_MARKERS): return " quota " return " error " while True: queue = read_queue() keep = [] for actor_id in queue: result = try_publish(actor_id, token) if result not in ( " published " , " already_public " ): keep.append(actor_id) write_queue(keep) time.sleep(600)
이것이 전체 패턴입니다. 속도 제한이 걸린 API + 재시도 루프 + 영구 큐 파일. 특별할 것 없습니다. 하지만 덕분에 저는 새벽 3시에 노트북을 닫고 아침에 일어나 다음 5개의 액터가 라이브된 것을 발견할 수 있었습니다.
일반적인 교훈은 이렇습니다: 속도 제한이 걸린 의존성(dependency)은 전체 설계를 바꿉니다. 빌드 파이프라인과 게시(publish) 파이프라인은 분리되어야 합니다. 프로세스를 공유할 수 없는데, 왜냐하면 하나는 사람이 타이핑하는 속도로 작동하고 다른 하나는 플랫폼 할당량(platform-quota) 속도로 작동하기 때문입니다.
작동하지 않았던 두 가지: 스브에이전트가 명세(spec)에서 벗어남
약 25개의 스브에이전트 중 3개가 제가 출력물을 검토할 때까지는 알아차리지 못한 방식으로 스크립트를 이탈했습니다. 가장 비용이 많이 드는 하나는 존재하지 않던 Docker 설정을 포함하는 "로컬에서 시도해 보기(Try it locally)" 섹션을 추가하기로 결정했습니다. 그럴듯해 보였습니다. 제가 우연히 해당 README 파일을 열어보지 않았다면 실제로 배포되었을 것입니다.
그 후 저는 단계를 추가했습니다: 모든 스브에이전트의 README에서 apify push 전에 가짜 명령어, 가짜 URL, Docker 참조를 grep(검색)하도록 했습니다. 이 방법으로 2개가 더 포착되었습니다.
패턴은 이렇습니다: 명세에 공백이 있으면 스브에이전트는 꾸며냅니다. 프롬프트의 모든 공백은 그럴듯해 보이는 것을 환각(hallucinate)할 기회입니다.
TODO.md 파일이 제 기억력을 능가했습니다. 저는 액터-팩토리 디렉토리에 TODO.md 파일을 두고 상태 변경 후마다 업데이트했습니다.
8시간 동안 여러 차례, Human-in-the-loop (Discord 통화 중인 친구)가 "ai-tool-stack-detector를 위한 README를 잊어버렸어"와 같은 말을 했습니다. 파일을 확인해 보니 실제로 제가 잊어버린 것이 맞았습니다. 파일이 옳았습니다. 25개의 서브에이전트(subagents)를 관리하는 저의 작업 기억력(working memory)이 부족했던 것입니다. 만약 제가 이 작업을 다시 한다면, TODO는 제가 수동으로 업데이트하는 마크다운(markdown) 파일이 아니라, 각 서브에이전트가 완료 시점에 자동으로 작성하는 구조화된 JSON 상태(state) 파일이 될 것입니다. 하지만 수동으로 업데이트하는 마크다운조차 기억하려고 애쓰는 것보다는 나았습니다.
제 평판을 구해준 거짓 양성(false positive). 제가 구축한 액터(actor) 중 하나는 claudemd-security-auditor입니다. 이 액터는 GitHub 저장소(repos)를 스캔하여 CLAUDE.md 파일과 .claude/hooks/* 스크립트 내의 위험한 패턴을 찾아냅니다. 저는 이를 직접 사용해 보기 위해(dogfood) 세 개의 저장소를 대상으로 실행했습니다. 그 결과 disler/claude-code-hooks-mastery에서 하나의 HIGH-severity(높음 수준의 심각도) 탐지 결과가 나왔습니다. user_prompt_submit.py의 128번 라인에 있는 rm -rf / 패턴이었습니다. 저의 첫 본능은 해당 저장소에 GitHub 이슈(issue)를 제기하는 것이었습니다. 그랬다면 매우 당혹스러운 상황이 되었을 것입니다. 대신 저는 서브에이전트에게 다음과 같이 말했습니다: "이슈를 제기하기 전에 이 탐지 결과를 수동으로 검증해줘."
서브에이전트는 파일을 열고, 10줄의 문맥(context)을 읽은 뒤 다음과 같이 보고했습니다: blocked_patterns = [ # 차단하려는 패턴을 추가하세요 # 예시: ('rm -rf /', '위험한 명령어가 감지되었습니다'), ]. rm -rf /는 Python 주석(comment) 안에 있었습니다. 그것은 실제 명령어가 아니라, 무엇을 차단해야 하는지를 보여주는 예시였습니다. 제가 파괴적인 명령어가 있다고 공개적으로 비난하려 했던 저장소는 사실 바로 그 패턴을 방어하고 있는 훌륭한 저장소 중 하나였습니다. (그들의 형제 파일인 pre_tool_use.py는 rm -rf를 종료 코드(exit code) 2와 함께 적극적으로 차단합니다.)
정규 표현식(regex)은 주석, blocked_patterns = [...] 내부의 문자열 리터럴(string literals), 또는 마크다운 펜스(markdown fences)를 인식하지 못했습니다. 그래서 저는 다음 버전의 액터에서 휴리스틱(heuristic)을 강화했습니다: 앞쪽 공백을 제거하고, #, /, //, -- 접두사를 확인하며, 주변 식별자 이름(blocked_patterns, BLOCKLIST, denylist)을 살펴보고, 매칭 결과가 명확하게 방어적인 문맥일 경우 등급을 낮추거나 건너뛰도록 했습니다. 교훈은 이것입니다: 확신은 값싸다.
95%의 확신으로 "높은 심각도(HIGH severity)의 결과"를 반환하는 모델은 일정 비율로 틀릴 것이며, 그 비율은 당신이 취하는 조치가 되돌릴 수 없는 것(공개 이슈 제기, 이메일 발송, 파일 삭제 등)일 때 매우 중요합니다. 검증(verify) 단계를 구축하세요. 그리고 구체적으로 만드세요. 저는 이 이야기의 더 긴 버전을 두 번째 dev.to 포스트로 작성했습니다 — 링크는 마지막에 있습니다.
복리로 쌓이는 작은 디테일들
AI 내레이터 스티커보다 수동으로 큐레이션한 스티커가 더 낫습니다. 공장은 공개 라이브 스트림(livestream) URL을 기반으로 운영되었습니다. 각 이벤트에 sticker_keys가 없으면 피드는 텍스트의 벽처럼 보였습니다. 페이로드(payload)당 3개의 명시적인 키(jsdelivr을 통한 Microsoft Fluent 3D 이모지)를 사용했더니, 스캔하는 데 5초가 걸리던 것이 1초 만에 가능해졌습니다: data = json . dumps ({ " sticker_keys " : [ " package " , " globe-network " , " sparkles " ], " actor_id " : actor_id , " actor_name " : actor_name , })
이벤트 로거(event logger)에는 3부 구성의 --detail 필드를 두었습니다: 무엇이 일어났는가, 그것이 왜 중요한가, 다음 단계는 무엇인가. 이 구조가 없으면 이벤트는 로그 파일처럼 읽히지만, 이 구조가 있으면 PM(프로젝트 매니저)의 업데이트처럼 읽힙니다.
외부 연락 전 교차 감사(Cross-audit)를 수행하세요. 저는 두 개의 리스트에서 거의 동일한 사람에게 두 번 이메일을 보낼 뻔했습니다. 이제 모든 배치 전송(batch send)은 이전에 보낸 sent*.csv 파일을 읽고, 지난 7일 이내에 나타난 주소는 거부합니다. 어리석을 정도로 단순하지만, 어리석은 피해를 방지합니다.
Apify 무료 티어에서는 2GB 메모리가 4GB보다 낫습니다. 기본값은 4GB입니다. 4GB를 사용하는 5개의 액터(actor)를 실행하면 20GB를 요청하게 되는데, 무료 티어의 한도는 8GB입니다. 해결 방법: 모든 실행 URL에 ?memory=2048을 추가하세요. 제가 구축한 모든 액터는 2GB에서도 잘 작동합니다.
Anthropic의 서브에이전트(subagent) 설계에서 공로를 돌리고 싶은 점은, 실제로는 알려진 것보다 더 많은 일을 수행한다는 것입니다. 구체적으로는 다음과 같습니다: 에이전트 도구(Agent tool)의 설명과 subagent_type의 분리는, 서브에이전트가 좁은 작업에 컨텍스트(context)를 소모하는 동안 부모 에이전트가 일관성을 유지할 수 있게 해줍니다. run_in_background: true 플래그는 파이프라인(pipeline)과 시퀀스(sequence)의 차이를 만듭니다. 서브에이전트가 기본적으로 부모의 컨텍스트를 공유하지 않는다는 사실은 제가 더 나은 프롬프트(prompt)를 작성하도록 강제했습니다. 만약 모든 것을 상속받았다면, 프롬프트는 더 게을러졌을 것이고 결과물은 더 나빴을 것입니다.
이것은 "AI가 모든 것을 해낸" 밤이 아니었습니다. 그것은 "AI가 타이핑을 하고, 인간이 프레임(framing)을 잡은" 밤이었습니다. 집중적인 검토(review)와 프롬프트 강화(prompt-tightening)에 들인 8시간은 필수적이었습니다. 25개의 서브에이전트(subagents)는 결과물의 양을 가능하게 했습니다. 어느 한 쪽도 다른 쪽을 대체할 수 없습니다.
현재 라이브 상태 및 아티팩트(artifacts) 위치
Apify Store에 공개된 현재 5개의 액터(Actors): apify.com/ianymu . 나머지 31개는 할당량(quota)이 허용하는 대로 하루에 5개씩 순차적으로 공개될 예정입니다. 실제 데이터셋 ID와 결과가 포함된 실제 실행 사례인 케이스 스터디(case studies)는 리포지토리(repo) 내 hook-pack-launch/outreach/actor-case-studies.md에 문서화되어 있으며, 공개된 액터 페이지에서 재현 가능합니다.
더 읽어보기
이 내용이 유용했다면, 이 비공식 시리즈의 이전 포스트 두 개를 확인해 보세요:
- 완료에 대해 Claude Code가 거짓말하는 것을 방지하는 방법 — 50줄짜리 bash 훅(hook) — 테스트가 통과하지 않았음에도 "모든 테스트 통과 ✅"라고 잡아내는 verify-before-stop 훅.
- 보안 스캐너를 만들었습니다. 첫 번째 발견 사항이 틀렸습니다. 제가 무엇을 변경했는지 여기 있습니다. — 위에서 언급한 rm -rf 오탐(false-positive) 이야기의 긴 버전.
verify-before-stop 리포지토리 (첫 번째 포스트의 종료 훅): https://github.com/ianymu/claude-verify-before-stop .
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기