Skip to main content
Summary

테스트 속도가 개발 속도를 결정한다

Apps SDK 앱을 만들 때 가장 느린 테스트 방식은 모든 변경을 ChatGPT 대화에서만 확인하는 것이다. 프롬프트를 입력하고, 모델이 도구를 선택하길 기다리고, MCP 서버가 응답하고, 위젯이 렌더링될 때까지 기다리면 작은 CSS 수정 하나도 오래 걸린다.

그래서 테스트 환경은 두 층으로 나누어야 한다.

첫째, ChatGPT 실제 루프 테스트다. 개발자 모드에서 커넥터를 등록하고, 모델이 실제로 도구를 발견하고 호출하는지 확인한다. 이 테스트는 최종 동작을 검증하는 데 필요하다.

둘째, 로컬 하네스 테스트다. 위젯을 ChatGPT 밖에서 독립적으로 렌더링하고, 도구 응답 fixture를 주입해 화면 상태를 빠르게 확인한다. 이 테스트는 개발 중 대부분의 반복을 담당한다.

핵심은 ChatGPT 전체 루프를 없애는 것이 아니라, 매번 기다리지 않는 것이다. 실제 루프는 통합 확인에 쓰고, UI와 데이터 계약 검증은 로컬에서 빠르게 끝내야 한다.

로컬 테스트와 디버깅 전략

개발자 모드에서 실제 연결 확인하기

ChatGPT 안에서 앱을 테스트하려면 개발자 모드를 켜고, Settings > Connectors에서 로컬 MCP 서버를 커넥터로 등록한다. 로컬 서버는 ChatGPT가 직접 접근할 수 없으므로 보통 ngrok 같은 터널 도구로 공개 HTTPS URL을 만든 뒤, 해당 URL의 MCP 엔드포인트를 등록한다. 공식 예제 저장소도 로컬 MCP 서버를 실행한 뒤 ngrok http 8000으로 공개 URL을 만들고, 그 URL을 ChatGPT Connectors에 추가하는 흐름을 안내한다1.

개발자 모드에서 반드시 확인할 것은 세 가지다.

  • 도구 목록이 최신 코드와 일치하는가

  • 입력 JSON Schema가 의도한 필드만 요구하는가

  • 도구 결과의 structuredContent, _meta, openai/outputTemplate이 실제 위젯 렌더링으로 이어지는가

Apps SDK에서는 도구 선언과 위젯 템플릿이 모델의 호출 판단과 클라이언트 렌더링에 모두 영향을 준다. 따라서 도구 이름이나 설명, 입력 스키마, 리소스 URI를 바꾼 뒤에는 커넥터 새로고침 또는 재등록으로 변경 사항이 반영되었는지 확인해야 한다. 서버만 재시작하고 ChatGPT 쪽 캐시나 등록 정보를 그대로 두면, 코드와 실제 호출 흐름이 서로 다른 상태가 될 수 있다.

특히 다음 변경은 재확인이 필요하다.

변경 내용확인할 것
도구 이름·설명 수정모델이 여전히 올바른 상황에서 도구를 선택하는가
입력 스키마 수정필수값, enum, 기본값이 의도대로 전달되는가
출력 구조 수정structuredContent가 위젯과 모델 양쪽에서 해석 가능한가
위젯 번들 재빌드_meta["openai/outputTemplate"] URI가 새 HTML·JS·CSS를 가리키는가
정적 자산 경로 수정ChatGPT 클라이언트에서 리소스를 불러올 수 있는가

로컬 개발 루프 구성하기

공식 예제의 로컬 루프는 대략 다음 순서로 구성된다. 의존성을 설치하고, 위젯을 빌드하고, 정적 자산 서버를 띄운 다음, MCP 서버를 실행하고, 터널 URL을 ChatGPT에 등록한다1.

pnpm install
pnpm run build
pnpm run serve

# 다른 터미널
uvicorn your_mcp_server.main:app --port 8000

# 다른 터미널
ngrok http 8000

이 구조에서 각 프로세스의 역할을 분리해서 이해해야 한다.

  • 위젯 빌드: React, CSS, 이미지 등을 ChatGPT가 읽을 수 있는 정적 번들로 만든다.

  • 정적 자산 서버: 번들 HTML, JS, CSS를 제공한다.

  • MCP 서버: 도구 목록, 도구 실행, 위젯 리소스 메타데이터를 제공한다.

  • 터널: 로컬 MCP 서버를 ChatGPT가 접근 가능한 HTTPS 주소로 노출한다.

로컬 터널은 빠른 실험에는 좋지만, 디버깅 변수가 된다. 무료 터널은 세션이 끊기면 URL이 바뀔 수 있다. URL이 바뀌면 ChatGPT 커넥터도 다시 등록해야 한다. 또한 HTTPS 요구사항, 터널 지연, DNS rebinding 보호, 방화벽, CORS 설정이 모두 실패 원인이 될 수 있다. 예제 저장소는 Python MCP 서버를 터널로 노출할 때 MCP_ALLOWED_HOSTS, MCP_ALLOWED_ORIGINS에 터널 호스트를 허용하라고 안내한다1.

