메시지 처리 파이프라인

제출, 라우팅, 입력 분류, 메시지 구성, API 정규화 — 4단계 파이프라인.

01

개요

사용자가 Enter를 누르면 텍스트가 Claude API에 도달하기까지 4단계 파이프라인을 통과합니다. 각 단계에서 입력이 분류, 확장, 변환되며, 최종적으로 API가 이해할 수 있는 정규화된 메시지 배열로 변환됩니다.

다루는 소스 파일: messages/handlePromptSubmit.tsmessages/processUserInput.tsmessages/processTextPrompt.tsmessages/normalizeMessagesForAPI.ts

4단계 파이프라인:

02

1단계: Submit & Route

handlePromptSubmit은 사용자 입력을 처리하는 최초 진입점입니다. 큐 가드로 동시 제출을 방지하고, 붙여넣기된 참조를 확장합니다.

// messages/handlePromptSubmit.ts — 큐 가드 & 참조 확장
let isProcessing = false

async function handlePromptSubmit(rawInput: string): Promise<void> {
  // 큐 가드: 이전 요청이 처리 중이면 무시
  if (isProcessing) {
    showWarning('이전 요청을 처리 중입니다...')
    return
  }
  isProcessing = true

  try {
    // 붙여넣기 참조 확장: @파일경로 → 파일 내용 인라인
    const expanded = expandPasteReferences(rawInput)

    // 입력 분류로 전달
    await processUserInput(expanded)
  } finally {
    isProcessing = false
  }
}

붙여넣기 참조 확장

사용자가 @src/main.ts 형태로 파일을 참조하면, 해당 파일의 내용이 인라인으로 확장됩니다. 이는 Claude에게 파일 전체를 보여주고 싶을 때 수동으로 복사-붙여넣기하는 수고를 덜어줍니다.

주의사항

큐 가드는 단순 boolean 플래그입니다. finally 블록에서 반드시 해제해야 하며, 예외 발생 시 해제되지 않으면 이후 모든 입력이 차단됩니다.

03

2단계: Input Classification

processUserInput은 확장된 입력을 분석하여 3방향 라우팅을 수행합니다. 이미지 전처리, ultraplan 키워드 감지도 이 단계에서 처리됩니다.

// messages/processUserInput.ts — 3방향 라우팅
async function processUserInput(input: ExpandedInput): Promise<void> {
  // 이미지 전처리: 붙여넣기된 이미지를 base64로 변환
  const processed = await preprocessImages(input)

  // 라우팅 1: 슬래시 커맨드
  if (processed.text.startsWith('/')) {
    return handleSlashCommand(processed)
  }

  // 라우팅 2: ULTRAPLAN 키워드 감지
  if (shouldTriggerUltraplan(processed.text)) {
    return launchUltraplan(processed)
  }

  // 라우팅 3: 일반 메시지 → 메시지 구성으로
  await processTextPrompt(processed)
}

이미지 전처리

사용자가 이미지를 터미널에 붙여넣으면 자동으로 감지되어 base64로 인코딩됩니다. 지원 형식(PNG, JPEG, GIF, WebP)이 확인되고, 크기 제한(20MB)이 적용됩니다.

3방향 라우팅

입력은 세 가지 경로 중 하나로 분기됩니다:

04

3단계: Message Construction

메시지 구성 단계에서는 사용자 입력을 Claude API가 이해하는 메시지 구조로 변환합니다. processTextPrompt이 텍스트를 처리하고, createUserMessage가 최종 메시지 객체를 생성합니다.

// messages/processTextPrompt.ts — 메시지 구성
async function processTextPrompt(input: ProcessedInput): Promise<void> {
  // 사용자 메시지 생성
  const userMsg = createUserMessage({
    text: input.text,
    images: input.images,       // base64 인코딩된 이미지
    files: input.fileContents,  // @참조로 확장된 파일 내용
  })

  // 합성 메시지: 컨텍스트 강화를 위한 추가 메시지
  const syntheticMsgs = buildSyntheticMessages({
    systemMemory: getSessionMemory(),
    recentFiles: getRecentlyEditedFiles(),
    gitStatus: getGitStatus(),
  })

  // 메시지 배열 구성
  const messages = [...syntheticMsgs, userMsg]

  // API 정규화로 전달
  await sendToAPI(normalizeMessagesForAPI(messages))
}

합성 메시지

