Claude의 도구 호출(tool-calling) 루프는 Node.js에서 복잡하게 얽힌 Promise 코드를 생성한다는 평이 있습니다. Node.js 22의 새로운 Promise.withResolvers()를 사용하면 개발자는 번거로운 new Promise 패턴을 Promise와 resolve/reject 함수를 한 번에 제공하는 단 한 줄의 코드로 대체할 수 있습니다. 그 결과, resolve 호출을 누락하는 일이 줄어들고, 중복 reject 경고가 발생하지 않으며, 제어 흐름이 평탄해져 서버리스 환경에서 테스트와 상태 유지가 더 쉬워집니다.

기존 패턴이 흐름을 끊는 이유

Claude와 같은 LLM이 도구를 요청할 때, 일반적인 Node 구현은 다음과 같습니다.

return new Promise((resolve, reject) => {
  // launch the tool, attach callbacks, maybe fire another async call
});

세 가지 반복적인 함정이 발생합니다.

  • resolve 누락 – 코드 경로에서 resolve를 호출하지 않으면, Lambda 또는 기타 서버리스 핸들러가 타임아웃이 발생할 때까지 대기 상태로 머물러 비용이 상승합니다.
  • 중복 reject – reject를 두 번 호출하는 에러 경로는 strict mode에서 프로세스를 중단시킬 수 있는 “unhandled rejection” 경고를 발생시킵니다.
  • 깊은 중첩 – 각 비동기 단계가 생성자 내부에 또 다른 콜백을 중첩시켜 로직을 분산시키고 단위 테스트를 까다롭게 만듭니다.

이러한 모든 문제는 Promise의 제어 함수가 생성자의 클로저(closure) 안에 갇혀 있어, 나머지 코드가 이를 다시 참조해야만 한다는 사실에서 비롯됩니다.

한 줄로 끝내는 Promise.withResolvers()

Node 22는 Promise와 이를 완료(settle)시키는 두 함수를 포함하는 객체를 반환하는 정적 헬퍼를 추가했습니다.

const { promise, resolve, reject } = Promise.withResolvers();

이제 Promise를 HTTP 핸들러, 데이터베이스 리스너 또는 백그라운드 워커와 같은 시스템의 어느 부분으로든 전달할 수 있으며, 원래 호출자는 단순히 Promise를 await하기만 하면 됩니다. 도구 실행 블록 전체를 new Promise 생성자로 감쌀 필요가 없습니다.

Claude의 도구 루프에 적용하기

Claude의 워크플로우는 다음과 같습니다.

  1. LLM이 도구 요청을 생성합니다.
  2. 코드가 도구를 실행합니다(예: API 호출, 파일 읽기).
  3. 도구 결과가 다음 턴을 위해 Claude에게 다시 전송됩니다.

withResolvers를 사용하면 루프가 다음과 같이 단순화됩니다.

async function runTool(request) {
  const { promise, resolve, reject } = Promise.withResolvers();

  // Kick off the tool; it can call resolve/reject from anywhere
  executeTool(request, { resolve, reject });

  // Optional timeout wrapper
  const timeout = setTimeout(() => reject(new Error('Tool timed out')), 10_000);
  try {
    const result = await promise;
    clearTimeout(timeout);
    return result;               // feed back to Claude
  } finally {
    // clean-up if needed
  }
}

도구 구현을 더 이상 새로운 Promise로 감쌀 필요가 없으며, 단순히 resolve와 reject를 받기만 하면 됩니다. 이를 통해 위에서 언급한 세 가지 실패 모드를 제거할 수 있습니다.

실전에서의 주요 고려 사항

Promise 구조가 깔끔해지더라도, 실제 환경의 에이전트는 다른 제약 사항에 직면합니다.

  • 타임아웃 – 위의 스니펫은 도구가 임계값을 초과하면 reject하는 간단한 타이머를 보여줍니다. SLA 기대치에 따라 지속 시간을 조정하십시오.
  • 스로틀링(Throttling) – 기반 서비스가 스로틀링 에러(예: Bedrock의 ThrottlingException)를 반환하면, 이를 캐치하여 일시 중지한 후 지수 백오프(exponential back-off)를 적용하여 재시도하십시오. resolve/reject 쌍은 동일하며 재시도 로직만 변경됩니다.
  • Lambda 비용 – AWS Lambda에서는 callbackWaitsForEmptyEventLoop = false로 설정하십시오. 이는 스트림이나 기타 백그라운드 핸들이 여전히 열려 있더라도 핸들러가 반환되는 즉시 런타임이 함수를 종료하도록 합니다. 이를 통해 Promise가 다른 곳에서 완료되는 동안 함수가 계속 남아 있는 것을 방지할 수 있습니다.

새로운 헬퍼가 만능 해결책은 아닌 경우

Promise.withResolvers()는 Node 22 이상에서만 사용할 수 있습니다. 이전 LTS 버전에 고정된 프로젝트는 이 패턴을 폴리필(polyfill)하거나 기존의 생성자를 계속 사용해야 합니다. 폴리필은 API를 모방할 수는 있지만 네이티브 성능 이점은 얻을 수 없습니다. 또한, 이 헬퍼가 논리적 버그를 마법처럼 해결해 주지는 않습니다. 개발자는 모든 요청에 대해 resolve 또는 reject 중 정확히 하나만 호출되도록 보장해야 하며, 그렇지 않으면 Promise는 무기한 대기(pending) 상태로 남게 됩니다.

향후 주목할 점

  • 프레임워크 채택 – LLM 에이전트 루프를 추상화하는 라이브러리(예: 오픈 소스 Claude 래퍼)들이 withResolvers를 선택적 기능(opt-in feature)으로 노출하기 시작했습니다. 이 패턴이 기본값으로 설정되는 업데이트를 주시하십시오.
  • Node 생태계 – 더 많은 서비스가 Node 22로 전환됨에 따라, 이 헬퍼는 LLM 에이전트뿐만 아니라 모든 “fire-and-wait” 비동기 패턴의 사실상 표준(de-facto standard)이 될 것입니다.
  • 도구 호출 표준 – 새롭게 등장하는 LLM 도구 호출 사양은 “단일 Promise” 계약을 규정할 수 있으며, 이는 withResolvers 방식과 완벽하게 일치합니다.

요약: 런타임이 Node 22를 지원한다면, 장황한 new Promise 래퍼를 한 줄짜리 Promise.withResolvers()로 교체함으로써 Claude 기반 에이전트는 더 명확한 흐름, 적은 런타임 오류, 그리고 서버리스 비용에 대한 더 정밀한 제어력을 얻을 수 있습니다.