제미나이 무료 API, 일일 RPD 게이트, 스트리밍, 유료 전환 | 서버에 붙이는 실습 순서는?
제미나이(Gemini) 무료 API를 AI 스튜디오 키 발급부터 서버 전용 호출, RPM·RPD 소프트 게이트, Python 스트리밍, Express 프록시, 유료 전환 시 원화·부가세 시나리오 계산까지 실습 순서로 정리한다. LLM·개발자·API·SDK·사이드 프로젝트 운영과 FAQ, 공식 Rate limits·Pricing 링크를 담은 튜토리얼.
제미나이(Gemini) 무료 API로 사이드 프로젝트를 돌리려면 키만 받아서는 부족합니다. 키 발급 → 서버 전용 호출 → RPM·RPD 게이트 → 스트리밍 응답 → 유료 전환 손익 표까지 한 루프로 묶어야 피크 날 429에 서비스가 멈추지 않습니다. 토큰 요금 0원 구간은 구글 AI 스튜디오(Google AI Studio) 무료 티어에서 시작하지만, 한도는 프로젝트 단위로 RPM(분당 요청)·TPM(분당 토큰)·RPD(일일 요청) 중 하나만 넘어도 거절됩니다. 아래는 1인 개발자가 공개 전에 붙이는 서버 실습 순서입니다. 고정 숫자는 계정·모델·시점에 따라 바뀌므로, 배포 전 AI Studio Rate Limit 화면을 최종 기준으로 두세요.
이 튜토리얼이 끝나면 로컬에 다음 파일이 남습니다. 브라우저에 키를 심는 패턴은 처음부터 버립니다.
단계
결과물
실패 신호
1. 키·환경
GEMINI_API_KEY 분리
깃에 키 문자열
2. 한도 읽기
RPM·TPM·RPD 캡처
블로그 숫자만 신뢰
3. 동기 호출
Python 한 줄 응답
401·404 모델
4. 스트리밍
청크 출력 루프
타임아웃·끊김
5. RPD 게이트
일일 카운터 파일
자정 PT 리셋 오해
6. Express 프록시
서버 전용 엔드포인트
클라이언트 직호출
7. 유료 손익
원화·부가세 시나리오
USD만 보고 결정
무료 티어는 입력·출력 토큰 요금이 0원인 대신, 요청 횟수·분당 토큰으로 막힙니다. RPD는 태평양 시간(PT) 자정에 리셋된다는 점만 기억해 두면, 국내 저녁 피크 설계가 달라집니다. 공식 설명은 Rate limits 문서에 있습니다.
실습 전제 — Python 3.10+ 또는 Node.js 18+, 구글 계정, 터미널. 모델 ID 예시는 시점에 따라 바뀝니다. 글 안의 gemini-2.5-flash 등은 배포 전 AI Studio 모델 목록에서 다시 고정하세요. 가격·한도는 “교육용 골격”이며 공식 Pricing·Rate Limit 화면이 우선입니다.
1단계 | 무료 API 키 발급과 서버 전용 보관
키 자체는 짧습니다. 막히는 지점은 “프론트에 넣고 배포한 뒤 한도가 하루 만에 증발”하는 패턴입니다.
아래는 로컬 최소 패턴입니다. 커밋 전에 키 문자열이 워킹 트리에 없는지 한 번 더 검색하세요.
환경 변수 준비 (.env, 깃 제외)
# .env — .gitignore 에 반드시 추가
GEMINI_API_KEY=여기에_발급받은_키
GEMINI_MODEL=gemini-2.5-flash
# 일일 소프트 캡 (AI Studio 실한도보다 낮게 잡기)
GEMINI_RPD_SOFT_CAP=80
GEMINI_RPM_SOFT_CAP=8
# 셸 1회 로드
set -a; source .env; set +a
echo "key_len=${#GEMINI_API_KEY} model=$GEMINI_MODEL"
# 실수 커밋 여부 (출력되면 즉시 폐기·재발급)
git grep -n "AIza" || echo "no key string in repo (good)"
키가 유출되면 AI Studio에서 폐기하고 새로 만듭니다. 무료라도 남이 내 RPD를 다 쓰면 내 사이드 프로젝트가 종일 429를 맞습니다. 버셀·클라우드 런 등에는 대시보드 시크릿으로만 넣고, 클라이언트 번들·모바일 앱 바이너리에는 절대 넣지 마세요.
제미나이 무료 API 키는 AI 스튜디오에서 발급하고 서버 환경 변수로만 주입한다
2단계 | RPM·TPM·RPD를 화면에서 캡처하기
블로그에 적힌 “Flash 10 RPM” 같은 숫자를 코드 상수로 박아 두면 한 달 뒤 틀린 값이 됩니다. 공식 문서는 한도가 티어·모델·계정 상태에 따라 달라진다고 명시하고, 실수치는 AI Studio Rate Limit 화면을 보라고 안내합니다.
지표
의미
사이드 프로젝트 팁
RPM
분당 요청 수
에이전트 다중 호출이면 가장 먼저 터짐
TPM
분당 입력 토큰
긴 컨텍스트·파일 업로드 시 주의
RPD
일일 요청 수
PT 자정 리셋 — KST 오후 4~5시경 리셋 감각
사용 티어
Free → Tier1(결제 연동) → 2·3(누적 지출)
한도 상향이 목표면 결제 연동 검토
지금 할 일: Rate Limit 화면을 스크린샷 또는 표로 옮겨 docs/gemini-quota.md 한 장에 적습니다. 소프트 캡은 실한도의 70~80%로 잡으면 피크 날 여유를 남길 수 있습니다. 개념 가이드는 무료 티어 가이드를 참고하세요.
3단계 | Python으로 동기 호출 한 번 통과시키기
SDK 이름·API 표면은 업데이트될 수 있습니다. 아래는 REST generateContent 를 직접 치는 최소 예로, 패키지 버전 차이를 줄입니다. 먼저 이게 200이면 네트워크·키·모델 ID는 살아 있는 겁니다.
Python 동기 호출 (requests)
import os, json, urllib.request
API_KEY = os.environ["GEMINI_API_KEY"]
MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent"
body = {
"contents": [{"parts": [{"text": "한 문장으로 서버 상태 점검 메시지를 써 줘."}]}],
"generationConfig": {"maxOutputTokens": 128, "temperature": 0.2},
}
req = urllib.request.Request(
URL,
data=json.dumps(body).encode("utf-8"),
headers={
"Content-Type": "application/json",
"x-goog-api-key": API_KEY,
},
method="POST",
)
with urllib.request.urlopen(req, timeout=60) as res:
data = json.loads(res.read().decode("utf-8"))
text = data["candidates"][0]["content"]["parts"][0]["text"]
usage = data.get("usageMetadata", {})
print("text:", text.strip())
print("usage:", usage)
# 401/403 → 키·권한 / 404 → 모델 ID / 429 → 한도
usageMetadata 에 입력·출력 토큰이 오면 나중에 유료 전환 계산에 그대로 씁니다. 응답이 비어 있으면 안전 필터·후보 차단 가능성이 있으니, 에러 본문 전체를 로그에 남기되 개인정보는 마스킹하세요.
4단계 | 스트리밍으로 체감 지연 줄이기
사이드 프로젝트 UI에서 “10초 동안 빈 화면”은 이탈로 이어집니다. 무료 티어에서도 스트림 엔드포인트를 쓰면 첫 토큰을 빨리 보여줄 수 있습니다. 아래는 streamGenerateContent 의 SSE 형태를 라인 단위로 읽는 골격입니다.
Python 스트리밍 (streamGenerateContent)
import os, json, urllib.request
API_KEY = os.environ["GEMINI_API_KEY"]
MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
URL = (
f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}"
f":streamGenerateContent?alt=sse"
)
body = {
"contents": [{"parts": [{"text": "세 문장으로 배포 체크리스트를 말해 줘."}]}],
}
req = urllib.request.Request(
URL,
data=json.dumps(body).encode("utf-8"),
headers={
"Content-Type": "application/json",
"x-goog-api-key": API_KEY,
"Accept": "text/event-stream",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=120) as res:
buf = b""
while True:
chunk = res.read(256)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
line = line.decode("utf-8", errors="ignore").strip()
if not line.startswith("data:"):
continue
payload = line[5:].strip()
if not payload or payload == "[DONE]":
continue
try:
obj = json.loads(payload)
except json.JSONDecodeError:
continue
parts = (
obj.get("candidates", [{}])[0]
.get("content", {})
.get("parts", [])
)
for p in parts:
t = p.get("text")
if t:
print(t, end="", flush=True)
print()
스트림은 요청 1회로 잡히는 경우가 많지만, 재시도 로직이 스트림을 여러 번 다시 열면 RPD가 빨리 닳습니다. 타임아웃 시 “부분 응답 표시 + 재시도 버튼”이 자동 재호출보다 안전합니다. 프론트는 서버가 중계한 스트림만 구독하게 하세요.
스트리밍은 첫 토큰을 빨리 보여 체감 지연을 줄이지만, 재시도 폭주 시 RPD를 더 빨리 소진한다
5단계 | 일일 RPD 소프트 게이트 코드
구글 한도에 닿기 전에 앱이 먼저 막는 편이 UX가 낫습니다. 429를 사용자에게 그대로 보여 주면 “서비스 다운”으로 느껴집니다. 아래는 날짜(PT) 기준 JSON 파일 카운터입니다. 단일 인스턴스·저트래픽 사이드에 적합하고, 다중 인스턴스면 Redis 등으로 바꾸면 됩니다.
RPD 소프트 게이트 (Python)
import json, os, time
from datetime import datetime, timezone, timedelta
from pathlib import Path
# 태평양 시간 근사: 서머타임 여부는 zoneinfo 로 교체 권장
PT = timezone(timedelta(hours=-8))
STATE = Path(os.environ.get("GEMINI_QUOTA_FILE", ".gemini_quota.json"))
SOFT_CAP = int(os.environ.get("GEMINI_RPD_SOFT_CAP", "80"))
def pt_day() -> str:
return datetime.now(PT).strftime("%Y-%m-%d")
def load_state():
if not STATE.exists():
return {"day": pt_day(), "count": 0}
data = json.loads(STATE.read_text(encoding="utf-8"))
if data.get("day") != pt_day():
return {"day": pt_day(), "count": 0}
return data
def save_state(data):
STATE.write_text(json.dumps(data), encoding="utf-8")
def acquire_slot() -> dict:
"""호출 직전에 호출. allowed=False 면 업스트림 요청 금지."""
st = load_state()
if st["count"] >= SOFT_CAP:
return {
"allowed": False,
"count": st["count"],
"cap": SOFT_CAP,
"retry_hint": "PT 자정 이후 또는 유료 티어 검토",
}
st["count"] += 1
save_state(st)
return {"allowed": True, "count": st["count"], "cap": SOFT_CAP}
# 사용 예
slot = acquire_slot()
if not slot["allowed"]:
raise SystemExit(f"soft RPD cap: {slot}")
# ... 여기서 generateContent / stream 호출 ...
print("quota", slot)
소프트 캡을 실한도와 같게 잡으면, 배치 작업·관리자 테스트가 사용자 쿼터를 먼저 먹어 버릴 수 있습니다. 운영 키와 배치 키를 프로젝트로 나누거나, 관리자 경로는 별도 일일 예산을 두는 편이 안전합니다. 분당 폭주는 아래 Express 쪽에서 간단한 슬라이딩 윈도우로 막습니다.
6단계 | Express 프록시 + RPM 가드
프론트에서 제미나이 엔드포인트를 직접 치면 키가 노출됩니다. 앱 서버가 프록시하고, 사용자·IP 단위로 분당 상한을 거는 최소 패턴을 둡니다. 패키지 @google/genai 를 써도 되고, 아래처럼 fetch 로 동일 REST를 감싸도 됩니다.
Express 서버 프록시 (RPM + RPD 가드)
// server.mjs (Node 18+)
import express from "express";
import fs from "fs";
import path from "path";
const app = express();
app.use(express.json({ limit: "32kb" }));
const KEY = process.env.GEMINI_API_KEY;
const MODEL = process.env.GEMINI_MODEL || "gemini-2.5-flash";
const RPM = Number(process.env.GEMINI_RPM_SOFT_CAP || 8);
const RPD = Number(process.env.GEMINI_RPD_SOFT_CAP || 80);
const STATE = process.env.GEMINI_QUOTA_FILE || ".gemini_quota.json";
// 프로세스 메모리 RPM (단일 인스턴스용)
const rpmHits = new Map(); // ip -> timestamps[]
function allowRpm(ip) {
const now = Date.now();
const windowMs = 60_000;
const arr = (rpmHits.get(ip) || []).filter((t) => now - t < windowMs);
if (arr.length >= RPM) return false;
arr.push(now);
rpmHits.set(ip, arr);
return true;
}
function ptDay() {
// 간단 근사: UTC-8. 운영에서는 Temporal/ luxon 권장
const d = new Date(Date.now() - 8 * 3600_000);
return d.toISOString().slice(0, 10);
}
function takeRpd() {
let st = { day: ptDay(), count: 0 };
try {
st = JSON.parse(fs.readFileSync(STATE, "utf8"));
} catch {}
if (st.day !== ptDay()) st = { day: ptDay(), count: 0 };
if (st.count >= RPD) return { ok: false, st };
st.count += 1;
fs.writeFileSync(STATE, JSON.stringify(st));
return { ok: true, st };
}
app.post("/api/gemini/chat", async (req, res) => {
const ip = req.headers["x-forwarded-for"]?.toString().split(",")[0] || req.ip;
if (!KEY) return res.status(500).json({ error: "missing_key" });
if (!allowRpm(ip)) return res.status(429).json({ error: "rpm_soft_cap" });
const rpd = takeRpd();
if (!rpd.ok) return res.status(429).json({ error: "rpd_soft_cap", ...rpd.st });
const prompt = String(req.body?.prompt || "").slice(0, 2000);
if (!prompt) return res.status(400).json({ error: "empty_prompt" });
const url = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`;
const r = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": KEY,
},
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { maxOutputTokens: 512, temperature: 0.3 },
}),
});
const data = await r.json().catch(() => ({}));
if (!r.ok) {
// 업스트림 429는 소프트 캡과 구분해 로그
console.error("upstream", r.status, JSON.stringify(data).slice(0, 400));
return res.status(r.status === 429 ? 429 : 502).json({
error: "upstream_error",
status: r.status,
});
}
const text =
data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
res.json({
text,
usage: data.usageMetadata || null,
quota: rpd.st,
});
});
app.listen(8787, () => console.log("gemini proxy on :8787"));
배포 시에는 인증(로그인·API 토큰)·CORS 제한·본문 길이 상한·프롬프트 인젝션 가드를 추가하세요. 무료 티어라도 공개 엔드포인트에 인증이 없으면 봇이 RPD를 먼저 소진합니다. 429 응답은 사용자에게 “잠시 후 다시 시도” 문구와 예상 대기(분 단위)를 보여 주면 이탈이 줄어듭니다.
429 처리 한 줄 — 소프트 캡 429는 즉시 재시도하지 않습니다. 업스트림 429만 지수 백오프(예: 2s → 4s → 8s, 최대 3회)를 쓰고, 일일 캡이면 재시도 없이 폴백 문구로 끝냅니다. 백오프 폭주는 한도를 더 갉아먹습니다. 상세 백오프 예는 첫 호출 튜토리얼을 참고하세요.
7단계 | JSON 구조화 출력으로 토큰 낭비 줄이기
사이드 프로젝트에서 “설명 문단”을 받은 뒤 파싱하면 실패율이 올라가고 재호출이 늘어 RPD가 빨리 닳습니다. 스키마를 프롬프트에 고정하고 responseMimeType 을 JSON으로 두면(지원 모델·시점 확인) 파싱 재시도가 줄어듭니다.
필드가 깨지면 업스트림을 두 번 치지 말고, 로컬 검증 실패 시 사용자에게 수정 폼을 보여 주는 편이 RPD에 유리합니다. 장문 요약이 필요하면 Flash-Lite·Flash 계열로 먼저 돌리고, 실패 케이스만 상위 모델로 올리는 2단 라우팅도 한도 절약에 도움이 됩니다. 단가 표 읽기는 Flash thinking 가격을 참고하세요.
유료 전환 전에는 2주치 usageMetadata 와 소프트 캡 히트를 표로 모아 손익 시나리오를 만든다
8단계 | 유료 전환 시점 | 원화·부가세 시나리오 계산
무료를 고수할 신호와 유료로 넘어갈 신호는 다릅니다. “돈이 아깝다”가 아니라 한도가 제품 약속을 깨는가로 판단하세요.
상황
권장 방향
메모
일 사용이 소프트 캡의 50% 미만
무료 유지
캐시·배치 시간대 조정
피크에 RPM·RPD 429 주 3회+
결제 연동(Tier 1) 검토
한도 상향이 1순위
데이터 학습 사용 정책이 부담
유료 약관 확인
무료/유료 약관 차이 공식 Terms
조직 IAM·VPC·감사 로그 필요
Vertex 검토
개발자 API와 청구 구조 다름
월 토큰 비용이 구독 대안보다 큼
모델·캐시·배치 재설계
단가만 올리지 말 것
유료 단가는 모델마다 다릅니다. 예: Flash-Lite 계열은 입력·출력이 상대적으로 낮고, Pro 계열은 높습니다. 아래 스크립트는 교육용 골격으로, 단가·환율을 본인 표로 덮어쓴 뒤 쓰세요. 공식 표는 Gemini Developer API pricing 입니다.
월 비용 시나리오 (USD → 원화, 부가세 가정)
# cost_scenario.py — 숫자는 예시. 배포 전 공식 Pricing 으로 교체
USD_KRW = 1400 # 조회 시점 환율
VAT_RATE = 0.10 # 카드 명세 기준이 다를 수 있음 → 시나리오용
# 예시: Flash-Lite 계열 근사 (1M 토큰당 USD) — 반드시 공식 표로 교체
PRICE = {
"flash_lite": {"in": 0.30, "out": 2.50},
"flash": {"in": 0.50, "out": 3.00},
}
def monthly_usd(model: str, in_tokens: int, out_tokens: int) -> float:
p = PRICE[model]
return (in_tokens / 1_000_000) * p["in"] + (out_tokens / 1_000_000) * p["out"]
def to_krw(usd: float, with_vat: bool = True) -> int:
krw = usd * USD_KRW
if with_vat:
krw *= (1 + VAT_RATE)
return int(round(krw))
# 2주 실측을 30일로 환산한 가정 예
scenarios = [
("flash_lite", 8_000_000, 1_500_000),
("flash", 8_000_000, 1_500_000),
("flash_lite", 30_000_000, 6_000_000),
]
for model, inn, out in scenarios:
usd = monthly_usd(model, inn, out)
print(model, f"USD {usd:.2f}", f"KRW~{to_krw(usd)} (부가세 시나리오 포함)")
# 출력 예시를 스프레드시트에 붙이고, 카드 명세 1회와 대조
골격 해석 예 — Flash-Lite 입력 8M·출력 1.5M을 위 예시 단가로 잡으면 USD 약 6.15, 환율 1,400원이면 약 8,600원, 부가세 10% 시나리오를 더하면 약 9,500원 전후입니다. 이 숫자는 교육용이며 실제 청구·환율·수수료와 다를 수 있습니다. AI 스튜디오 유료(개발자 API 결제 연동)와 Vertex 중 선택은 같은 모델 계열이라도 인증·IAM·청구 단위가 다릅니다. MVP·기존 키 코드 유지는 개발자 API 유료 연동이 단순하고, 조직 규정·VPC가 필요하면 Vertex를 검토하는 흐름이 일반적입니다. 표 중심 손익은 유료 전환 손익 글을 이어서 보세요.
주의 — 원화·부가세 예시는 의사결정용 시나리오입니다. “월 몇 원 확정”처럼 단정하지 말고, 스프레드시트에 단가 조회일·환율 조회일을 적으세요. 실제 기준은 구글 청구서·카드 명세입니다.
배포 전 10분 체크 | 무료 티어가 깨지는 지점
키가 클라이언트 번들·공개 저장소에 없는가.
Rate Limit 화면 숫자를 docs/gemini-quota.md 에 캡처했는가.
소프트 RPD·RPM 이 실한도보다 낮은가.
프록시 앞에 인증 또는 최소한의 남용 방지(IP·유저 쿼터)가 있는가.
업스트림 429와 소프트 캡 429를 로그에서 구분하는가.
스트림 재시도가 무한 루프가 아닌가.
usageMetadata 를 2주간 저장하는가 (유료 전환 근거).
모델 ID가 환경 변수로 고정돼 있는가.
폴백 문구·기능 플래그 off 경로가 있는가.
유료 전환 시 약관(데이터 사용)·단가표를 다시 읽었는가.
이 목록을 PR 템플릿 한 줄로 넣으면, “일단 키만 넣고 배포” 사고가 줄어듭니다. 내부 도구 수준이면 무료만으로도 충분한 경우가 많고, 공개 서비스 SLA가 생기면 결제 연동을 미루지 않는 편이 운영 비용이 낮습니다.