ChatGPT에서 요기요 배달 쓰기 — MCP + 위젯 연동 개발기

2 weeks ago 11
ChatGPT Apps SDK와 MCP(Model Context Protocol)로 요기요 배달 서비스를 연동한 프로젝트의 기술적인 설계와 구현, 그리고 개발 과정에서 얻은 인사이트를 공유합니다.

1. 왜 ChatGPT App인가

약 1년 전쯤, 한 경제 유튜버가 “곧 모든 앱이 AI로 대체되는 시대가 온다”며 배달 앱을 예로 든 적이 있었습니다. 배달 앱을 열어서 카테고리를 누르고, 가게를 고르고, 메뉴를 스크롤 하는 대신 — AI에게 “오늘 매운 거 먹고 싶어”라고 말하면 메뉴 추천부터 주문까지 한 번에 처리해 주는 세상이 올 거라는 이야기였습니다. 배달 플랫폼을 개발하는 입장에서 그 영상을 보면서 든 생각은 솔직히 회의적이었습니다. 수만 개 레스토랑의 메뉴 정보, 실시간 배달 가능 여부, 라이더 매칭, 결제 처리 — 이 모든 걸 AI가 어떻게 다 처리한다는 거지? 배달 플랫폼이 직접 데이터와 기능을 열어주지 않는 한 불가능할 텐데 라고 생각했습니다.

그런데 2025년 12월, ChatGPT Apps가 출시되면서 그 “불가능”이 현실이 되는 구조가 만들어졌습니다. OpenAI가 제공하는 MCP(Model Context Protocol)Apps SDK를 통해, 외부 서비스가 자신의 데이터와 기능을 ChatGPT에 연결할 수 있는 표준 인터페이스가 열린 것입니다. AI가 혼자서 배달 서비스를 만드는 게 아니라, 배달 플랫폼이 MCP 서버를 통해 도구(Tool)를 제공하면 ChatGPT가 대화 맥락에 맞춰 그 도구를 호출하는 구조 — 결국 플랫폼이 정보를 열어야 가능하다는 그때의 생각이 맞았고, MCP가 바로 그 “여는 방법”을 표준화한 프로토콜이었습니다.

여기서 주목할 점은 MCP 자체와 ChatGPT App의 차이입니다. 기존 MCP는 REST API처럼 AI가 외부 시스템의 기능을 호출할 수 있도록 Tool을 확장하는 프로토콜입니다. AI가 Tool을 호출하면 JSON 형태의 데이터를 받아서 텍스트로 요약해 응답하는 구조 — 결국 사용자가 보는 것은 텍스트뿐이었습니다. 그런데 ChatGPT Apps SDK는 여기에 위젯(Widget) 이라는 레이어를 추가했습니다. MCP 서버가 Tool 응답에 _meta.outputTemplate이라는 메타데이터를 포함하면, ChatGPT가 단순 텍스트 대신 HTML 기반의 인터랙티브 UI를 대화 안에 인라인으로 렌더링합니다. 사용자는 이 위젯 안에서 지도를 드래그하고, 가게를 클릭하고, 메뉴를 스크롤 하고, 주문 버튼을 누를 수 있습니다. 텍스트 응답을 읽는 것이 아니라 서비스와 직접 상호작용하는 경험이 가능해진 것입니다.

이제 사용자는 “강남역 근처 치킨 배달 가능한 곳 알려줘”라고 대화하는 것만으로, ChatGPT가 요기요의 레스토랑 검색 Tool을 호출하고, 그 결과를 가게 리스트·상세 정보·메뉴·리뷰·지도 같은 위젯으로 바로 보여줄 수 있게 됩니다. 기존 요기요 앱/웹의 전체 기능을 그대로 옮기는 것이 아니라, 대화 흐름 속에서 필요한 데이터만 Tool로 가져오고, 그에 맞는 UI를 위젯으로 렌더링하는 구조 — 이것이 이 프로젝트가 만들고자 한 경험이었습니다.

2. 개발 환경 세팅

