
서버 없이 LLM API를 연결해봤다
처음 LLM API를 써보려고 할 때 "백엔드 서버부터 만들어야 하나?" 생각했다. API 키 노출 걱정도 됐고, CORS 처리도 해야 할 것 같고. 근데 찾아보니 생각보다 직접 연동이 훨씬 쉬웠다. 특히 프로토타입이나 개인 프로젝트에서는 서버 없이도 충분했다.
이번에 실제로 해보면서 알게 된 것들을 정리한다.

어떤 상황에서 직접 연동이 가능한가
먼저 구분이 필요하다. 프론트엔드에서 직접 API를 호출하면 API 키가 브라우저에 노출된다. 그래서 다음 경우에만 직접 연동을 쓸 것:
- 로컬에서만 돌리는 개인 도구
- Electron 앱처럼 코드가 공개되지 않는 환경
- 서버를 통하되 환경변수로만 관리하는 경우 (Next.js API Route 등)
공개 서비스라면 꼭 백엔드를 거치거나 BFF(Backend For Frontend) 패턴을 써야 한다.

OpenAI API 기본 연동
가장 기본적인 OpenAI Chat API 호출이다:
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: '당신은 도움이 되는 어시스턴트입니다.' },
{ role: 'user', content: userMessage }
],
max_tokens: 1000
})
});
const data = await response.json();
const reply = data.choices[0].message.content;
이게 전부다. fetch 하나로 된다. 직접 써봤을 때 "이게 이렇게 간단해?" 싶었다.

스트리밍 응답 처리하기
ChatGPT처럼 글자가 하나씩 출력되는 효과를 내려면 스트리밍이 필요하다. stream: true를 추가하면 된다:
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true
})
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
for (const line of lines) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const token = parsed.choices[0]?.delta?.content || '';
onToken(token); // UI 업데이트 콜백
} catch {}
}
}
SSE(Server-Sent Events) 형식으로 오는 스트림을 직접 파싱하는 코드다. 처음에 이 부분이 좀 낯선데, 패턴을 한 번 익히면 다른 LLM API에도 비슷하게 적용된다.
Anthropic Claude API도 비슷하다
OpenAI 방식을 알면 Claude API도 금방 익힌다. 헤더와 엔드포인트만 다르다:
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-3-5-haiku-20241022',
max_tokens: 1024,
messages: [{ role: 'user', content: userMessage }]
})
});
응답 구조도 약간 다른데, response.content[0].text로 꺼내면 된다.
비용 관리는 처음부터 신경 써야 한다
직접 써보면서 제일 신경 쓰인 건 비용이었다. API는 토큰 수로 과금되니까 프롬프트가 길어질수록 돈이 나간다. 몇 가지 실용적인 방법:
- 모델 선택: GPT-4o 대신 GPT-4o-mini, Claude 3.5 Haiku처럼 소형 모델로 먼저 테스트
- max_tokens 제한: 필요 이상으로 긴 응답 방지
- system 프롬프트 최적화: 공통 지시사항은 짧게, 중복 제거
- rate limiting: 프론트에서 연속 호출 방지 (디바운스 처리)
한계도 있다
직접 연동의 가장 큰 약점은 히스토리 관리가 수동이라는 거다. OpenAI는 대화 히스토리를 서버에서 기억하지 않는다. 매번 이전 메시지를 전부 넘겨줘야 한다. 대화가 길어질수록 토큰 비용이 선형으로 증가한다.
그래서 실제 서비스 수준의 챗봇을 만든다면 결국 서버 쪽에서 히스토리를 관리하고 적절히 요약하거나 트리밍하는 로직이 필요해진다.
프로토타입에는 충분하다
아이디어 검증, 개인 도구, 사내 전용 앱 정도라면 서버 없이 직접 연동해도 충분히 쓸 만하다. 환경변수만 제대로 관리하고 API 키 노출만 조심하면 된다. 복잡한 인프라 없이 LLM 기능을 빠르게 붙일 수 있다는 게 꽤 매력적이었다.
📎 참고 자료
'ai' 카테고리의 다른 글
| Cursor Automations 써보니, 자리 비워도 에이전트가 일한다 (0) | 2026.03.25 |
|---|---|
| ChatGPT 3월 업데이트, 대학생이면 지금 확인해볼 것 (0) | 2026.03.25 |
| 알리바바가 Slack에 AI 에이전트를 붙이겠다는 얘기 (0) | 2026.03.24 |
| MiniMax M2.7 써보니, 스스로 진화한다는 게 반은 사실이었다 (0) | 2026.03.24 |
| DeepSeek V4, 계속 미뤄지고 있는 이유가 뭘까 (0) | 2026.03.24 |