합성(synthetic) 메시지는 사용자가 직접 입력하지 않았지만, 컨텍스트를 강화하기 위해 자동으로 생성되는 메시지입니다. 세션 메모리, 최근 편집 파일, git 상태 등이 합성 메시지로 주입됩니다.

딥 다이브 — createUserMessage의 다중 콘텐츠 블록

Claude API의 메시지 형식은 하나의 메시지 내에 여러 콘텐츠 블록(텍스트, 이미지, 파일)을 포함할 수 있습니다. createUserMessage는 사용자의 텍스트, 붙여넣기 이미지, @ 참조 파일을 모두 하나의 메시지 내 별도 콘텐츠 블록으로 구성합니다.

05

4단계: API Normalization

normalizeMessagesForAPI는 내부 메시지 배열을 Claude API가 요구하는 형식으로 정규화합니다. 다중 패스로 처리됩니다.

// messages/normalizeMessagesForAPI.ts — 다중 패스 정규화
function normalizeMessagesForAPI(messages: InternalMessage[]): APIMessage[] {
  let result = messages

  // 패스 1: 연속된 같은 역할의 메시지를 병합
  // API 제약: user/assistant가 교대로 와야 함
  result = mergeConsecutiveSameRole(result)

  // 패스 2: 빈 텍스트 블록 제거
  result = removeEmptyTextBlocks(result)

  // 패스 3: 도구 사용 결과를 올바른 위치에 배치
  result = reorderToolResults(result)

  // 패스 4: 컨텍스트 윈도우 초과 시 오래된 메시지 압축
  result = compressIfOverLimit(result, contextWindowSize)

  // 패스 5: 최종 API 형식 변환
  return result.map(toAPIFormat)
}
딥 다이브 — 왜 다중 패스인가?

단일 패스로 모든 정규화를 처리하면 복잡한 상호 의존성이 발생합니다. 예를 들어, 합성 메시지 삽입(패스 3)이 연속 동일 역할 제약(패스 1)을 위반할 수 있습니다. 다중 패스는 각 변환을 독립적으로 적용하여 순서에 대한 추론을 단순화합니다.

06

메시지 분류 — 6타입

내부적으로 메시지는 6가지 타입으로 분류됩니다. 각 타입은 파이프라인에서 다르게 처리됩니다.

// 6가지 메시지 타입
type MessageType =
  | 'user_text'        // 사용자가 직접 입력한 텍스트
  | 'user_image'       // 사용자가 붙여넣기한 이미지
  | 'assistant_text'   // Claude의 텍스트 응답
  | 'tool_use'         // Claude의 도구 사용 요청
  | 'tool_result'      // 도구 실행 결과
  | 'synthetic'        // 시스템이 자동 생성한 컨텍스트 메시지

각 타입의 정규화 처리:

07

쿼리 프로파일링

메시지 처리 파이프라인은 각 쿼리의 특성을 프로파일링하여 최적의 처리 전략을 결정합니다.

// 쿼리 프로파일링 — 입력 특성 분석
interface QueryProfile {
  tokenCount: number         // 입력 토큰 수 추정
  hasImages: boolean          // 이미지 포함 여부
  hasFileRefs: boolean        // @파일 참조 포함 여부
  estimatedComplexity: 'simple' | 'moderate' | 'complex'
  suggestedEffort: EffortLevel // 권장 노력 수준
}

function profileQuery(messages: InternalMessage[]): QueryProfile {
  const totalTokens = estimateTokens(messages)
  const hasImages = messages.some(m => m.type === 'user_image')

  return {
    tokenCount: totalTokens,
    hasImages,
    hasFileRefs: messages.some(m => m.fileRefs?.length > 0),
    estimatedComplexity: totalTokens > 50_000 ? 'complex'
      : totalTokens > 10_000 ? 'moderate' : 'simple',
    suggestedEffort: inferEffort(totalTokens, hasImages),
  }
}
주의사항

토큰 수 추정은 실제 토큰화 없이 휴리스틱으로 계산됩니다(예: 영어 4자 = 1토큰). 정확한 토큰 수는 API 응답의 usage 필드에서만 알 수 있습니다. 추정치는 컨텍스트 윈도우 초과 여부를 판단하는 데만 사용됩니다.

08

핵심 요약

