본문 바로가기
글 목록

Claude API 메시지 스레드 베타: 긴 대화에서 새 메시지만 보내는 법

2026년 11월 베타로 나온 Claude API 메시지 스레드의 동작 방식, 고정 필드, 5분 만료·ZDR 같은 제약, 프롬프트 캐싱과의 차이를 공식 문서 기준으로 정리했어요.

IntentCode··9분 읽기

3줄 요약

  • Claude API에 대화를 서버에 잠시 보관하는 메시지 스레드가 베타로 나왔어요(2026-11-02).
  • 이어지는 요청엔 새 메시지만 보내니 요청이 작아지고 첫 토큰까지 시간이 줄어요.
  • 5분 만료·append-only·ZDR 비대상이라 원본 대화는 앱이 계속 가지고 있어야 해요.

메시지 스레드가 푸는 문제

Messages API는 원래 상태가 없는(stateless) API예요. 그래서 대화가 열 번째 턴에 이르면 앞의 아홉 턴과 시스템 프롬프트, 도구 정의를 매번 다시 보내야 했어요. 에이전트 루프에서는 도구 호출 결과까지 쌓이기 때문에 요청 본문이 금방 수 MB 단위로 커져요.

이 방식은 단순하지만 대화가 길어질수록 두 가지 비용이 생겨요. 하나는 네트워크로 같은 내용을 반복해서 보내는 비용이고, 다른 하나는 서버가 큰 요청을 받아 처리하기 시작할 때까지 걸리는 시간이에요. 사용자는 이걸 "답이 늦게 시작된다"로 느껴요.

2026년 11월 2일 Anthropic은 이 문제를 줄이는 메시지 스레드(message threads)를 베타로 공개했어요. 공식 릴리스 노트는 API가 대화를 서버에 보관하므로 이후 요청에는 새 메시지만 보내면 된다고 설명해요. 문서는 요청이 작아지고 첫 토큰까지의 시간(TTFT, time to first token)이 대화가 길어져도 더 낮고 안정적으로 유지된다고 적고 있어요.

동작 방식: create 한 번, continue 여러 번

사용법은 요청 본문에 thread 필드를 하나 추가하는 거예요. 모든 요청에 anthropic-beta: message-threads-2026-08-12 헤더를 붙여야 해요.

첫 요청은 "thread": {"type": "create"}와 함께 지금까지의 전체 대화, system, tools를 보내요. API는 Claude의 답까지 포함한 대화를 응답의 id 아래에 저장해요. 응답 형식 자체는 평소와 같은 메시지예요.

다음 턴부터는 "thread": {"type": "continue", "previous_message_id": "<직전 응답 id>"}를 넣고, 직전 응답 이후의 메시지만 보내요. 보통은 사용자의 다음 질문이거나, 직전 응답의 tool_use에 답하는 tool_result 블록이에요. 그리고 매번 새로 받은 응답 id를 다음 턴에 써요.

bash
curl -sS https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -H "anthropic-beta: message-threads-2026-08-12" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "배송은 보통 며칠 걸려요?"}],
    "thread": {"type": "continue", "previous_message_id": "'"$MESSAGE_ID"'"}
  }'

위 예시는 두 번째 턴이에요. system과 tools가 없다는 점이 핵심이에요.

바꿀 수 있는 필드와 못 바꾸는 필드

스레드는 처음 만든 요청의 설정을 지문(fingerprint)처럼 고정해요. 문서 기준으로 필드는 세 갈래로 나뉘어요.

구분필드continue 요청에서
create 전용system, tools보내면 400 (값이 null이어도)
고정model, tool_choice, thinking, mcp_servers, output_config, 최상위 cache_control, beta 헤더 묶음 등다르면 400 thread_fingerprint_mismatch
자유max_tokens, stop_sequences, stream, metadata, service_tier턴마다 바꿔도 됨

실무에서 자주 걸릴 부분은 모델 전환이에요. 대화 중간에 비용 때문에 작은 모델로 바꾸는 라우팅을 쓰고 있다면, 그 시점에 새 스레드를 create로 시작해야 해요. 시스템 프롬프트에 날짜나 사용자 설정을 매 턴 갱신해 넣는 구조라면 그 값을 메시지 쪽으로 옮기는 설계 변경도 필요해요.

꼭 알아야 할 제약 다섯 가지

첫째, 스레드는 best-effort(최선 노력)예요. 마지막 요청 후 5분이 지나면 만료되고, 마지막 사용 후 1시간 안에 삭제돼요. 저장된 응답이 생길 때마다 5분이 다시 시작돼요. 이어갈 수 없으면 continue 요청이 404와 thread_not_found를 돌려주니, 그때는 전체 대화로 새 create 요청을 보내면 돼요.

둘째, 응답은 그 턴이 저장됐는지 알려 주지 않아요. 그래서 문서는 앱이 대화 원본을 계속 따로 보관하라고 안내해요. 스레드는 저장소가 아니라 전송을 줄이는 캐시에 가깝다고 생각하는 게 안전해요.