ChatGPT App을 개발하고 테스트하려면 먼저 몇 가지 환경이 갖춰져야 합니다.

  • ChatGPT 워크스페이스 또는 Pro 계정: 개발 앱을 등록하고 테스트하려면 ChatGPT 개발자 워크스페이스 접근 권한이 있는 계정이 필요합니다. 무료 플랜에서는 개발 앱 등록이 불가능합니다.
  • 퍼블릭 도메인 또는 터널링 서비스: ChatGPT가 MCP 서버에 접속해야 하므로, 외부에서 접근 가능한 HTTPS 엔드포인트가 필요합니다. 프로덕션이라면 도메인과 SSL 인증서를, 로컬 개발이라면 ngrok 같은 터널링 서비스를 사용하여 로컬 서버를 외부에 노출합니다.
Registering a Dev version app to the workspace
  • 개발 앱의 Tool 갱신: 개발 앱으로 등록된 상태에서는 MCP 서버의 Tool 정의를 수정한 뒤, ChatGPT 대화창에서 새로고침만 하면 변경된 Tool 목록이 즉시 반영됩니다. 앱을 다시 등록하거나 재배포할 필요 없이 Tool 스키마를 빠르게 반복 테스트할 수 있어 개발 사이클이 매우 짧습니다.
Refresh MCP in Dev mode

3. 개발 타임라인

2025년 12월 초 ChatGPT Apps 서비스의 공개가 예정되어 있었기 때문에, 한정된 일정 안에 프로덕션 수준의 MCP 서버와 위젯을 완성해야 하는 상황이었습니다. 처음부터 모든 것을 구축하기보다는 OpenAI가 공개한 Apps SDK Examples 레포지토리의 레퍼런스 구현체(Pizzaz/ecommerce 예제)를 기반으로 빠르게 프로토타이핑한 뒤, 요기요 배달 도메인에 맞춰 확장해 나가는 전략을 택했습니다. 특히 프로토타이핑 단계에서는 AI 코딩 어시스턴트(Claude)를 적극 활용한 바이브 코딩(Vibe Coding) 방식으로 개발 속도를 끌어올렸습니다. 예제 코드의 구조를 파악하고, 요기요 API 연동 코드를 생성하고, 위젯 컴포넌트의 초기 스캐폴딩을 만드는 과정에서 AI와의 대화 기반 개발이 반복적인 보일러플레이트 작업을 크게 줄여주었습니다.

기술 스택 선정에서도 개발 속도를 우선시했습니다. OpenAI의 예제 레포는 MCP 서버를 Python(FastMCP)Node.js(@modelcontextprotocol/sdk) 두 가지 스택으로 제공하고 있었는데, 이번 프로젝트는 프론트엔드 개발자가 위젯(React/TypeScript)과 MCP 서버를 모두 담당하는 구조였기 때문에, 클라이언트와 서버 간 언어 컨텍스트 스위칭 없이 TypeScript 단일 스택으로 개발할 수 있는 Node.js 기반 MCP 서버를 선택했습니다. 이를 통해 위젯의 타입 정의(types.ts)를 서버와 공유하고, pnpm 워크스페이스로 모노레포를 구성하여 빌드·배포 파이프라인을 일원화할 수 있었습니다.

Phase 1 — 도메인 전환 & API 연동