핵심 포인트

  • 메시지 처리는 4단계 파이프라인으로 구성됩니다: Submit & Route → Input Classification → Message Construction → API Normalization
  • handlePromptSubmit큐 가드는 동시 제출을 방지하고, 붙여넣기 참조 확장@파일경로를 파일 내용으로 인라인합니다
  • processUserInput3방향 라우팅(슬래시 커맨드, ULTRAPLAN, 일반 메시지)을 수행합니다
  • 메시지 구성 단계에서 합성 메시지(세션 메모리, git 상태 등)가 자동으로 추가되어 컨텍스트를 강화합니다
  • normalizeMessagesForAPI다중 패스로 정규화합니다: 역할 병합 → 빈 블록 제거 → 도구 결과 정렬 → 컨텍스트 압축 → API 형식 변환
  • 메시지는 6가지 타입(user_text, user_image, assistant_text, tool_use, tool_result, synthetic)으로 분류됩니다
  • 쿼리 프로파일링이 토큰 수, 복잡도, 이미지 유무를 분석하여 최적의 처리 전략을 결정합니다
09

지식 확인

퀴즈 — 5문제

Q1. handlePromptSubmit의 큐 가드가 필요한 이유는?

  • A) API Rate Limit을 준수하기 위해
  • B) 이전 요청이 처리 중일 때 새 입력이 동시에 제출되는 것을 방지하여 상태 충돌을 예방
  • C) 메모리 사용량을 제한하기 위해
  • D) 사용자 경험을 위해 입력 속도를 조절하기 위해
이전 요청의 API 호출이 진행 중일 때 새 제출이 들어오면 메시지 배열의 상태가 불일치할 수 있습니다. 큐 가드는 한 번에 하나의 제출만 처리되도록 보장합니다.

Q2. processUserInput의 3방향 라우팅에서 슬래시 커맨드가 가장 먼저 체크되는 이유는?

  • A) 슬래시 커맨드는 API 호출이 필요 없으므로 빠르게 처리할 수 있고, / 접두사로 명확하게 식별 가능하기 때문에
  • B) 슬래시 커맨드가 가장 자주 사용되기 때문에
  • C) ULTRAPLAN 체크가 슬래시 커맨드와 충돌하기 때문에
  • D) 알파벳 순서상 /가 가장 앞에 오기 때문에
슬래시 커맨드는 / 접두사로 명확하게 식별 가능하며, 대부분 로컬에서 즉시 처리됩니다(API 호출 불필요). 가장 먼저 체크하면 불필요한 ULTRAPLAN 키워드 스캔이나 메시지 구성을 건너뛸 수 있습니다.

Q3. 합성(synthetic) 메시지의 역할은?

  • A) 사용자의 이전 대화를 요약하여 표시
  • B) API 오류를 시뮬레이션하여 테스트
  • C) 다른 사용자의 메시지를 가져와 컨텍스트 공유
  • D) 사용자가 입력하지 않은 컨텍스트(세션 메모리, git 상태 등)를 자동으로 주입하여 Claude의 이해를 강화
합성 메시지는 시스템이 자동으로 생성하여 대화에 주입하는 컨텍스트입니다. 세션 메모리, 최근 편집 파일, git 상태 등이 포함되어 Claude가 프로젝트의 현재 상태를 이해할 수 있게 합니다.

Q4. normalizeMessagesForAPI가 다중 패스로 정규화하는 이유는?

  • A) 성능 최적화를 위해 병렬 처리하기 위해
  • B) 각 패스가 서로 다른 API 버전을 대상으로 하기 때문에
  • C) 각 변환을 독립적으로 적용하여, 한 변환이 다른 변환의 제약을 위반하는 상호 의존성을 단순화하기 위해
  • D) 에러 복구를 위해 각 패스를 개별적으로 롤백할 수 있도록
합성 메시지 삽입이 연속 동일 역할 제약을 위반할 수 있듯, 변환 간 상호 의존성이 존재합니다. 다중 패스는 각 변환을 순서대로 독립적으로 적용하여 이런 복잡성을 관리합니다.

Q5. 컨텍스트 윈도우 초과 시 가장 먼저 압축되는 메시지 타입은?

  • A) user_text — 사용자 입력은 요약 가능하므로
  • B) synthetic — 시스템이 자동 생성한 컨텍스트 메시지이므로 우선 압축 대상
  • C) tool_result — 도구 결과는 대부분 중복 정보이므로
  • D) assistant_text — 이전 응답은 현재 질문과 관련이 적으므로
합성 메시지(synthetic)는 사용자가 직접 입력한 것이 아닌 시스템이 자동 생성한 컨텍스트입니다. 컨텍스트 윈도우가 부족할 때 사용자의 실제 대화를 보존하면서 합성 메시지를 먼저 축소하는 것이 자연스럽습니다.
0 / 5