셋째, 스레드는 append-only(뒤에 덧붙이기만 가능)이고 각 응답 id는 한 번만 이어갈 수 있어요. 앞의 메시지를 고치거나 예전 지점에서 갈라지려면 새 스레드를 만들어야 해요. "답변 다시 생성" 버튼이 있는 채팅 UI라면 이 경우를 따로 처리해야 해요.

넷째, 스트리밍에서는 응답 id가 message_start 이벤트에 오지만, message_stop을 받은 뒤에 이어가야 해요. 그 전에 continue를 보내면 404가 날 수 있어요. stop_reason이 pause_turn이면 "messages": []로 이어가고, refusal이면 그 스레드는 더 이어가지 말라고 문서는 적고 있어요.

다섯째, 데이터 보존 정책이에요. 스레드는 영구 저장소에 쓰이지 않고 서버 관리 키로 암호화된 메모리에 보관되지만, 제로 데이터 보존(ZDR, zero data retention) 대상 기능은 아니에요. 개인정보 처리방침에 "AI 제공자에 대화를 남기지 않는다"고 적어 둔 서비스라면 켜기 전에 문구부터 확인해야 해요.

프롬프트 캐싱과는 어떻게 다를까

헷갈리기 쉬운 부분이라 따로 정리할게요. 문서는 스레드가 프롬프트를 대신 캐시해 주지 않는다고 분명히 적어요. 프롬프트 캐싱은 스레드가 없을 때와 똑같이 동작하고, 캐시 위치(breakpoint)는 여전히 개발자가 정해요.

즉, 스레드는 "보내는 양"을 줄이고 캐싱은 "다시 읽는 비용"을 줄여요. 둘은 함께 쓰는 게 기본이에요. 최상위 cache_control을 매 요청에 보내면 각 턴이 캐시되도록 할 수 있어요.

다만 스레드를 쓰는 요청은 cache_control 위치를 최대 4개까지만 가질 수 있고, 최상위 cache_control도 하나로 센다는 점을 주의하세요. create 요청의 system, tools에 둔 위치도 continue 요청마다 다시 계산돼요. 넘치면 400 thread_unsupported_request가 나요.

도입 체크리스트

적용 전에 다음을 순서대로 확인해 보세요.

  • 대화가 실제로 긴가요? 두세 턴짜리 질의응답이라면 얻는 게 거의 없어요.
  • 대화 원본을 이미 DB나 메모리에 보관하고 있나요? 404 복구 경로가 이 원본에 기대요.
  • 중간에 모델·시스템 프롬프트·도구를 바꾸는 로직이 있나요? 있다면 그 지점을 새 스레드 시작으로 바꿔야 해요.
  • 사용하는 플랫폼이 지원하나요? 릴리스 노트 기준 Claude API, Claude Platform on AWS, Claude Models on Google Cloud(Anthropic 관리형), Microsoft Foundry에서 베타예요.
  • ZDR이나 규제 요건이 있는 조직인가요? 그렇다면 이 기능은 대상이 아니에요.
python
def send_turn(client, state, new_messages):
    thread = ({"type": "continue", "previous_message_id": state.last_id}
              if state.last_id else {"type": "create"})
    extra = {} if state.last_id else {"system": state.system, "tools": state.tools}
    try:
        res = client.messages.create(model=state.model, max_tokens=1024,
            messages=new_messages if state.last_id else state.history + new_messages,
            extra_body={"thread": thread}, extra_headers={"anthropic-beta": "message-threads-2026-08-12"}, **extra)
    except NotFoundError:  # thread_not_found → 전체 대화로 다시 create
        state.last_id = None
        return send_turn(client, state, new_messages)
    state.history += new_messages + [{"role": "assistant", "content": res.content}]
    state.last_id = res.id
    return res

위 코드는 구조를 보여 주기 위한 스케치예요. 원본 history는 항상 앱이 갖고, 스레드가 사라지면 그 원본으로 다시 시작하는 흐름이 핵심이에요. 실제 SDK의 파라미터 이름과 예외 클래스는 쓰는 SDK 버전의 문서로 확인하세요.

우리 스택에서의 판단

인텐트코드 홈페이지의 챗봇은 Claude로 답을 만드는 RAG 구조예요. 질문 하나에 검색 결과를 붙여 답하는 짧은 대화가 대부분이라, 지금 당장은 스레드보다 프롬프트 캐싱이 더 효과가 큰 쪽이에요. 반대로 도구를 여러 번 부르는 에이전트 루프나 긴 문서를 붙잡고 오래 대화하는 기능이라면 먼저 실험해 볼 만해요.

정리하면 메시지 스레드는 긴 대화의 전송량과 대기 시간을 줄이는 최적화예요. 대화 저장소를 대신하지 않고, 데이터 보존 정책도 바뀌는 점을 기억하면 위험 없이 시험해 볼 수 있어요.

참고 자료

새 글을 메일로 받아 보세요

제품을 만들며 배운 것을 주 2회 정리해요. 궁금한 건 도트에게 물어보세요.