OpenAI API를 쓰다가 Claude API로 넘어오면서 생각보다 다른 부분이 많았습니다. 인터페이스 자체는 비슷해 보이지만, 실제로 코드를 짜기 시작하면 설계 철학이 다르다는 게 느껴집니다. 특히 처음에 헷갈렸던 부분과, 써보니 오히려 더 나았던 점을 정리해 둡니다.
system 메시지가 messages 배열 밖에 있다#
OpenAI API에서는 { role: "system", content: "..." }을 messages 배열의 첫 번째 항목으로 넣는 패턴이 익숙합니다. Claude API는 다릅니다. system이 최상위 파라미터로 분리되어 있습니다.
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: "You are a helpful assistant writing in Korean.",
messages: [
{ role: "user", content: "블로그 포스트 제목 5개 제안해줘." }
],
});
처음에는 왜 이렇게 분리했는지 의아했는데, 쓰다 보니 명확해졌습니다. system 프롬프트가 대화 맥락과 섞이지 않아서 코드를 읽을 때 역할 구분이 더 뚜렷합니다. 긴 system 프롬프트를 쓸 때 특히 유용합니다.
max_tokens는 선택이 아니라 필수#
OpenAI API에서는 max_tokens를 생략하면 모델 기본값이 적용됩니다. Claude API에서는 필수 파라미터입니다. 처음에 이걸 빠뜨렸다가 타입 에러를 만났습니다.
type ClaudeRequest = {
model: string;
max_tokens: number; // 필수 — 없으면 컴파일 에러
messages: Array<{ role: "user" | "assistant"; content: string }>;
system?: string;
};
필수로 지정하도록 강제하는 설계가 처음엔 번거롭게 느껴졌지만, 생각해보면 출력 길이를 명시적으로 의식하게 만드는 장점이 있습니다. 무심코 긴 응답을 생성해서 비용이 커지는 상황을 막을 수 있습니다.
prompt caching으로 반복 호출 비용을 줄일 수 있다#
Claude API에서 특히 유용했던 기능은 prompt caching입니다. 긴 system 프롬프트나 문서를 반복해서 넘기는 패턴에서 캐싱을 활성화하면 입력 토큰 비용이 크게 줄어듭니다.
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 2048,
system: [
{
type: "text",
text: longStyleGuideText,
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: userPrompt }],
});
cache_control: { type: "ephemeral" }을 붙이면 5분 TTL 캐시가 적용됩니다. 같은 system 프롬프트를 반복 호출하는 자동화 파이프라인에서 이 옵션 하나로 입력 비용이 90% 가까이 줄어드는 경우도 있습니다.
스트리밍 응답 처리 방식#
스트리밍도 인터페이스가 조금 달랐습니다. OpenAI의 for await...of stream 패턴과 비슷하지만, Claude SDK는 이벤트 타입이 더 구조화되어 있습니다.
const stream = await anthropic.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
process.stdout.write(chunk.delta.text);
}
}
const finalMessage = await stream.finalMessage();
finalMessage()로 전체 응답을 한 번에 받을 수도 있어서, 스트리밍 출력을 화면에 보여주면서 동시에 완료 후 후처리를 하는 패턴을 쓰기 편했습니다.
실제로 써보니#
처음에는 익숙한 API 형태가 아니라 진입 장벽이 느껴졌지만, 막상 쓰기 시작하면 설계가 일관성 있다는 게 느껴집니다. system 분리, max_tokens 필수화, prompt caching 등 각 선택이 나름의 이유를 가지고 있습니다.
AI 관련 글을 계속 써오면서 느끼는 건, API 하나를 쓰는 방식보다 어떤 맥락에서 AI를 도구로 쓰는가가 더 중요하다는 점입니다. 도구가 바뀌어도 제어하려는 목적이 명확하면 적응은 빠릅니다.