터널을 쓸 때는 다음 값을 .env.local이나 개발용 설정 파일에 명시적으로 기록해 두는 편이 좋다.

APP_BASE_URL=https://your-tunnel.ngrok-free.app
MCP_SERVER_PORT=8000
STATIC_ASSET_ORIGIN=http://localhost:4444
LOG_LEVEL=debug

이렇게 하면 "방금 본 화면이 어느 서버와 어느 번들에서 온 것인지"를 추적하기 쉬워진다.

무엇을 테스트할 것인가

Apps SDK 통합 테스트의 초점은 넓게 잡으면 흐려진다. 공식 테스트 가이드는 커넥터를 사용자에게 노출하기 전에 도구 정확성, 컴포넌트 UX, 발견 정확도를 검증하라고 설명한다2. 이 세 가지를 기준으로 테스트 항목을 나누면 실수가 줄어든다.

도구 정확성은 도구가 올바른 입력을 받고 올바른 결과를 반환하는지 보는 것이다. 입력 스키마에 없는 값이 들어왔을 때 거부하는지, 필수값이 빠졌을 때 명확한 오류를 내는지, 외부 API 실패를 사용자에게 이해 가능한 메시지로 바꾸는지 확인한다.

컴포넌트 UX는 위젯이 도구 결과를 안정적으로 표현하는지 보는 것이다. structuredContent가 비어 있을 때, 항목이 하나일 때, 수백 개일 때, 이미지 URL이 깨졌을 때, 로딩이 길어질 때 화면이 무너지지 않아야 한다.

발견 정확도는 모델이 적절한 순간에 적절한 도구를 호출하는지 보는 것이다. 도구 설명이 너무 모호하면 모델이 호출을 주저한다. 반대로 설명이 과장되어 있으면 관련 없는 상황에서도 호출될 수 있다. 도구 이름, 설명, read-only 여부, 입력 필드 설명은 모두 모델을 위한 UX다.

테스트 프롬프트도 도구별로 fixture처럼 관리하는 것이 좋다.

[
  {
    "name": "정상 호출",
    "prompt": "이번 주 서울의 전시 일정을 찾아줘",
    "expectedTool": "search_events"
  },
  {
    "name": "도구 미호출",
    "prompt": "전시회란 무엇인지 설명해줘",
    "expectedTool": null
  },
  {
    "name": "경계값",
    "prompt": "1900년 1월의 이벤트를 찾아줘",
    "expectedTool": "search_events"
  }
]

이런 목록을 만들어 두면 도구 설명을 바꾼 뒤 발견 정확도가 좋아졌는지 나빠졌는지 비교할 수 있다.

위젯은 로컬 하네스에서 먼저 깨뜨려라

위젯 UI는 ChatGPT 안에서만 개발하면 속도가 너무 느리다. 더 나은 방법은 위젯을 일반 웹 앱처럼 독립 렌더링하는 로컬 하네스를 만드는 것이다. 현장 사례에서도 ChatGPT 전체 루프를 거치지 않고 structuredContent fixture와 window.openai mock을 주입하는 하네스가 위젯 개발 속도를 크게 높인다고 설명한다3.

하네스의 목표는 ChatGPT를 완벽히 복제하는 것이 아니다. 위젯이 의존하는 최소 런타임만 흉내 내면 된다. 실제 호스트 API의 세부 반환값은 SDK 버전과 실행 환경에 따라 달라질 수 있으므로, mock은 "내 위젯이 의존하는 계약"을 명확히 보여 주는 용도로 유지한다.

const mockWidgetState = {
  sort: "recommended",
  selectedId: null
};

window.openai = {
  toolInput: mockToolInput,
  toolOutput: mockToolOutput.structuredContent ?? mockToolOutput,
  displayMode: "inline",
  theme: "light",
  widgetState: mockWidgetState,
  setWidgetState: async (state) => {
    Object.assign(mockWidgetState, state);
  },
  callTool: async (name, args) => mockCallTool(name, args),
  requestDisplayMode: async ({ mode }) => ({ mode }),
  openExternal: async ({ href }) => window.open(href, "_blank"),
  sendFollowUpMessage: async ({ message }) => {
    console.log("follow-up message", message);
  }
};

핵심은 위젯이 읽는 toolInput, toolOutput, displayMode, theme, widgetState를 모두 주입하고, setWidgetState, callTool, requestDisplayMode 같은 동작을 최소한으로 흉내 내는 것이다. 실제 ChatGPT 호스트는 표시 모드 요청을 거절하거나 다른 모드로 조정할 수 있으므로, 하네스에서도 필요하면 requestDisplayMode가 { mode: "inline" }을 반환하는 실패 fixture를 따로 만들어 두면 좋다.

그다음 실제 도구 응답과 같은 모양의 fixture를 준비한다.

{
  "structuredContent": {
    "items": [
      { "id": "evt_1", "title": "디자인 전시", "date": "2026-06-01" }
    ]
  },
  "_meta": {
    "widgetSessionId": "dev-session-1"
  }
}

