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를 도구로 쓰는가가 더 중요하다는 점입니다. 도구가 바뀌어도 제어하려는 목적이 명확하면 적응은 빠릅니다.