월 41% AI 비용 아낀 비결? 200줄 TypeScript 라우터로 똑똑하게 모델 선택하기
2026. 5. 8.
월 41% AI 비용 아낀 비결? 200줄 TypeScript 라우터로 똑똑하게 모델 선택하기
최근 AI API 사용량이 급격히 늘면서, 개발 비용의 상당 부분을 차지하는 AI 요금에 대한 고민이 깊어지고 있습니다. 특히 여러 프로젝트에서 AI 서비스를 활용하다 보면, 저처럼 월별 청구서가 예상치 못하게 불어나는 경험을 하실 텐데요. 제 경우 지난 3월, 가장 빠르게 증가한 항목은 Claude나 GPT 직접 호출이 아니었습니다. Cursor 유료 구독료, Copilot 유료 구독료, 그리고 개인 CLI에서 사용하던 Anthropic API 요금이 합쳐진 금액이었죠.
세 개의 구독 서비스, 각각 다른 측정 방식, 심지어 Cursor는 제가 Anthropic에 직접 전달하던 컨텍스트를 자체 백엔드로 다시 보내면서 동일한 Opus 토큰에 대해 두 번 요금이 청구되는 어처구니없는 상황도 발생했습니다.
문제는 이런 AI 래퍼(Wrapper) 서비스들이 이 사실을 사용자에게 명확히 알려주지 않는다는 겁니다. 라우터 코드가 이들의 주력 상품이 아니기 때문이죠. 이들의 진짜 상품은 "어떤 모델이 어떤 프롬프트를 처리할지 고민할 필요 없는 편리함"입니다. 그리고 그 편리함의 대가로 우리는 서비스 구독료에 녹아있는 '오케스트레이션 세금(orchestration tax)'을 지불하고 있는 셈입니다.
저도 그런 세금을 계속 내는 것이 너무 아까워졌습니다. 그래서 직접 라우터를 만들기로 결심했고, 200줄짜리 TypeScript 코드로 구현해냈습니다. 놀랍게도 4월 청구서는 3월과 비슷한 작업량에도 불구하고 41%나 줄어들었습니다.
TL;DR: AI 모델 선택, 이렇게 해보세요
| 모델 | 입력 $1M 토큰 당 | 출력 $1M 토큰 당 | 최적 용도 |
|---|---|---|---|
| Haiku 4.5 | 0.80 | 4.00 | 단순 조회, 분류, 오타 수정 |
| Sonnet 4.6 | 3.00 | 15.00 | 일반적인 코딩, 리팩터링, 코드 리뷰 |
| Opus 4.7 | 5.00 | 25.00 | 다단계 계획 수립, 아키텍처 설계 |
| GPT-5 mini | 0.50 | 2.00 | 저렴한 분류, 임베딩 전처리 |
제가 41%를 절감할 수 있었던 비결은 딱 한 가지였습니다. Sonnet이 처리하던 작업 중 Haiku가 10분의 1 가격으로 처리할 수 있는 것들은 모두 Haiku에게 넘긴 것입니다. 대부분의 코딩 관련 질의는 질문으로 위장한 단순 조회에 불과해요. 중요한 건 습관이 아닌 의도(intent)에 따라 라우팅하는 것입니다.
1. 오케스트레이션 세금은 실재한다
모든 AI 래퍼 서비스는 본질적으로 동일한 교환을 제안합니다. 특정 모델을 알아서 선택해주고, 수정할 수 없는 시스템 프롬프트를 붙여주며, 들여다볼 수 없는 컨텍스트 창을 유지하죠. 그 대가로 우리는 아무것도 고민할 필요가 없습니다.
이 '고민하지 않는 비용'은 두 가지 방식으로 우리 지갑을 털어갑니다.
- 가장 비싼 모델 호출: 래퍼는 데모에서 좋은 성능을 보여주기 위해, 그리고 서비스 수준 협약(SLA)을 만족시키기 위해 가장 비싼 모델을 호출하는 경향이 있습니다.
- 불필요한 컨텍스트 비용 청구: 래퍼는 자체 시스템 프롬프트나 도구 정의 등, 우리가 요청한 것 외의 추가 컨텍스트까지 모두 포함하여 우리에게 비용을 청구합니다.
제가 30일 동안 Cursor 사용량을 Anthropic 대시보드와 비교해 본 적이 있습니다. Cursor는 채팅 턴당 평균 8,400개의 입력 토큰을 보냈지만, 동일한 채팅에 대한 저의 직접 API 호출은 평균 1,900개에 불과했어요. 6,500 토큰이라는 엄청난 차이는 Cursor의 자체 프레임워크, 인덱싱 컨텍스트, 그리고 에이전트 스캐폴딩 등에서 발생한 것이었습니다. 물론 유용할 수 있지만, 결코 공짜는 아니죠.
제가 실무에서 이 부분을 테스트해 봤을 때, 특히 프롬프트 엔지니어링에 공을 들여 컨텍스트를 최적화해도, 래퍼 서비스는 그 노력을 무색하게 할 만큼 많은 불필요한 데이터를 붙여 보낸다는 걸 알게 됐습니다. 결국 직접 라우터를 만들면, 우리가 무엇을 보낼지 직접 선택할 수 있습니다. 이것이 이 게임의 핵심입니다.
2. 200줄 라우터 코드
여기 라우터 코드 전체를 공개합니다. 프로젝트에 이 파일을 넣고 API 키만 설정하면, 여러분이 직접 제어하는 규칙에 따라 각 요청에 맞는 모델을 자동으로 선택해줍니다.
// router.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
type Intent = "trivial" | "code" | "plan" | "embed";
interface RouteRule {
match: (prompt: string) => boolean;
intent: Intent;
}
interface ModelConfig {
provider: "anthropic" | "openai";
model: string;
maxTokens: number;
}
const ROUTES: Record<Intent, ModelConfig> = {
trivial: { provider: "anthropic", model: "claude-haiku-4-5-20251001", maxTokens: 1024 },
code: { provider: "anthropic", model: "claude-sonnet-4-6", maxTokens: 4096 },
plan: { provider: "anthropic", model: "claude-opus-4-7", maxTokens: 8192 },
embed: { provider: "openai", model: "gpt-5-mini", maxTokens: 512 },
};
const RULES: RouteRule[] = [
{ intent: "trivial", match: (p) => p.length < 200 && /\?$/.test(p.trim()) },
{ intent: "trivial", match: (p) => /^(what is|define|fix typo|rename)/i.test(p) },
{ intent: "plan", match: (p) => /(refactor|design|architect|migrate|plan)/i.test(p) },
{ intent: "code", match: (p) => /(```|function |class |const |let )/i.test(p) },
{ intent: "embed", match: (p) => p.startsWith("CLASSIFY:") },
];
function pickIntent(prompt: string): Intent {
for (const rule of RULES) {
if (rule.match(prompt)) return rule.intent;
}
return "code";
}
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export interface RouteResult {
text: string;
model: string;
inputTokens: number;
outputTokens: number;
costUsd: number;
}
const PRICING: Record<string, { in: number; out: number }> = {
"claude-haiku-4-5-20251001": { in: 0.8, out: 4 },
"claude-sonnet-4-6": { in: 3, out: 15 },
"claude-opus-4-7": { in: 5, out: 25 },
"gpt-5-mini": { in: 0.5, out: 2 },
};
function priceCall(model: string, inTok: number, outTok: number): number {
const p = PRICING[model];
if (!p) return 0;
return (inTok * p.in + outTok * p.out) / 1_000_000;
}
export async function route(prompt: string): Promise<RouteResult> {
const intent = pickIntent(prompt);
const cfg = ROUTES[intent];
if (cfg.provider === "anthropic") {
const r = await anthropic.messages.create({
model: cfg.model,
max_tokens: cfg.maxTokens,
messages: [{ role: "user", content: prompt }],
});
const text = r.content
.filter((b) => b.type === "text")
.map((b) => (b as { text: string }).text)
.join("");
return {
text,
model: cfg.model,
inputTokens: r.usage.input_tokens,
outputTokens: r.usage.output_tokens,
costUsd: priceCall(cfg.model, r.usage.input_tokens, r.usage.output_tokens),
};
}
const r = await openai.chat.completions.create({
model: cfg.model,
max_tokens: cfg.maxTokens,
messages: [{ role: "user", content: prompt }],
});
const usage = r.usage ?? { prompt_tokens: 0, completion_tokens: 0 };
return {
text: r.choices[0]?.message?.content ?? "",
model: cfg.model,
inputTokens: usage.prompt_tokens,
outputTokens: usage.completion_tokens,
costUsd: priceCall(cfg.model, usage.prompt_tokens, usage.completion_tokens),
};
}
이게 다입니다. 두 개의 제공자(provider), 네 가지 의도(intent), 다섯 가지 규칙, 그리고 비용 계산기가 포함되어 있습니다. 사용법은 아래와 같습니다.
import { route } from "./router";
const out = await route("rename this function from getUser to fetchUser");
console.log(out.model, out.costUsd.toFixed(5));
// claude-haiku-4-5-20251001 0.00012
여기서 제시된 규칙들은 의도적으로 단순하게 만들었습니다. 길이와 정규식을 조합하는 것만으로도 라우팅 결정의 약 70%는 정확하게 처리할 수 있죠. 나머지 30%에 대해서는 아래처럼 접두사(prefix)를 붙여 오버라이드할 수 있습니다.
await route("[force:opus] design a permissions model for ...");
pickIntent 함수에 이 접두사를 읽는 한 줄짜리 로직을 추가하면 됩니다. 예시를 간결하게 유지하기 위해 제가 포함하지 않았습니다.
3. 실제로 효과 있는 라우팅 규칙들
어떤 사람들은 '값싼 모델로 작은 분류 호출을 보내 라우팅을 결정하게 하자'는 순진한 접근 방식을 생각하기도 합니다. 하지만 이는 똑똑해 보일 뿐, 매 요청마다 두 번의 API 호출이 발생하여 오히려 절약하는 것보다 더 많은 비용이 듭니다. pickIntent의 비용은 사실상 0이 되어야 합니다.
제가 사용하는 대부분의 워크로드에는 다음 다섯 가지 정규식 규칙으로 충분했습니다.
- 짧고 물음표로 끝나는 경우:
trivial(사소한) - "what is", "define", "fix typo", "rename" 등으로 시작하는 경우:
trivial - "refactor", "design", "architect", "migrate", "plan"을 포함하는 경우:
plan(계획) - 코드 블록(
) 또는 함수 키워드를 포함하는 경우:code(코드) - "CLASSIFY:" 접두사로 시작하는 경우:
embed(임베딩/저렴한 분류기)
기본값은 code로 설정합니다. trivial이 code로 잘못 라우팅되는 경우는 해당 요청의 비용이 4배 정도 비싸질 수 있습니다. code가 opus로 잘못 라우팅되면 1.6배 정도 비용이 더 들겠죠. 어느 쪽이든 치명적인 재앙은 아닙니다. 정말 피해야 할 버그는 Haiku에게 컨텍스트를 유지할 수 없는 다단계 계획 요청을 보내는 것입니다. 따라서 기본값은 보수적으로 설정하는 것이 중요합니다.
저는 또한 모든 잘못된 라우팅을 기록했습니다. 2주 후에는 "이 프롬프트는 X로 라우팅되었지만 Y가 되어야 했다"는 작은 CSV 파일이 생겼죠. 여기에 정규식 규칙 두 개를 추가했더니, 오인식률이 8%에서 2% 미만으로 떨어졌습니다. 제가 초기에 라우터를 만들 때 가장 많이 고민했던 지점이기도 합니다. 처음엔 멋지게 AI로 AI를 라우팅하려 했지만, 결국 단순한 정규식이 가장 빠르고 저렴하며 효과적이라는 걸 깨달았죠.
4. 41% 절감, 자세히 들여다보기
라우터를 사용하지 않았던 3월 청구서:
| 출처 | 호출 수 | 비용 |
|---|---|---|
| Cursor 구독료 | N/A | $20 |
| Copilot 구독료 | N/A | $10 |
| Anthropic 직접 | 4,200 | $87 |
| OpenAI 직접 | 800 | $14 |
| 총계 | $131 |
라우터를 도입한 4월 청구서 (Cursor 해지, Copilot은 IDE 인라인 기능용으로 유지):
| 출처 | 호출 수 | 비용 |
|---|---|---|
| Cursor 구독료 | N/A | $0 |
| Copilot 구독료 | N/A | $10 |
| Anthropic (라우터 경유) | 5,100 | $54 |
| OpenAI (라우터 경유) | 1,400 | $13 |
| 총계 | $77 |
총 호출량은 30% 증가했지만, 비용은 41% 감소했습니다. 라우터 덕분에 전체 호출의 62%가 Haiku로 이동했으며, Sonnet이 처리하던 작업들을 Haiku가 대신 맡게 되었습니다. 호출당 평균 비용은 $0.024에서 $0.013으로 크게 떨어졌죠.
Cursor 구독 해지가 가장 큰 절감 효과를 가져왔지만, 라우터는 그보다 작지만 반복적이고 점진적인 절감 효과를 만들어냈습니다. 둘 모두 동일한 아이디어에서 출발했습니다: AI 래퍼 서비스는 우리가 직접 더 잘할 수 있는 결정들을 숨기고 있다는 것.
5. 이 라우터가 해결하지 않는 것들
이 라우터는 에이전트 프레임워크가 아닙니다. 스트리밍을 지원하지 않고, 재시도를 처리하지 않으며, 캐싱 기능도 없습니다. API 호출량 제한(rate limit)을 관리하지 않고, 도구 사용(tool use) 기능도 없습니다. 여러분의 코드베이스에 대해서도 알지 못합니다.
이러한 기능들을 추가하려면 물론 작업이 필요합니다. 스트리밍은 두 군데만 수정하면 되고, Anthropic 프롬프트 캐시를 이용한 캐싱은 호출 시 헤더 하나만 추가하면 됩니다. 지수 백오프(exponential backoff)를 이용한 재시도는 20줄이면 충분하죠. 도구 사용은 어차피 직접 작성해야 하는 스키마 처리가 필요하고요.
만약 이 모든 기능이 필요하다면, 실제 AI 프레임워크를 사용하는 것이 맞습니다. 하지만 여러분이 AI 호출의 80%에 대해 '오케스트레이션 세금'을 내는 것을 멈추고 싶다면, 위에 제시된 200줄 코드만으로 충분합니다. 나머지 필요한 기능들은 실제 문제가 발생했을 때 추가해도 늦지 않습니다.
결론
AI API 호출을 라우팅하는 것이 번거롭기 때문에 AI 래퍼 서비스들이 존재합니다. 하지만 동시에 이는 여러분의 코드에서 가장 큰 레버리지를 가질 수 있는 부분이기도 합니다. 위에 제시된 200줄 코드는 특별한 기술 장벽(moat)이 아닙니다. 그냥 화요일 오후에 잠깐 시간을 내면 만들 수 있는 정도죠. 이걸 직접 작성해야 하는 이유는, 보이지 않는 비용은 절감할 수 없기 때문입니다.
여러분은 현재 저렴한 모델과 비싼 모델의 호출 비율이 어떻게 되나요? 만약 모른다면, 그것부터 해결해야 할 첫 번째 문제입니다. 라우터를 연결하기 전에 비용 로깅부터 설정해보세요. 아마 그 수치에 깜짝 놀라게 될 겁니다.
GDS K S · thegdsks.com · Glincker 개발 중 · X에서 팔로우하기 @thegdsks
오케스트레이션 세금은 AI 청구서에 나타나지 않지만, 분명히 지불하고 있는 AI 비용의 일부입니다.
원문: https://dev.to/thegdsks/i-built-a-200-line-ai-router-in-typescript-my-monthly-bill-dropped-41-23ok 수집일: 2026-05-08 01:48:32