본문 바로가기
프로젝트/AIKit

AIKit #1 - 왜 AI API 통합 라이브러리를 만들었나

by 루까(Luka) 2026. 2. 5.
반응형

AIKit — 왜 만들었나

문제 인식

2026년 현재 AI API를 쓰는 프로젝트를 여럿 운영하고 있다. 퍼즐 게임의 힌트 생성에 OpenAI, 블로그 초안 작성에 Claude, 간단한 분류 작업에 Gemini. 프로젝트마다 AI를 붙이는 건 좋은데, 매번 같은 코드를 다르게 작성하는 게 문제였다.


문제 1: API마다 구조가 다르다

세 프로바이더의 API 호출 방식을 비교하면 이렇다.

// OpenAI — Authorization 헤더, messages 배열
const response = await fetch('https://api.openai.com/v1/chat/completions', {
    headers: { 'Authorization': `Bearer ${apiKey}` },
    body: JSON.stringify({
        model: 'gpt-4o-mini',
        messages: [{ role: 'user', content: message }]
    })
});
const text = data.choices[0].message.content;

// Claude — x-api-key 헤더, anthropic-version 필수, max_tokens 필수
const response = await fetch('https://api.anthropic.com/v1/messages', {
    headers: {
        'x-api-key': apiKey,
        'anthropic-version': '2023-06-01'
    },
    body: JSON.stringify({
        model: 'claude-3-5-sonnet-20241022',
        max_tokens: 1024,  // ← 이거 빠뜨리면 에러
        messages: [{ role: 'user', content: message }]
    })
});
const text = data.content[0].text;

// Gemini — API 키를 URL에 넣음, contents/parts 구조
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
const response = await fetch(url, {
    body: JSON.stringify({
        contents: [{ parts: [{ text: message }] }]
    })
});
const text = data.candidates[0].content.parts[0].text;

인증 방식, 요청 구조, 응답 파싱 전부 다르다. 프로젝트 3개면 이 코드가 3번 중복된다.


문제 2: 프로바이더 전환 비용

OpenAI가 요금을 올리거나, Claude가 새 모델을 출시하거나, Gemini가 무료 티어를 확대할 때. 프로바이더를 바꾸려면:

  1. API 호출 코드 수정
  2. 응답 파싱 로직 수정
  3. 에러 핸들링 수정
  4. 전체 재테스트

한 프로젝트에서 이 작업을 했는데 2일이 걸렸다. 벤더 락인이다.


문제 3: 비용 추적 불가

API를 쓰면서 가장 불안한 건 비용이다. OpenAI 대시보드, Anthropic 대시보드, Google 대시보드를 각각 열어서 확인해야 한다. 프로젝트 단위로 "지금까지 얼마 썼는지"를 한눈에 볼 수 없었다.


문제 4: 장애 대응

API가 429(Rate Limit)를 뱉거나 서버가 다운되면? 수동으로 다른 프로바이더로 전환해야 한다. 자동 폴백이 없다.


해결: AIKit

이 4가지 문제를 한 번에 해결하는 라이브러리를 만들었다.

// Before — 프로바이더마다 다른 코드
const openaiResponse = await callOpenAI(message);
const claudeResponse = await callClaude(message);
const geminiResponse = await callGemini(message);

// After — 하나의 인터페이스
const ai = new AIKit({ provider: 'openai', apiKey: 'sk-...' });
const response = await ai.chat('안녕하세요');
console.log(response.content);  // 동일한 응답 구조

프로바이더를 바꾸고 싶으면 provider: 'claude'로 한 줄만 수정하면 된다.

핵심 기능

기능 설명
통합 인터페이스 OpenAI, Claude, Gemini를 동일한 chat() 메서드로 호출
자동 폴백 프로바이더 장애 시 다음 순위로 자동 전환
비용 추적 프로바이더별, 모델별 실시간 비용 계산
스마트 캐싱 동일 질문에 대한 응답 재사용 (LocalStorage 기반)
응답 검증 QA 관점의 응답 유효성 검사

자동 폴백 예시

const ai = new AIKit({
    autoFallback: true,
    providers: [
        { name: 'openai', apiKey: 'sk-...', priority: 1 },
        { name: 'claude', apiKey: 'sk-ant-...', priority: 2 },
        { name: 'gemini', apiKey: 'AIza...', priority: 3 }
    ]
});

// OpenAI 장애 → 자동으로 Claude 시도 → 그것도 실패하면 Gemini
const response = await ai.chat('분석해줘');

왜 순수 JavaScript인가

TypeScript가 아니라 순수 JavaScript를 선택한 이유:

항목 JavaScript TypeScript
빌드 도구 불필요 필수 (tsc)
브라우저 호환 즉시 실행 빌드 후 실행
CDN 배포 <script> 한 줄 번들링 필요
의존성 0개 @types 등 필요
진입장벽 낮음 중간

CDN으로 <script> 한 줄이면 바로 쓸 수 있는 라이브러리를 목표로 했다. npm 설치도 가능하지만, 가장 빠른 시작은 CDN이다.


Before / After

항목 Before After (AIKit)
프로바이더 전환 2일 1줄 수정
코드 중복 프로바이더당 ~100줄 chat() 한 번
비용 확인 각 대시보드 개별 확인 getCostReport()
장애 대응 수동 전환 자동 폴백

현재 상태

  • 버전: v1.1.1
  • 빌드: ESM + UMD + Minified (Rollup)
  • 테스트: 45개 통과, 커버리지 87%
  • 의존성: 0개 (런타임)
  • GitHub: github.com/lukaPlayground/aikit

마무리

이 글은 AIKit 시리즈 8편 중 첫 번째다. 다음 편에서는 Adapter Pattern을 이용한 아키텍처 설계를 다룬다.


기술 스택: Vanilla JavaScript, Rollup, Jest
소스 코드: GitHub
라이브 데모: AIKit Playground

반응형