fixture는 최소한 다음 상태를 포함해야 한다.

  • 정상 데이터

  • 빈 데이터

  • 부분적으로 누락된 데이터

  • 오류 응답

  • 느린 응답

  • 매우 긴 목록

  • 권한이 없는 상태

  • 이전 widgetState가 복원된 상태

이렇게 하면 버튼 클릭, 필터 변경, 상태 저장, 재조회, 오류 배너 같은 UI 흐름을 ChatGPT 없이 반복 검증할 수 있다. 실제 ChatGPT 테스트는 "하네스에서 통과한 위젯이 호스트 안에서도 같은 계약으로 동작하는지" 확인하는 마지막 단계가 된다.

구조화 로그로 원인을 좁히기

Apps SDK 디버깅에서 가장 힘든 순간은 원인이 서버인지, 위젯인지, ChatGPT 클라이언트인지 모를 때다. 공식 문제 해결 문서도 문제가 생기면 먼저 서버, 컴포넌트, ChatGPT 클라이언트 중 어느 계층의 문제인지 분리하라고 안내한다4.

이를 위해 로그는 문자열 한 줄이 아니라 구조화된 이벤트로 남기는 것이 좋다.

{
  "event": "tool.finish",
  "tool": "search_events",
  "requestId": "req_123",
  "durationMs": 842,
  "inputKeys": ["city", "dateRange"],
  "structuredContentKeys": ["items"],
  "hasMeta": true,
  "outputTemplate": "ui://widgets/events.html"
}

최소한 다음 이벤트는 남겨야 한다.

  • tool.start: 어떤 도구가 어떤 입력 키로 시작했는가

  • tool.finish: 실행 시간, 결과 개수, 출력 키는 무엇인가

  • tool.error: 외부 API 오류인지, 검증 오류인지, 내부 오류인지

  • widget.load: 어떤 toolOutput과 _meta를 받아 렌더링했는가

  • widget.action: 사용자가 어떤 버튼이나 입력을 실행했는가

  • widget.callTool: 위젯에서 다시 호출한 도구와 인자는 무엇인가

  • asset.load: HTML, JS, CSS, 이미지가 정상 로드되었는가

원인이 모호한 424 계열 실패나 렌더링 실패는 대개 계약 불일치에서 시작된다. 예를 들어 structuredContent가 위젯이 기대하는 객체가 아니거나, _meta["openai/outputTemplate"]가 가리키는 리소스 URI가 등록되지 않았거나, 실제 정적 자산 경로가 달라졌을 수 있다. 커뮤니티 사례에서도 424 오류는 불친절하게 보이지만, 실제로는 응답 shape나 메타데이터 불일치에서 자주 비롯된다고 보고한다3.

디버깅 순서는 단순하게 가져가자.

  1. MCP 서버가 도구 목록을 올바르게 내보내는지 확인한다.

  2. 도구 입력이 JSON Schema와 일치하는지 확인한다.

  3. 도구 출력의 structuredContent와 _meta를 로그로 저장한다.

  4. _meta["openai/outputTemplate"] URI가 실제 접근 가능한 HTML 리소스를 가리키는지 확인한다.

  5. 위젯이 받은 초기 toolInput, toolOutput, widgetState를 콘솔에 출력한다.

  6. 외부 API 시간이 긴지, ChatGPT 재호출이 많은지 타임스탬프로 나눈다.

로그에는 민감 정보를 남기지 않는다. 액세스 토큰, 세션 쿠키, 이메일, 결제 정보, 원문 대화 전체는 마스킹하거나 저장하지 않는다. 디버깅을 위해 남기는 값은 "문제를 재현하고 계약을 확인하는 데 필요한 최소 정보"여야 한다.

빠른 검증을 위한 개발 습관

로컬 테스트 전략은 거창한 테스트 프레임워크보다 습관에 가깝다. 도구마다 정상 fixture와 오류 fixture를 만든다. 위젯은 ChatGPT 밖에서 먼저 렌더링한다. 커넥터를 재등록해야 하는 변경과 서버 재시작만으로 충분한 변경을 구분한다. 그리고 모든 도구 호출에 요청 ID와 실행 시간을 붙인다.

Apps SDK 앱은 모델, 서버, 위젯, 외부 API가 함께 움직인다. 문제가 생겼을 때 "ChatGPT가 이상하다"라고 뭉뚱그리면 오래 걸린다. 반대로 입력, 출력, 메타데이터, 자산, 렌더링 시점을 나누어 보면 대부분의 문제는 좁은 영역으로 줄어든다.

개발 생산성은 결국 기다림을 줄이는 데서 갈린다. ChatGPT 전체 루프는 꼭 필요할 때만 사용하고, 나머지는 로컬 하네스와 fixture, 구조화 로그로 빠르게 검증하자.

참고

2Test your integration - Apps SDK

3Building with the OpenAI Apps SDK: A Field Guide

4Troubleshooting - Apps SDK

책 목록

ChatGPT 앱을 만드는 법: OpenAI Apps SDK 실전 가이드

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기