Figma 퍼블리싱 파이프라인 03. 병렬화 시도와 토큰 폭발
스킬 시스템을 실제 업무에 투입했다. 처음엔 괜찮았는데 복잡한 페이지를 처리하다가 토큰 게이지가 폭주했다. 추적해보니 서브에이전트가 66개 동시에 돌고 있었다.
섹션을 나눠 동시에 돌리면 빨라질 줄 알았다
기본 구조는 잘 돌아갔는데 느렸다. 섹션 5개를 순차로 처리하면 5개 시간이 그대로 합산된다. 병렬로 돌리면 가장 오래 걸리는 하나만큼만 기다리면 된다.
나누는 데는 다른 이유도 있었다. Figma MCP의 get_design_context는 한 번에 돌려줄 수 있는 데이터량에 한계가 있어서, 큰 프레임을 통째로 조회하면 하위 레이어가 조용히 잘린다. 에러가 나는 게 아니라 없는 것처럼 응답이 온다.
그래서 get_metadata를 먼저 부른다. 이건 실제 스타일 값 없이 노드 트리 구조만 XML로 돌려주기 때문에 응답이 가볍다. DOM으로 치면 각 요소의 계산된 스타일까지 다 읽는 대신 태그 구조만 훑는 것에 가깝다. 이 트리를 보고 기능 단위로 영역을 나눈 뒤, 각 영역만 get_design_context로 따로 조회하면 잘림이 거의 없어진다.
실제로 받은 트리는 이런 모양이었다.
Pagination이 깊이 6단계에 있다. 통째로 조회했다면 이게 제일 먼저 사라졌을 자리다.
영역별로 조회하고 에이전트를 붙였다
각 서브에이전트는 자기 파일만 만들고, 최종 조립은 메인 세션이 순차로 한다.
| 항목 | 순차 | 병렬 |
|---|---|---|
| 디자인 조회 | 1회 통째로 조회해 잘림 발생 | 영역별 개별 조회로 누락 없음 |
| 구현 속도 | 순차 합산 시간 | 가장 느린 하나의 시간 |
| 누락 대응 | 완성 후 발견해 재작업 | 노드 트리로 사전 전수 확인 |
| 컨텍스트 소모 | 메인 세션에 모든 코드 누적 | 서브에이전트가 분산 처리 |
자리를 비운 사이 토큰 게이지가 37%까지 올라갔다
평소처럼 퍼블리싱을 맡기고 자리를 비웠다가 돌아오니 게이지가 37%였다. 페이지 하나 처리하는 동안 1분에 3%씩 오르고 있었다.
백그라운드를 보니 claude -p 프로세스가 66개 동시에 돌고 있었다.
토큰을 어디서 쓰는지부터 확인했다.
PHASE 0부터 1.5까지는 Python과 Figma API만 써서 LLM 토큰이 0이다. 소비는 전부 PHASE 2에서 난다.
섹션 7개면 에이전트 2개, 8개면 8개였다
figma_publish.py의 split_sections()를 열어보니 이랬다.
MAX_SECTION_TOKENS = 8000 # child JSON 토큰 예산
if n <= 3:
return [단일 에이전트]
elif n <= 7:
return [2개 그룹]
else:
return [n개 개별 에이전트] # ← 문제섹션이 7개일 때는 에이전트 2개인데 8개가 되는 순간 8개가 된다. 입력이 하나 늘었을 뿐인데 비용이 네 배가 되는 계단이 여기 있었다. 복잡한 페이지는 필연적으로 마지막 분기에 걸린다.
이 페이지는 MAX_SECTION_TOKENS = 8000 기준으로 나누니 66개 섹션이 나왔고, 그대로 66개 에이전트가 떴다.
에이전트마다 같은 데이터를 30K씩 들고 있었다
서브에이전트 하나가 실제로 받는 컨텍스트를 재봤다.
| 항목 | 토큰 |
|---|---|
| 시스템 프롬프트 | 약 8K |
| 스킬 프롬프트 | 약 4K |
| 빌드 프롬프트 | 약 2K |
shared.json | 약 25K |
| section 파일 | 약 36K |
└ 그중 specCache | 약 30K |
| └ 실제 노드 데이터 | 약 6K |
| 에이전트당 입력 합계 | 약 75K |
Sonnet의 컨텍스트 창은 200K인데 75K만 쓰고 125K를 비워두고 있었다. 에이전트를 더 잘게 쪼갤 이유가 없었다는 뜻이다.
더 나쁜 건 specCache였다. 섹션 파일의 83%를 차지하는 이 30K가 66개 파일에 전부 똑같이 복사돼 있었다. 실제 노드 데이터는 6K × 66 = 396K인데, 중복된 캐시가 30K × 66 = 1,980K였다. 쓸모 있는 데이터의 다섯 배를 중복으로 실어 나르고 있었다.
나누는 것과 묶는 것을 분리했다
MAX_SECTION_TOKENS를 8K에서 30K로 올리고, 나눈 뒤에 인접 섹션을 다시 묶는 merge_by_budget()을 추가했다. 나누는 단계는 트리 구조를 보존하며 분해하는 일만 하고, 묶는 단계가 토큰 예산에 맞춰 재조립한다.
66개가 4개로 줄었다.
| 그룹 | 섹션 수 | 토큰 |
|---|---|---|
| frame-1261172071 | 6개 | 49K |
| dropdown | 11개 | 59K |
| frame-1437265828 | 10개 | 55K |
| table_cpa | 7개 | 42K |
specCache는 각 섹션 파일에서 빼고 shared.json 한 곳으로 옮겼다. 토큰 목록도 전체를 넘기지 않고 이 페이지에서 실제 참조하는 것만 걸러 넘기게 했다.
PHASE 2 서브에이전트 모델은 Haiku로 내렸다. 이 단계는 이미 확정된 스펙대로 컴포넌트를 찍어내는 일이라 추론이 크게 필요하지 않다. 자동으로 돌던 QA 파이프라인은 필요할 때만 고르게 바꿨고, chakra와 tailwind로 갈라져 있던 스킬 4개는 프레임워크를 자동 감지하는 2개로 합쳤다.
| 항목 | 이전 | 이후 | 절감 |
|---|---|---|---|
| PHASE 2 입력 | 4,950K | 484K | -90% |
| PHASE 2 출력 | 528K | 170K | -68% |
| QA 파이프라인 | 230K | 75K | -67% |
| 총계 | 5,748K | 769K | -87% |
같은 페이지를 다시 돌렸을 때 게이지는 37%가 아니라 5%에서 멈췄다.
막힌 지점과 원인
| 문제 | 원인 | 해결 |
|---|---|---|
| 에이전트 66개 동시 실행 | 섹션 8개를 넘기면 섹션 수만큼 에이전트를 만드는 계단식 분기 | 토큰 예산 상향 + merge_by_budget()으로 재조립 |
| 에이전트당 입력의 40%가 중복 데이터 | specCache가 모든 섹션 파일에 복사됨 | shared.json 한 곳으로 이동해 참조 |
| 200K 창에 75K만 사용 | 분할 단위가 실제 처리 용량보다 훨씬 작음 | 그룹당 40~60K로 묶어 창 활용률 상향 |
| 큰 프레임 조회 시 하위 레이어 누락 | get_design_context 응답이 조용히 잘림 | get_metadata로 트리를 먼저 받고 영역별 개별 조회 |
API 응답이 잘리는 문제와 LLM 컨텍스트를 어떻게 쓰느냐는 별개인데, 처음엔 둘을 같은 손잡이로 풀려고 했다. 잘림을 막으려고 잘게 나눴더니 그 잘게 나눈 단위가 그대로 에이전트 수가 됐다. 조회 단위와 실행 단위를 분리하고 나서야 둘 다 잡혔다.