Next.js로 Claude 에이전트 만들 때 프론트엔드 개발자가 마주치는 세 가지 실전 마찰

Next.js로 Claude 에이전트 만들 때 프론트엔드 개발자가 마주치는 세 가지 실전 마찰

빠른 프로토타이핑이 끝나는 곳에서 시작되는 API 변경 대응, 상태 설계 실수, 과금 엣지케이스—세 마찰을 미리 알면 구조가 달라진다.

Claude API Tool Calling Next.js 에이전트 Adaptive Thinking MCP 과금 에이전트 루프 프론트엔드 AI 설계
광고

프로토타입은 하루면 뚝딱이다. Next.js 라우트 핸들러 하나에 Claude API 붙이고, Tool Calling 루프 짜고, 채팅 UI 연결하면 '에이전트'처럼 돌아가는 무언가가 금방 만들어진다. 그런데 그 순간부터가 진짜 시작이다. 실제 사용자가 붙고, 모델 버전이 올라가고, 과금 로직이 필요해지면 프로토타입에서 눈치채지 못했던 마찰 세 개가 동시에 터진다.

마찰 1: 에이전트 루프의 상태 설계 실수

dev.to에 올라온 AutoValue 사례는 이 문제를 정직하게 짚는다. Claude API는 무상태(stateless)다. 매 요청마다 전체 대화 히스토리를 다시 보내야 한다. 여기까지는 다들 안다. 그런데 실제로 에이전트 루프를 짜면서 놓치는 건 따로 있다.

첫 번째는 어시스턴트 응답을 저장할 때 텍스트만 뽑아서 넣는 실수다. response.content 배열에는 tool_use 블록과 thinking 블록이 섞여 있는데, 이걸 통째로 히스토리에 넣지 않고 텍스트만 추출해 저장하면 다음 턴에서 API가 400을 반환한다. tool_result가 참조하는 tool_use_id가 히스토리에 없어서다. 항상 response.content 전체를 history.push해야 한다.

두 번째는 병렬 툴 결과를 여러 개의 user 메시지로 나눠 보내는 실수다. 모델은 한 턴에 여러 툴을 동시에 요청할 수 있다. 각 결과를 별도 user 메시지로 보내면 role 교대 규칙이 깨지고, 더 나쁜 건 모델이 슬그머니 병렬화를 포기하기 시작한다는 것이다. 모든 tool_result는 하나의 user 메시지 배열에 담아야 한다.

세 번째가 가장 은밀한 함정이다. AutoValue가 6단계 플로우를 운영하면서 발견한 것인데, 1단계에서 수집한 데이터(차체 타입 같은 것)가 3단계 툴 호출의 인자로 신뢰성 있게 전달되지 않는다. 모델이 대화를 '기억'하는 것처럼 보여도, 구조화된 팩트를 재현하는 능력은 생각보다 훨씬 불안정하다. 모델은 대화를 이끌고, 코드가 팩트를 소유해야 한다. 단계 간에 전달해야 할 데이터는 서버 사이드 코드에서 직접 축적하고 툴 실행 시 주입해야 한다.

마찰 2: 모델 버전 올리는 순간 터지는 API 변경

프로토타입이 안정화되면 자연스럽게 더 나은 모델로 올리고 싶어진다. 그 순간 또 다른 마찰이 기다린다. dev.to의 또 다른 글은 budget_tokens 파라미터가 Opus 4.7 이상에서 완전히 사라진 경험을 다룬다. 입력 크기의 30%를 thinking 예산으로 주는 헬퍼를 정성껏 만들었는데, 모델 버전 하나 올렸다고 400 에러와 함께 그 코드 전체가 무의미해졌다.

대체 방식은 thinking: { type: 'adaptive' }output_config: { effort: 'high' } 조합이다. budget_tokens가 '얼마나 생각할 수 있는가'를 제한했다면, effort는 '얼마나 생각하고 행동할 것인가'를 통합적으로 제어한다. 같은 축이 아니라서 1:1 매핑은 없다. 실무적으로는 분류·라우팅엔 low, 일반 앱 트래픽엔 medium~high, 코딩·에이전트 루프엔 xhigh가 기준점이 된다.

