제미나이 무료 API, 응답 캐시, 요청 큐, 프로젝트 분리 | 사이드 앱을 무료로 버티는 실습은?
제미나이 무료 API 키 발급 후 앱별 클라우드 프로젝트 분리, 응답 캐시로 RPD 절약, RPM 요청 큐, 사용량 CSV·소프트 캡, 유료 전환 시 원화·부가세 10% 손익 계산까지 단계별 실습으로 정리한다. AI 스튜디오 Rate Limit·Pricing 출처, @google/genai 스모크, 개발자·LLM·사이드 프로젝트·API 운영 가이드.
제미나이(Gemini) 무료 API로 사이드 앱을 하루 종일 돌리려면 키 발급만으로는 부족합니다. 응답 캐시로 RPD를 아끼고, 요청 큐로 RPM을 맞추고, 앱마다 클라우드 프로젝트를 갈라 한도 풀을 분리해야 429가 제품 약속보다 먼저 오지 않습니다. 토큰 요금은 0원이어도 RPM·TPM·RPD 중 하나만 넘으면 거절됩니다. 한도는 키 개수가 아니라 프로젝트 단위로 잡히는 경우가 일반적이고, RPD는 태평양 시간 자정에 리셋됩니다.
한 단계가 통과해야 다음으로 갑니다. 각 단계마다 터미널 출력 또는 파일이 있어야 합니다.
단계
결과물
실패 시 증상
1. 키·프로젝트 분리
앱별 프로젝트 ID + API 키
한 프로젝트에 키만 늘림
2. 스모크 호출
HTTP 200 + 짧은 텍스트
401·모델 ID 오류
3. 한도 스냅샷
RPM·TPM·RPD 메모
블로그 숫자 그대로 배포
4. 응답 캐시
동일 프롬프트 2회차 0 업스트림
캐시 없이 RPD 소진
5. 요청 큐
분당 상한 이하 호출
버스트 후 연속 429
6. 사용량 로그
일별 CSV·소프트 캡
전환 시점 감으로 결정
7. 원화·부가세 표
무료 유지 / 청구 연동 분기
환율·VAT 누락
무료 티어는 입력·출력 토큰 요금이 0원이지만, 요청 횟수와 분당 토큰으로 막힙니다. 공식 설명은 Rate limits와 Pricing에 있습니다. 글 안 숫자는 계산 연습용이며, 배포 직전 두 페이지와 AI 스튜디오 화면으로 덮어쓰세요.
실습 전제 | Node.js 18+, 구글 계정, 터미널. 키는 서버·로컬 스크립트에만 둡니다. 브라우저 번들·공개 저장소·슬랙 스크린샷에 키를 올리지 마세요. 모델 ID 예시(gemini-2.5-flash 등)는 시점에 따라 바뀌므로 AI 스튜디오 모델 목록에서 다시 고정합니다. 1인 개발자 사이드 프로젝트 기준이며, 조직 VPC·IAM이 필요하면 Vertex 경로를 별도로 봅니다.
1단계 | 무료 키 발급과 앱별 프로젝트 분리
키 발급은 짧습니다. 막히는 지점은 “키를 코드에 붙여 넣고 깃에 올리는” 실수와, “키를 여러 개 만들면 한도가 늘어난다”는 오해입니다.
프로젝트 ID를 메모한다. RPM·TPM·RPD는 키마다 따로가 아니라 프로젝트 단위로 합산되는 경우가 일반적이다.
데모 앱·본업 봇·실험 스크립트가 한 프로젝트 키를 공유하면, 실험 한 번이 데모 RPD를 비운다.
로컬은 .env.local, 배포는 호스트 시크릿(버셀 등)으로만 주입한다.
같은 프로젝트에 키를 10개 만들어도 일일 요청(RPD) 풀은 공유됩니다. 앱 단위로 한도를 가르려면 키 복제가 아니라 프로젝트 분리가 맞습니다. 티어 승급(청구 연동·누적 사용)은 한도를 키우는 공식 경로이고, 점검은 사이드 프로젝트 체크리스트를 참고하세요.
앱별 환경 변수 분리 (.env 예시, 깃 제외)
# .env.demo | 데모 앱 전용 프로젝트 키
GEMINI_API_KEY=demo_project_key_here
GEMINI_MODEL=gemini-2.5-flash
SOFT_RPM=8
SOFT_RPD=800
APP_NAME=demo
# .env.bot | 봇/워커 전용 프로젝트 키 (별도 프로젝트)
# GEMINI_API_KEY=bot_project_key_here
# SOFT_RPM=10
# SOFT_RPD=1200
# APP_NAME=bot
# 셸에서 앱 하나 로드 (zsh/bash)
set -a; source .env.demo; set +a
echo "app=$APP_NAME key_len=${#GEMINI_API_KEY}"
# 저장소에 키 문자열이 남았는지 확인 (출력되면 즉시 폐기·재발급)
git grep -nE 'AIza[0-9A-Za-z_-]{20,}' || echo "no key string in repo (good)"
키가 유출되면 AI 스튜디오에서 폐기하고 새로 만듭니다. 무료라도 남이 내 한도를 다 쓰면 내 서비스가 종일 429를 맞습니다. 배포 환경에는 대시보드 시크릿으로만 넣고, NEXT_PUBLIC_ 접두사 변수에는 절대 넣지 마세요.
앱마다 클라우드 프로젝트를 갈라 키를 발급하면 한도 풀이 섞이지 않는다
2단계 | 스모크 호출과 한도 스냅샷
캐시·큐를 붙이기 전에 “키가 살아 있는지”와 “내 프로젝트 한도가 얼마인지”를 고정합니다. 신규 코드는 공식 문서가 안내하는 @google/genai 쪽을 기준으로 맞추는 편이 유지보수에 유리합니다. 구 패키지 @google/generative-ai 예제와 import 경로를 한 파일에서 섞지 마세요.
한도 숫자는 블로그가 아니라 Rate Limit 화면에서 캡처합니다. RPM·TPM·RPD·모델명을 메모장에 적고, 소프트 캡은 실한도의 70~80%로 둡니다.
패키지 설치 + 스모크 호출
mkdir gemini-cache-lab && cd gemini-cache-lab
npm init -y
npm install @google/genai dotenv
# package.json 에 "type": "module" 권장
# smoke.mjs
import 'dotenv/config'
import { GoogleGenAI } from '@google/genai'
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY })
const model = process.env.GEMINI_MODEL || 'gemini-2.5-flash'
const res = await ai.models.generateContent({
model,
contents: '한 줄로 인사만. 무료 티어 스모크.',
config: { maxOutputTokens: 64, temperature: 0.2 },
})
console.log({
model,
text: (res.text || '').slice(0, 120),
usage: res.usageMetadata || null,
})
limits.snapshot.md 템플릿 (배포 전 채우기)
# limits.snapshot.md | git 에 숫자만, 키는 넣지 말 것
# 캡처 시각(KST): 2026-08-09
# 출처: https://aistudio.google.com/rate-limit
project_id: your-gcp-project-id
tier: Free
model: gemini-2.5-flash
# 화면 숫자로 덮어쓰기 (아래는 자리표시)
RPM: ___
TPM: ___
RPD: ___
# 소프트 캡 = 실한도 * 0.75 근처
SOFT_RPM: ___
SOFT_RPD: ___
# 리셋 참고: RPD 는 태평양 시간 자정 (공식 Rate limits 문서)
스모크가 401이면 키·환경 변수, 404·모델 오류면 모델 ID, 429면 이미 한도를 쓴 상태입니다. 스모크 직후 대시보드 사용량이 올라갔는지 확인하면 “어느 프로젝트 키를 썼는지” 오인을 줄일 수 있습니다.
3단계 | 응답 캐시 | 같은 질문은 업스트림을 안 탄다
사이드 앱 RPD를 가장 빨리 비우는 패턴은 “버튼 누를 때마다 같은 프롬프트를 다시 보내는” 것입니다. FAQ·온보딩 문구·고정 템플릿 요약처럼 입력이 자주 반복되면 로컬(또는 Redis) 캐시가 한도보다 먼저 일합니다.
아래는 단일 프로세스용 인메모리 캐시입니다. 서버리스·다중 인스턴스면 공유 저장소로 바꿉니다. TTL은 기능에 맞게 잡으세요. 개인정보가 섞인 프롬프트는 캐시 키에 넣기 전에 해시·마스킹 정책을 정합니다.
response-cache.mjs | TTL 캐시 + 호출 래퍼
import crypto from 'node:crypto'
import { GoogleGenAI } from '@google/genai'
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY })
const model = process.env.GEMINI_MODEL || 'gemini-2.5-flash'
const TTL_MS = Number(process.env.CACHE_TTL_MS || 15 * 60 * 1000)
// 단일 인스턴스 전제. 서버리스면 Redis/Upstash 로 교체
const store = new Map()
function cacheKey(prompt, opts = {}) {
const raw = JSON.stringify({
model,
prompt: String(prompt).trim(),
temperature: opts.temperature ?? 0.2,
maxOutputTokens: opts.maxOutputTokens ?? 256,
})
return crypto.createHash('sha256').update(raw).digest('hex')
}
export async function generateCached(prompt, opts = {}) {
const key = cacheKey(prompt, opts)
const now = Date.now()
const hit = store.get(key)
if (hit && hit.expireAt > now) {
return { text: hit.text, usage: hit.usage, cache: 'hit', key }
}
const res = await ai.models.generateContent({
model,
contents: prompt,
config: {
temperature: opts.temperature ?? 0.2,
maxOutputTokens: opts.maxOutputTokens ?? 256,
},
})
const text = res.text || ''
const usage = res.usageMetadata || null
store.set(key, { text, usage, expireAt: now + TTL_MS })
return { text, usage, cache: 'miss', key }
}
// CLI: node response-cache.mjs
if (import.meta.url === `file://${process.argv[1]}`) {
const q = process.argv[2] || '무료 티어 한도를 한 문장으로.'
console.log(await generateCached(q))
console.log(await generateCached(q)) // 2회차는 cache:hit 이어야 함
}
캐시 키 설계 | 모델·온도·최대 토큰·시스템 지시가 바뀌면 키를 분리하세요. “비슷한 질문”을 뭉개면 잘못된 답을 재사용합니다. 사용자 입력 원문을 디스크에 평문으로 쌓지 말고, 키 해시만 로그에 남기는 편이 안전합니다.
동일 프롬프트는 캐시, 버스트 요청은 큐로 나눠 무료 한도를 지킨다
4단계 | 요청 큐 | 분당 상한을 코드로 지킨다
캐시가 빗나가면 요청이 한꺼번에 몰립니다. 무료 티어에서 흔한 실패는 “프론트 연타 → 서버 동시 호출 → RPM 초과 → 사용자 전원 429”입니다. 소프트 RPM 아래에서만 업스트림을 태우는 간단한 큐를 먼저 붙이세요.
아래 큐는 프로세스 하나 기준입니다. 동시성 1 + 분당 토큰 버킷이면 대부분 사이드 앱에는 충분합니다. 다중 인스턴스면 공유 카운터(Redis INCR + TTL)로 같은 규칙을 옮깁니다.
rpm-queue.mjs | 분당 상한 큐
const softRpm = Number(process.env.SOFT_RPM || 8)
let windowStart = Date.now()
let windowCount = 0
const waiters = []
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
async function acquireSlot() {
for (;;) {
const now = Date.now()
if (now - windowStart >= 60_000) {
windowStart = now
windowCount = 0
}
if (windowCount < softRpm) {
windowCount += 1
return { rpmUsed: windowCount, softRpm }
}
const wait = 60_000 - (now - windowStart) + 50
await sleep(Math.min(wait, 2000))
}
}
export async function runWithRpmLimit(fn) {
const slot = await acquireSlot()
try {
const result = await fn()
return { result, slot }
} catch (e) {
// 업스트림 실패 시 슬롯을 돌려줄지 정책 선택.
// 보수적으로는 반환하지 않아 재버스트를 막음.
throw e
}
}
// 사용 예
// const { result } = await runWithRpmLimit(() => generateCached(prompt))
server-route.mjs | 캐시 + 큐 합친 최소 핸들러
import 'dotenv/config'
import { generateCached } from './response-cache.mjs'
import { runWithRpmLimit } from './rpm-queue.mjs'
export async function handleAsk(userText) {
const prompt = String(userText || '').trim().slice(0, 2000)
if (!prompt) {
const err = new Error('empty_prompt')
err.status = 400
throw err
}
// 캐시 히트면 업스트림·RPM 슬롯을 거의 쓰지 않음
// (generateCached 내부 miss 시에만 API 호출)
const { result, slot } = await runWithRpmLimit(() => generateCached(prompt))
return {
text: result.text,
cache: result.cache,
usage: result.usage,
slot,
}
}
// CLI 부하 흉내: 같은 질문 5번
if (import.meta.url === `file://${process.argv[1]}`) {
const q = '사이드 프로젝트 무료 한도를 두 문장으로.'
for (let i = 0; i < 5; i++) {
const out = await handleAsk(q)
console.log(i + 1, out.cache, out.slot)
}
}
큐 대기 중에도 클라이언트 타임아웃이 짧으면 사용자는 “멈춘 앱”으로 느낍니다. UI에는 “대기열 N번째” 또는 “잠시 후 다시”를 보여주고, 소프트 캡 초과 시 업스트림 재시도는 하지 마세요. 지수 백오프 코드는 쿼터 미터 실습과 첫 호출 튜토리얼에 있는 패턴을 그대로 이어서 쓰면 됩니다.
5단계 | 사용량 로그 | 전환 시점을 숫자로 본다
“며칠 더 무료로 버틸까?”는 감이 아니라 로그로 답합니다. 최소 필드는 시각(KST)·앱 이름·캐시 히트 여부·업스트림 호출 여부·추정 입력/출력 토큰·에러 코드입니다. 하루가 끝나면 RPD 대비 소진율과 캐시 히트율을 보면, 캐시를 키울지·프로젝트를 나눌지·청구를 켤지 갈립니다.
import fs from 'node:fs'
const LOG = process.env.USAGE_LOG || './usage.csv'
const softRpd = Number(process.env.SOFT_RPD || 800)
const today = new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Seoul' }).slice(0, 10)
const lines = fs.existsSync(LOG) ? fs.readFileSync(LOG, 'utf8').trim().split('\n').slice(1) : []
let upstream = 0
let hits = 0
let total = 0
for (const line of lines) {
// ts_kst 가 YYYY-MM-DD 로 시작
if (!line.startsWith(today)) continue
total += 1
const cols = line.split(',')
if (cols[2] === 'hit') hits += 1
if (cols[3] === '1') upstream += 1
}
const hitRate = total ? ((hits / total) * 100).toFixed(1) : '0.0'
const rpdUsedPct = softRpd ? ((upstream / softRpd) * 100).toFixed(1) : 'n/a'
console.log({
today,
total_requests: total,
upstream_calls: upstream,
cache_hit_rate_pct: hitRate,
soft_rpd: softRpd,
soft_rpd_used_pct: rpdUsedPct,
action:
Number(rpdUsedPct) >= 80
? 'soft_rpd 80%+ → 캐시 TTL 연장·기능 제한·청구 검토'
: '여유 있음 → 관측 유지',
})
소프트 RPD의 80%를 넘기 시작하면 기능을 줄이거나, 캐시 TTL을 늘리거나, 청구 연동을 검토합니다. 공개 데모 URL 앞에는 IP·유저 단위 추가 쿼터를 두는 편이 안전합니다. 키가 새는 문제는 서버 게이트 쪽 체크리스트를 이어서 보세요.
6단계 | 원화·부가세 손익 | 유료 전환 시점을 표로 고정
무료 티어는 토큰 단가가 0원이지만, 제품 약속(응답 지연·일일 용량)을 못 지키면 비용이 아니라 이탈로 값이 붙습니다. 반대로 청구를 켠 뒤에는 입력·출력 토큰·모델 단가·환율·부가세(10%)를 같이 넣어야 카드 청구가 예상과 맞습니다. 단가 표는 공식 Pricing을 보고, 아래 숫자는 연습용입니다.
상황
선택
메모
일 업스트림 < 소프트 RPD 60%
무료 유지
캐시·큐만으로 충분
60~80%
기능 제한 + 관측 강화
데모 시간대 제한·모델 다운그레이드
80%+ 또는 429가 사용자에게 노출
청구 연동(Tier 1) 검토
한도 상향·토큰 요금 발생
VPC·IAM·조직 정책 필요
Vertex 경로 검토
AI Studio 무료와 청구 구조가 다름
월 예상 토큰 요금 × 환율 × 1.1
원화 부가세 포함 예산
카드 결제는 청구서 통화·환율 확인
더 긴 손익 표와 Vertex 분기는 유료 전환 손익을 참고하세요. 아래 스크립트는 “월 예상 USD → 원화(연습 환율) → 부가세 10%”만 계산합니다.
무료 유지 | 캐시 히트율 50% 이상, 소프트 RPD 60% 미만, 사용자 대면 429 없음. 프로젝트 분리·큐·로그로 충분.
AI Studio 청구 연동(Tier 1 후보) | 일 사용이 소프트 캡을 반복 돌파하거나, 응답 SLA를 제품에 적은 경우. 청구 설정 후 티어·한도는 Usage tiers 설명을 따른다.
Vertex | 조직 계정·VPC·IAM·기존 GCP 청구 단위가 필요할 때. 사이드 앱 혼자면 보통 AI Studio 경로가 먼저다.
티어 승급 조건(누적 결제·대기 일수)은 공식 문서 기준으로 확인하고, 폼으로 한도 상향을 요청할 수도 있습니다. 보장된 승인은 아니므로, 제품 쪽에는 “한도 초과 시 대기열·캐시·기능 축소” 폴백을 남겨 두세요.
전환 전 체크 | ① 키를 클라이언트에 심지 않았는지 ② 앱별 프로젝트가 갈라져 있는지 ③ 캐시·큐·소프트 캡이 켜져 있는지 ④ usage.csv 7일분 ⑤ Pricing·Rate Limit 화면 스크린샷 날짜. 다섯이 없으면 청구를 켜도 “왜 이렇게 나왔는지”를 설명하기 어렵습니다.
구글 계정으로 AI Studio API key 페이지에 들어가 Create API key를 누르면 됩니다. 클라우드 프로젝트를 연결하거나 새로 만들고, 발급된 문자열은 환경 변수로만 주입하세요. 브라우저 공개 코드·깃 커밋·슬랙 스크린샷에 올리면 즉시 폐기 후 재발급하는 편이 안전합니다.
API 키를 여러 개 만들면 한도가 늘어나나요?
보통 아닙니다. 공식 문서 기준으로 한도는 키마다 따로가 아니라 프로젝트 단위로 적용되는 경우가 많습니다. 같은 프로젝트에 키만 늘리면 RPD 풀을 나눠 쓰는 것에 가깝습니다. 앱마다 한도를 가르려면 클라우드 프로젝트를 분리하세요.
RPM·TPM·RPD 중 무엇을 먼저 보나요?
사이드 앱은 보통 RPD(하루 요청)와 RPM(분당 요청)이 먼저 터집니다. 긴 프롬프트·대량 컨텍스트면 TPM(분당 토큰)도 같이 봅니다. 세 값 중 하나만 넘어도 429가 납니다. 내 숫자는 AI 스튜디오 Rate Limit 화면 스냅샷이 기준입니다.
응답 캐시를 쓰면 개인정보 문제가 생기나요?
프롬프트에 이메일·토큰·주민번호 등이 들어가면 캐시 저장소가 사실상 개인정보 저장소가 됩니다. 캐시 키는 해시로 두고, 원문 저장 여부·TTL·접근 권한을 정한 뒤에만 켜세요. 사용자별 민감 대화는 캐시 대상에서 빼는 편이 낫습니다.
무료로 버티다 유료로 넘길 때 원화 비용은 어떻게 잡나요?
공식 Pricing의 입력·출력 단가(USD)에 월 예상 토큰을 곱한 뒤, 사용 시점 환율과 부가세 10%를 더해 카드 예산을 잡습니다. 무료 티어 구간은 토큰 요금 0원이므로, 청구 연동 이후에만 이 계산이 의미가 있습니다. Vertex는 계정·네트워크 요구가 다를 수 있어 별도 견적이 필요합니다.
서버리스(버셀 등)에서도 인메모리 캐시·큐가 동작하나요?
인스턴스가 여러 개이거나 콜드 스타트가 잦으면 메모리 상태는 공유되지 않습니다. 히트율·RPM 상한을 진지하게 지키려면 Redis·Upstash 같은 공유 저장소로 카운터·캐시를 옮기세요. 단일 장기 프로세스(홈 서버·한 컨테이너)에서는 인메모리로 충분할 수 있습니다.
429가 났을 때 바로 재시도해도 되나요?
즉시 무한 재시도는 RPM·RPD를 더 빨리 태웁니다. 지수 백오프와 최대 2~3회 상한을 두고, 소프트 캡으로 막힌 요청은 재시도하지 말고 사용자에게 대기를 안내하세요. 캐시 히트·큐 대기로 업스트림 호출 자체를 줄이는 쪽이 먼저입니다.
제미나이무료 API응답 캐시요청 큐프로젝트 분리RPMRPD부가세AI Studio개발자LLMAPI사이드 프로젝트