OpenAI Apps SDK Examples의 pizzaz_server_node 구현체를 fork하여, 요기요 배달 서비스에 맞게 재구성하는 것으로 시작했습니다.

  • 기존 데모의 하드코딩된 mock 데이터를 제거하고, 요기요 내부 API와의 실제 연동으로 교체. Tool의 inputSchema를 배달 도메인에 맞춰 재설계하고, CallToolRequest 핸들러에서 내부 API를 호출하여 structuredContent로 응답하는 파이프라인을 구축했습니다.
  • 위젯 ID를 기능 단위(shop-list, shop-detail, map, reviews, menu-list)로 정리하고, 각 위젯별 outputTemplate URI(ui://widget/*.html) 매핑을 구성했습니다.

Phase 2 — 위젯 UI/UX 고도화

디자인팀과 협업하여 Figma 디자인 가이드를 위젯에 적용했습니다. OpenAI가 제공하는 App Design GuidelinesUI Guidelines를 기반으로, 인라인 카드·풀스크린 등 위젯 디스플레이 모드의 제약 사항을 반영하면서 요기요 브랜드 디자인을 적용했습니다.

  • 리뷰 위젯 신규 추가: 가게별 리뷰 목록을 렌더링하는 reviews.html 위젯과 대응하는 get-restaurant-review-list Tool을 구현했습니다.
  • Figma 시안 기반으로 전체 위젯 UI를 재설계: 가게 리스트(shop-list), 가게 상세(shop-detail), 메뉴 리스트(menu-list), 지도(map) 등의 레이아웃·타이포그래피·컬러 토큰을 Tailwind CSS 커스텀 테마로 정의하고, 반응형 브레이크포인트와 다크 모드 대응을 적용했습니다.
  • CTA 버튼 스타일, 요기요 앱 딥링크 스킴, 코드 포맷팅 등 세부 사항 정리.

Phase 3 — 아키텍처 분리: 위젯 Static Hosting vs MCP Server

초기에는 MCP 서버(Node.js)가 Tool 요청 처리와 위젯 정적 파일(HTML/JS/CSS/이미지) 서빙을 모두 담당하는 모놀리식 구조였습니다. 운영 환경을 구성하면서 이 두 책임을 분리하기로 결정했습니다.

분리 결정의 근거:

  • 관심사 분리(Separation of Concerns): MCP 서버는 tools/call 요청에 대한 비즈니스 로직 처리(API 호출, 데이터 가공, structuredContent 생성)에 집중하고, 위젯 렌더링에 필요한 정적 리소스(빌드된 JS 번들, CSS, 이미지 등)는 별도의 Static Hosting으로 분리하는 것이 각 계층의 역할을 명확히 합니다.
  • 배포 독립성: 위젯 UI만 수정된 경우 S3에 정적 파일만 재배포하면 되고, Tool 로직이 변경된 경우 MCP 서버만 재배포하면 됩니다. 서로의 배포 사이클이 독립적이므로 롤백과 핫픽스가 용이합니다.
  • 성능·확장성: 정적 파일을 S3 + CloudFront CDN을 통해 서빙하면 엣지 캐싱으로 레이턴시가 줄고, MCP 서버의 네트워크 대역폭과 이벤트 루프를 Tool 처리에만 할당할 수 있습니다.

이에 따라 MCP 서버 엔드포인트 URL과 위젯 정적 리소스(S3/CDN) URL을 환경변수로 분리하고, ReadResourceRequest 핸들러가 반환하는 위젯 HTML 내의 JS/CSS 참조 경로가 위젯 CDN 도메인을 바라보도록 빌드 파이프라인을 구성했습니다.

Phase 4 — CSP 정책·Tool 메타데이터·UX 개선

ChatGPT의 sandbox iframe 환경에서 위젯이 외부 리소스에 접근하려면, MCP 서버가 _meta 응답에 허용 도메인을 명시적으로 선언해야 합니다.

  • CSP(Content Security Policy) 프로덕션 정리: connect_domains, resource_domains, script_domains에서 localhost를 제거하고, 프로덕션 도메인(*.yogiyo.co.kr, S3 오리진, Mapbox API 등)만 화이트리스트에 등록했습니다.
  • widgetDomain 메타 추가: ChatGPT UI 상단에 표시되는 브랜드 링크용 도메인을 pm2 ecosystem config로 관리.
  • suggestedNextTools 적용: Tool 응답에 다음 호출 후보를 포함시켜, ChatGPT가 맥락에 맞는 후속 Tool을 자동 제안하도록 구성.
  • Mapbox 지도 위젯에 사용자 현재 위치 마커 추가, 필터 적용 시 스켈레톤 UI 도입.

Phase 5 — Observability 구축 & 크로스 플랫폼 대응

프로덕션 운영을 위한 모니터링 체계를 구축하고, iOS/다크 모드 등 다양한 클라이언트 환경에 대응했습니다.

  • Datadog APM 연동: dd-trace를 MCP 서버에 통합하고, SSE 연결(mcp.sse_connection)과 개별 메시지 전송(mcp.sse_send), POST 요청(mcp.post_message)에 대한 커스텀 스팬을 추가하여 요청별 트레이스를 수집했습니다.
  • 빌드 파일 해시: 위젯 번들에 content hash를 적용하여 CDN 캐시 무효화와 버전 추적이 가능하도록 구성.
  • iOS 폰트 대응: SF Pro 폰트 패밀리를 플랫폼별로 분기 적용. 다크 모드 렌더링 이슈 수정.
  • OpenAI Apps 도메인 검증: .well-known/openai-apps-challenge 엔드포인트 추가로 앱 소유권 인증 처리.
  • Tool annotations 설정: readOnlyHint, destructiveHint, openWorldHint 등을 명시하여 ChatGPT가 승인 프롬프트 없이 Tool을 호출할 수 있도록 구성.

Phase 6 — 안정화 & 유지보수

  • Datadog 트레이서 초기화 수정: dd-trace import 순서 이슈로 인한 트레이싱 누락 버그를 수정. ESM 환경에서 tracer가 다른 모듈보다 먼저 로드되도록 import 순서를 보장했습니다.

4. 전체 아키텍처

Service Architecture

MCP 서버 (Node.js / TypeScript)

  • 역할: 툴 목록 제공(tools/list), 툴 호출 처리(tools/call), 위젯용 HTML/리소스 정보 제공, CSP 메타데이터 선언.
  • 전송: SSE(Server-Sent Events)로 ChatGPT와 장기 연결 유지.
  • 실제 데이터: 요기요 내부 API를 호출해 위치·레스토랑·메뉴·리뷰·지도 데이터를 가져옵니다.

위젯 (React + Vite)

  • 리스트·상세·메뉴·지도·리뷰 등 화면을 독립된 HTML/JS/CSS 번들로 빌드.
  • ChatGPT 쪽에서는 iframe + web sandbox 안에서 이 번들을 로드해 렌더링합니다.
  • 위젯 ↔ ChatGPT: window.openai.callTool() 등 Apps SDK API로 통신.

배포

  • 위젯 정적 파일: S3 + CloudFront CDN으로 서빙.
  • MCP 서버: EC2에서 실행, ALB를 통해 HTTPS로 노출.

5. MCP 서버와 툴 설계

MCP 서버는 다음 세 가지를 구현합니다.

  1. List tools — 사용 가능한 툴 목록과 입력 스키마, 그리고 어떤 위젯 HTML과 매핑할지 (_meta.openai/outputTemplate) 를 응답.
  2. Call tools — ChatGPT가 선택한 툴과 인자를 받아, 요기요 API를 호출한 뒤 구조화된 결과 + 위젯 메타데이터를 반환.
  3. Read resources — 위젯 HTML 템플릿을 제공. ChatGPT가 해당 위젯을 iframe으로 띄울 때 사용.

각 툴은 readOnlyHint: true 어노테이션으로 “읽기 전용 도구”임을 표시해, 불필요한 사용자 확인을 줄였습니다.

6. 위젯과 데이터 흐름

위젯은 ChatGPT의 sandbox iframe 안에서 동작합니다. 우리가 빌드한 HTML/JS/CSS가 connector_*.web-sandbox.oaiusercontent.com 같은 도메인의 iframe으로 로드되고, 그 안에서만 실행됩니다.

  • 데이터 수신: MCP 서버가 툴 결과와 함께 위젯 URL을 주면, ChatGPT가 해당 URL을 iframe으로 열고, tool output을 위젯에 전달합니다.
  • 상호작용: 위젯 안에서는 CSP 설정을 통해 외부 API를 직접 호출하는 것도 가능하지만, 대신 window.openai.callTool()을 사용했습니다. callTool을 거치면 요청이 ChatGPT를 통해 MCP 서버로 전달되기 때문에, ChatGPT가 사용자의 위젯 내 액션(어떤 Tool이 어떤 인자로 호출되었는지, 어떤 데이터가 반환되었는지)을 대화 맥락에 기록할 수 있습니다. 이를 통해 사용자가 “방금 본 가게 리뷰도 보여줘”처럼 후속 대화를 이어갈 때 맥락이 끊기지 않도록 했습니다.
  • 외부 이동: 결제 등 외부 페이지로 보낼 때는 window.openai.openExternal({ href: ‘…’ }) 를 사용해, iframe 밖 브라우저 탭으로 열도록 했습니다.

7. CSP: sandbox iframe 안에서의 보안 제약

ChatGPT Apps 위젯 개발에서 가장 많이 부딪힌 부분이 CSP(Content Security Policy)였습니다.

ChatGPT는 위젯을 sandbox iframe 안에서 실행하며, 기본적으로 모든 외부 네트워크 요청을 차단합니다. 우리 위젯처럼 외부 JS(Mapbox GL), 이미지 CDN, MCP 서버 호출이 필요한 경우, MCP 메타데이터의 openai/widgetCSP 필드에 허용 도메인을 명시적으로 선언해야 합니다.

"openai/widgetCSP": {
connect_domains: [mcpUrl, widgetUrl, "https://api.mapbox.com", ...], // fetch, callTool 등
resource_domains: [widgetUrl, "https://rev-static.yogiyo.co.kr", "data:", ...], // 이미지, CSS
script_domains: [widgetUrl, "https://api.mapbox.com", ...], // 외부 JS 로드
}

이 메타데이터에 없는 도메인으로의 요청은 브라우저가 조용히 차단합니다. 에러 팝업 없이 이미지가 안 뜨거나, 지도가 빈 화면이 되거나, API 호출이 실패합니다. 브라우저 콘솔의 Refused to… 로그가 유일한 단서이기 때문에, CSP 문제는 알고 있으면 5분, 모르면 반나절이 걸립니다.

개발 중 겪은 대표적인 CSP 이슈들:

  • 지도 빈 화면: Mapbox 타일 도메인(api.mapbox.com, events.mapbox.com)이 connect_domains에서 빠지면 지도가 회색으로만 렌더링 — 토큰 문제로 오인하기 쉬움
  • 이미지 깨짐: 음식점 썸네일 CDN이나 data: URI(Base64 마커 아이콘)를 resource_domains에 빠뜨리면 조용히 실패
  • data: URI 누락: Mapbox 마커나 커스텀 아이콘을 Base64로 인라인 삽입할 때, resource_domains에 “data:”가 없으면 마커 이미지가 표시되지 않음 — 이것도 조용히 실패하는 유형이라 원인 파악에 시간 소요

8. 배포와 운영에서의 선택

URL 분리

배포 환경에서는 세 가지 URL을 분리하여 관리합니다.

  • MCP 서버 URL: ChatGPT가 MCP 엔드포인트에 연결하는 주소
  • 위젯 CDN URL: 위젯 정적 파일(JS/CSS/HTML)이 서빙되는 S3/CloudFront 주소
  • 위젯 도메인: ChatGPT UI에서 위젯 상단 브랜드 링크 클릭 시 이동하는 서비스 도메인

빌드 시점에 위젯 CDN URL을 주입하여 HTML 안의 스크립트/스타일 경로를 만들고, MCP 서버의 CSP에도 이 도메인을 허용하도록 맞췄습니다.

서버에도 빌드가 필요한 이유

MCP의 ReadResource에서 위젯 HTML 템플릿을 내려줄 때, HTML 안에 <script src=”…”> 로 링크된 JS 파일명에는 빌드 시 생성되는 콘텐츠 해시가 포함됩니다(예: shop-list.a3f2b1c.js). S3/CDN에 배포된 JS 파일과 이 해시가 일치해야 위젯이 정상 로딩되므로, 서버 배포 시에도 반드시 pnpm run build를 실행하여 최신 빌드 결과물의 해시를 맞춰야 합니다. 위젯 빌드 → S3 업로드 → MCP 서버 배포 순서가 어긋나면 해시 불일치로 위젯 로딩이 실패하기 때문에, CI/CD 파이프라인에서 이 순서를 명시적으로 보장하도록 구성했습니다.

로컬 개발

로컬에서는 MCP 서버와 위젯 정적 파일을 하나의 서버(포트 8000)로 함께 서빙한 뒤, ngrok 같은 터널링 서비스로 외부에 노출하고, ChatGPT의 개발자 워크스페이스에 개발 앱으로 등록하여 실제 ChatGPT 대화 안에서 E2E 테스트를 진행했습니다.

다만 위젯 UI를 수정할 때마다 ChatGPT를 거치면 반복 비용이 크기 때문에, 위젯을 독립적으로 로컬 브라우저에서 바로 띄워 테스트할 수 있는 환경도 구성했습니다. test_widget/ 디렉토리에 각 위젯별 테스트 HTML(test-widget-shop-list.html, test-widget-map.html 등)을 만들어, 브라우저에서 직접 열어 위젯을 확인할 수 있도록 했습니다.

핵심은 mock-openai.js입니다. ChatGPT sandbox 안에서만 주입되는 window.openai 객체를 로컬에서 모의(mock)로 구현한 파일로, toolOutput, callTool, setWidgetState, openExternal 등 위젯이 의존하는 API를 동일한 인터페이스로 제공합니다. 특히 callTool은 로컬 MCP 서버에 실제 JSON-RPC 요청을 보내고 SSE 스트림으로 응답을 받아오도록 구현했기 때문에, ChatGPT 없이도 위젯 → MCP 서버 → 요기요 API 전체 흐름을 로컬에서 테스트할 수 있습니다. 테스트 HTML에서 updateWidgetProps()로 샘플 데이터를 주입하면 위젯이 즉시 렌더링되고, 위젯 안에서의 callTool 호출도 실제 MCP 서버를 거쳐 동작합니다.

또한 위젯 코드 자체에서도 useWidgetProps 훅에 defaultState 폴백을 두어, window.openai.toolOutput이 없는 환경에서도 빈 화면 대신 기본 UI가 렌더링되도록 처리했습니다.

9. 모니터링: Datadog APM과 SSE

운영에서 Datadog APM으로 트레이스와 성능을 보기로 했습니다. Node 서버에는 dd-trace를 쓰고, 호스트에 Datadog Agent가 떠 있으면 트레이스가 자동으로 수집됩니다.

여기서 두 가지를 특히 신경 썼습니다.

Tracer 초기화 순서

dd-trace는 다른 모듈(예: node:http)이 로드되기 전에 init() 되어야, HTTP 등이 자동 계측됩니다. ESM에서는 모든 import가 먼저 평가된 뒤에 top-level 코드가 실행되므로, 같은 파일 안에서 순서를 바꿔도 소용이 없습니다.

그래서 tracer 전용 파일(tracer.ts)을 두고, 진입점(server.ts)의 맨 첫 번째 import로 두어, node:http보다 먼저 tracer.init()이 실행되게 했습니다.

// server.ts — 반드시 첫 번째 import
import tracer from "./common/tracer.js";

import { createServer } from "node:http"; // 이 시점에서 이미 dd-trace가 패치 완료
// ...

SSE 트레이싱

MCP는 SSE로 한 연결이 오래 유지되기 때문에, “요청 하나 = 스팬 하나”인 일반 HTTP와 다르게 보입니다. 그래서 커스텀 스팬을 넣었습니다.

  • mcp.sse_connection: 클라이언트가 GET /mcp 로 SSE 연결을 열 때 시작하고, 연결이 끊길 때 finish.
  • mcp.sse_send: SSE로 JSON-RPC 메시지를 보낼 때마다 connection span의 자식 스팬으로 기록.

Datadog APM에서 “GET /mcp → mcp.sse_connection → mcp.sse_send 여러 개”처럼 한 트레이스 트리로 보이게 했고, 태그(mcp.transport: sse)로 SSE만 필터링해 볼 수 있습니다.

10. 앱 등록: 코드 완성이 끝이 아니었다

개발이 마무리되면 바로 서비스를 올릴 수 있을 거라 생각했지만, 실제로는 앱 등록 절차라는 예상치 못한 관문이 기다리고 있었습니다.

ChatGPT Apps의 SDK 문서와 MCP 개발 가이드는 사전에 충분히 검토할 수 있었지만, 정작 앱을 OpenAI에 등록(submit)하는 과정에서 요구하는 항목들은 등록 페이지가 열리기 전까지는 전혀 알 수 없었습니다. 12월 15일, 앱 등록이 공개되자마자 제출을 시도했는데, 다음과 같은 항목들이 필요했습니다.

  • 사업자 인증(Business Verification): 법인/사업자 정보와 관련 서류 제출
  • 도메인 소유권 인증: .well-known/openai-apps-challenge 엔드포인트를 통한 MCP 서버 도메인의 소유권 검증
  • 테스트 케이스 작성: 앱의 주요 기능별 시나리오와 기대 결과를 문서화하여 제출
  • OS별 구동 영상: iOS, Android, 데스크톱 등 각 플랫폼에서 앱이 정상 동작하는 모습을 녹화한 스크린 레코딩

코드 한 줄 건드리지 않는 작업이었지만, 서류 준비부터 영상 촬영·편집까지 꼬박 하루가 소요되었습니다. 특히 도메인 인증의 경우 MCP 서버에 .well-known 경로를 서빙하는 핸들러를 급히 추가해야 했고, 테스트 케이스는 “사용자가 치킨을 검색 → 가게 상세 확인 → 요기요 앱으로 주문”과 같은 End-to-End 시나리오를 플랫폼별로 작성해야 했습니다.

등록 제출 이후에는 OpenAI의 리뷰 프로세스를 기다려야 했습니다. 약 50일이 지난 뒤 앱이 Approved 상태가 되었고, 그제서야 ChatGPT 사용자들이 실제로 요기요 배달 앱을 사용할 수 있게 되었습니다. 개발 완료부터 실제 출시까지의 리드 타임이 생각보다 길었기 때문에, 앱 등록에 필요한 준비물을 개발 단계에서 미리 파악하고 병행하는 것이 중요하다는 교훈을 얻었습니다.

11. 개발하면서 만난 것들

CSP 위반은 소리 없이 실패한다

  • 가장 반복적으로 겪은 문제입니다. 이미지가 안 뜨거나 지도가 빈 화면이면, 코드 버그보다 CSP 도메인 누락을 먼저 의심해야 합니다. 브라우저 콘솔의 Refused to… 로그가 유일한 단서입니다.

CSP 도메인을 하드코딩하면 환경 전환이 어렵다

  • 로컬, ngrok, 프로덕션에서 MCP URL이 다르므로, CSP 메타데이터에 들어가는 도메인도 동적으로 결정해야 합니다. 처음에는 하드코딩해두고 환경마다 수동으로 바꿨는데, getMcpPublicUrl()로 런타임 결정하는 방식으로 바꾸니 실수가 줄었습니다.

ESM의 import 평가 순서

  • server.ts 한 파일 안에서는 node:http 가 이미 로드된 다음에 tracer.init() 이 실행되어, dd-trace가 HTTP를 패치할 수 없었습니다.
  • 별도 모듈로 분리하면, 그 모듈을 로드하는 시점에 tracer.init() 이 실행되고, 그 다음에 server.ts의 node:http 가 로드되므로 패치가 적용됩니다.

SSE가 APM에 안 보이는 것처럼 느껴졌던 점

  • 기본 HTTP 스팬만 있으면 “긴 연결 하나”로만 보이고, 내부에서 어떤 메시지를 몇 번 보냈는지 알기 어렵습니다.
  • 커스텀 스팬 + 부모/자식 관계를 명확히 하니, 트레이스 트리와 태그로 SSE 트래픽을 분석하기 쉬워졌습니다.

위젯 버전과 MCP 서버

  • 위젯을 S3에 새로 올렸을 때, MCP 서버가 참조하는 HTML 안의 JS/CSS 경로(해시)가 바뀌므로, MCP 서버도 같은 버전으로 빌드·배포하는 절차를 문서와 배포 스크립트에 넣었습니다.

12. 마치며

ChatGPT Apps SDK와 MCP를 쓰면, “대화 + 구조화된 도구 호출 + 인라인 위젯”을 한 번에 설계할 수 있었습니다. 툴 스키마와 위젯 매핑을 잘 나누어 두면, 이후에 툴을 추가하거나 위젯만 바꾸는 확장도 수월했습니다.

특히 CSP는 ChatGPT sandbox 환경에서 위젯을 만들 때 피할 수 없는 주제입니다. “어떤 외부 리소스를 쓰는가”를 위젯 개발 초기부터 파악해두고, MCP 메타데이터에 빠짐없이 선언하는 것이 디버깅 시간을 크게 줄여줍니다. 조용히 실패하는 특성 때문에, CSP 관련 문제는 알고 있으면 5분, 모르면 반나절이 걸립니다.

앞으로는 더 많은 툴(예: 주문·결제 플로우), 위젯 UX 개선, 그리고 사용 지표 연동까지 이어갈 수 있을 것 같습니다.

이 글은 ChatGPT App(요기요 배달 연동) 프로젝트의 기술적 구현과 개발 (Vibe Code with AI)경험을 정리한 글입니다. 실제 서비스 정책이나 API 스펙은 OpenAI 문서를 참고해 주세요. 이 글의 초안 작성과 구성에는 AI가 활용되었으며, 개발자가 검토·보완하여 완성하였습니다.

참고 링크


ChatGPT에서 요기요 배달 쓰기 — MCP + 위젯 연동 개발기 was originally published in YOGIYO Tech Blog - 요기요 기술블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.

Read Entire Article