흥미로운 역설도 있다. 멀티스텝 에이전트 작업에서 effort를 높이면 총 토큰이 오히려 줄어드는 경우가 생긴다. 초반에 더 잘 계획하니 루프 횟수가 줄어서다. '높은 effort = 무조건 비싸다'는 직관이 항상 맞지 않는다. 또 하나 놓치기 쉬운 것: 4.7 이상에서 thinking 블록은 스트리밍 되지만 텍스트가 기본적으로 비어 있다. UI에서 추론 과정을 보여주려면 thinking: { type: 'adaptive', display: 'summarized' }를 명시해야 한다. 이걸 모르면 스트리밍이 멈춘 것처럼 보인다.

마찰 3: 과금 로직의 엣지케이스는 해피패스에 없다

에이전트가 외부 API를 호출하거나 사용량 기반 과금을 도입하는 순간 세 번째 마찰이 등장한다. MCP 서버 개발 경험을 공유한 dev.to 글은 Stripe 공식 문서가 다루지 않는 세 가지 시나리오를 정면으로 다룬다.

재시도 중복 과금: 클라이언트가 네트워크 단절로 재시도하면 논리적으로 하나인 요청이 두 번 처리될 수 있다. SELECT-before-INSERT로 막으려 하면 동시 재시도 레이스 컨디션에 뚫린다. DB 유니크 제약으로 idempotencyKey를 걸어야 애플리케이션 레이어가 아닌 데이터베이스 레이어에서 막힌다.

스트리밍 중단 과금 기준: 3,000 토큰 스트리밍 중에 사용자가 탭을 닫으면 어떻게 할 것인가? 정답은 없다. 중요한 건 명시적으로 결정하고 문서화하는 것이다. 이 글의 저자는 completed/partial/failed 세 상태를 만들고, Postgres엔 모두 기록하되 Stripe엔 BILLABLE_STATUSES 설정으로 어떤 상태를 청구할지 코드 레벨에서 선언했다.

실패 유형 구분: 서버 예외로 실패한 호출과 사용자 입력이 잘못돼 400을 반환한 호출은 다르게 취급해야 할 수 있다. 후자는 실제 컴퓨팅이 발생했으니까. 이 역시 failed 상태를 Postgres엔 기록(감사 로그)하되 Stripe 청구 여부는 설정으로 분리하는 방식이 유연하다.

시사점: '빠른 프로토타이핑'의 기술 부채는 운영 첫날 청구된다

세 마찰의 공통점은 프로토타입 단계에서 보이지 않는다는 것이다. 로컬에서 혼자 테스트하면 재시도 없고, 모델 버전 안 바꾸고, 병렬 툴 호출도 잘 안 생긴다. 하지만 실제 사용자가 붙으면 세 가지 모두 '정상 운영 조건'이 된다.

프론트엔드 개발자 관점에서 이 흐름은 명확하다. 에이전트 UX의 품질은 대화 디자인보다 상태 소유권 설계에 달려 있다. 모델이 기억한다고 믿고 팩트 관리를 위임하면 반드시 깨진다. API 변경은 벤더가 '인시덴털한 파라미터'를 정리할 때 예고 없이 온다—추상화 레이어를 쌓기 전에 그 파라미터가 근본적인 개념인지 구현 세부인지 먼저 파악해야 한다. 과금 엣지케이스는 해피패스 문서엔 없으니 설계 단계에서 세 가지 상태(완료/부분/실패)와 청구 기준을 명시적으로 결정해야 한다.

빠른 프로토타이핑은 여전히 옳다. 단, 이 세 마찰을 미리 알고 시작하면 프로토타입과 프로덕션 사이의 거리가 훨씬 짧아진다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요