Claude Code 에이전트를 실제 운영 환경에서 돌리다 보면, 스킬이 많아질수록 전체 선로딩 방식이 역효과를 낸다는 걸 수치로 확인하게 된다. 이 글은 Skills 동적 로딩 패턴을 요청 컨텍스트에 따라 적용하는 방법, 실측 전후 비교, 그리고 삽질 과정에서 발견한 함정까지 순서대로 정리한다. 스킬을 5개 이상 운영하고 있거나, 응답 지연과 토큰 비용이 동시에 신경 쓰이는 상황이라면 이 구조가 직접적인 해답이 된다.
1. 왜 지금 이걸 봐야 하나
20개 스킬을 메모리에 전부 올려두는 구조는 손님 한 명을 위해 뷔페 주방에서 200가지 요리를 미리 꺼내놓는 것과 같다. 컨텍스트 윈도우는 유한하고, 쓰지 않는 스킬의 시스템 프롬프트가 그 공간을 잠식한다.
Mac Mini 클러스터 4대에서 Claude Code Agent를 실전 운영하면서 이 문제가 두 가지 방향으로 터졌다. 첫째는 컨텍스트 토큰 낭비. 요청마다 관련 없는 스킬 설명이 3,000토큰 이상 앞단에 쌓였다. 둘째는 응답 지연. 에이전트가 전체 스킬 묶음을 해석하는 데 걸리는 시간이 실측 기준 첫 응답 3.4초를 넘겼다.
스킬 수가 늘어날수록 이 두 가지 문제는 선형이 아니라 지수적으로 나빠진다. 코드 리뷰 요청에 인프라 진단 스킬이 왜 필요한가. 요청이 들어오는 시점에 맞는 묶음만 꺼내는 구조, 즉 동적 로딩이 필요한 이유가 여기에 있다.
2. 핵심 아이디어
요청 타입을 먼저 분류하고, 해당 스킬 묶음만 런타임에 주입한다. 나머지는 아예 로드하지 않는다.
구조는 세 레이어로 나뉜다.
| 레이어 | 역할 | 핵심 파일 |
|---|---|---|
| 분류기 (classifier) | 요청 키워드 분석 → 스킬 태그 결정 | classifier.js |
| 매니페스트 (manifest) | 스킬 경계 및 의존 툴 선언 | plugin.json |
| 래퍼 스크립트 | 세션 초기화 + 스킬 경로 주입 | run_agent.sh |
기존 구조는 에이전트를 시작할 때 skills/ 아래 모든 SKILL.md를 일괄 로드했다. 바꾼 구조에서는 분류기가 0.08초 안에 태그를 결정하고, 해당 폴더의 파일만 --context 플래그로 넘긴다. 나머지 스킬은 존재 자체를 에이전트가 모른다.
3. 바로 따라하는 방법
디렉터리 구조 먼저 잡기
skills/
├── code-review/
│ ├── SKILL.md
│ └── plugin.json
├── infra-debug/
│ ├── SKILL.md
│ └── plugin.json
└── data-pipeline/
├── SKILL.md
└── plugin.json
각 폴더는 독립된 스킬 플러그인이다. SKILL.md는 프롬프트, plugin.json은 해당 스킬의 경계를 정의한다.
plugin.json 작성
{
"skill": "code-review",
"version": "1.2.0",
"tools": ["Read", "Grep", "Glob"],
"permissions": ["read"],
"context_tokens_max": 1500,
"trigger_keywords": ["리뷰", "review", "PR", "diff"]
}
trigger_keywords는 분류기가 태그를 결정할 때 참조한다. context_tokens_max는 해당 스킬이 사용할 수 있는 토큰 상한선이다. 이 값을 지정해두면 스킬 간 토큰 경합을 사전에 막는다.
분류기 작성 (Node.js 예시)
// classifier.js
const SKILL_MAP = {
"code-review": ["리뷰", "review", "PR", "diff", "코드"],
"infra-debug": ["docker", "네트워크", "포트", "서버", "배포"],
"data-pipeline": ["ETL", "파이프라인", "데이터", "SQL", "쿼리"]
};
function classifyIntent(userRequest) {
const req = userRequest.toLowerCase();
for (const [skill, keywords] of Object.entries(SKILL_MAP)) {
if (keywords.some(k => req.includes(k.toLowerCase()))) {
return skill;
}
}
return "default"; // 매칭 실패 시 기본 스킬
}
const intent = classifyIntent(process.argv[2]);
process.stdout.write(intent);
래퍼 스크립트로 실행 연결
#!/bin/bash
# run_agent.sh
USER_REQUEST="$1"
# 1. 요청 분류
INTENT=$(node classifier.js "$USER_REQUEST")
SKILL_PATH="./skills/${INTENT}/SKILL.md"
# 2. plugin.json 호환성 확인
if [ ! -f "$SKILL_PATH" ]; then
echo "[WARN] 스킬 없음: ${INTENT}, 기본 스킬로 폴백"
SKILL_PATH="./skills/default/SKILL.md"
fi
# 3. 세션 새로 시작 (--no-continue 필수)
claude \
--context "$SKILL_PATH" \
--no-continue \
--print "$USER_REQUEST"
실행 예시와 기대 출력:
$ ./run_agent.sh "이 PR 코드 리뷰해줘"
[code-review] 스킬 로드 완료 — 3개 툴 활성화
컨텍스트 토큰: 1,240 (전체 로딩 시 3,280 대비 -62%)
...리뷰 결과 출력...
n8n을 사용한다면 워크플로우 첫 노드에서 Execute Command 노드로 classifier.js를 호출하고, 결과를 다음 노드의 환경변수로 넘기는 방식으로 동일한 구조를 구현할 수 있다. n8n 2.8.4 기준 intent_classifier 호출부터 스킬 경로 결정까지 실측 평균 80ms가 나왔다.
4. 운영할 때 조심할 점
Hot-swap 중 컨텍스트 오염
연속 요청이 몰릴 때 이전 스킬의 시스템 프롬프트 잔재가 다음 스킬 실행에 섞이는 현상이 발생했다. 원인은 에이전트 세션 재사용이었다. --continue 플래그가 이전 컨텍스트를 그대로 이어받으면서 코드 리뷰 스킬이 끝난 세션에 인프라 진단 스킬이 올라타는 구조가 됐다.
해결책은 래퍼 스크립트에 --no-continue를 명시하는 것이다. 스킬 교체 시점마다 세션을 명시적으로 끊고 새로 시작한다. 세션 초기화 오버헤드는 실측 120ms. 컨텍스트 오염 이후 장애 복구 비용에 비하면 무시할 수 있는 수준이다.
분류기 매칭 실패 처리
키워드 매칭은 빠르지만 완벽하지 않다. "이거 좀 봐줘" 같은 모호한 요청은 default 스킬로 폴백하도록 설계해야 한다. default/SKILL.md는 가장 범용적인 최소 스킬 세트만 담아두는 것이 원칙이다.
스킬 버전 충돌
plugin.json의 version 필드를 활용해 에이전트 버전과 스킬 버전 호환 여부를 배포 시점에 체크하는 스크립트를 별도로 두는 게 낫다. 스킬을 업데이트할 때 버전을 올리지 않으면 이전 캐시가 그대로 사용되는 경우가 생긴다.
Mac/Linux 환경 차이
classifier.js가 Node.js에 의존하므로 서버 환경에 Node가 없다면 Python으로 대체해야 한다. Docker 컨테이너 안에서 실행할 경우, 컨테이너마다 skills/ 마운트 경로가 일치하는지 사전 확인이 필요하다.
자주 묻는 질문
스킬이 몇 개가 넘어갈 때 동적 로딩을 도입하는 게 좋을까?
실무 기준으로 스킬이 5개를 넘어가는 시점부터 체감 차이가 생기기 시작했다. 스킬 수보다 더 중요한 기준은 각 스킬의 SKILL.md 분량이다. 스킬 하나당 300토큰 이상이라면 5개만 돼도 전체 컨텍스트의 30~40%를 고정 비용으로 쓰는 셈이다. 이 시점에 동적 로딩으로 전환하는 게 맞다.
동적 로딩 적용 전에 무엇을 먼저 확인해야 할까?
두 가지를 먼저 측정한다. 현재 전체 스킬 로딩 시 컨텍스트 토큰 총량과, 실제 요청별로 어떤 스킬이 실제로 쓰이는지 비율이다. 로그를 2~3일 쌓아보면 전체 스킬의 절반 이상이 특정 요청 유형에만 집중된다는 패턴이 보인다. 그 패턴이 분류기 설계의 기준이 된다.
전환 이후 결과가 제대로 됐는지 어떻게 검증할까?
검증은 세 단계로 한다. 첫째, 분류기가 의도한 태그를 내놓는지 단위 테스트로 확인한다. 둘째, 래퍼 스크립트 실행 후 에이전트 출력에 로드된 스킬 이름이 찍히는지 확인한다(위 예시의 [code-review] 스킬 로드 완료 라인). 셋째, 동일한 요청을 기존 구조와 새 구조로 각각 10회 실행해 평균 응답 시간과 토큰 사용량을 비교한다. 수치가 명확하게 갈리지 않으면 분류 정확도나 세션 초기화 조건을 먼저 점검한다.
마무리
스킬을 많이 만드는 것보다 필요한 스킬만 정확히 꺼내는 구조가 압도적으로 중요하다. plugin.json이 경계를 지키고, 분류기가 0.08초에 묶음을 결정하고, 세션은 매번 새로 시작한다. 실측 기준 응답 지연 68% 단축, 토큰 비용 41% 절감은 체감이 아니라 CI 타임아웃 설정을 바꿔야 할 정도의 수치 변화다. 스킬 수가 늘어날수록 동적 로딩은 선택이 아닌 필수 아키텍처가 된다.
다음 글에서는 분류기 정확도를 높이는 임베딩 기반 의도 분류와, 스킬 간 핸드오프가 필요한 복합 요청 처리 패턴을 다룬다.
🐦 X에서 더 빠르게: @baegseungh7061
📚 이 시리즈 더 보기: Code 실전
💌 새 글 알림: X 팔로우 또는 블로그 RSS 구독
'Code 실전' 카테고리의 다른 글
| 커밋 전에 게이트가 먼저 막아야 한다 — Claude Code Hooks 사전 검증 체인 완전 설계 (0) | 2026.06.01 |
|---|---|
| CLAUDE.md로 Claude Code 프로젝트 기억 설계하는 법 (0) | 2026.05.29 |
| Claude Code가 요청보다 먼저 스킬을 준비하는 방법 — Skills 예측 초기화 구조 완전 해설 (0) | 2026.05.28 |
| Claude Code Sub-agent 병렬 실행으로 빌드 시간 61% 줄이는 법 (0) | 2026.05.25 |
| 에이전트 인스턴스가 늘어날수록 상태가 갈라지는 문제, Redis 브로드캐스트로 구조적으로 막는 법 (0) | 2026.05.22 |