1. ChatGPT App의 스모크‑테스트란?
일반 웹 개발 세계에서 스모크‑테스트는 “시스템이 기본적으로 살아 있는가?”를 확인하는 최소한의 점검입니다. 페이지가 열리고, 버튼이 다운되지 않으며, 치명적인 문제가 터지지 않는지 보는 것이죠.
ChatGPT Apps 세계의 스모크‑테스트는 연결 고리가 여러 개라 조금 더 흥미롭습니다. 다음 요소들이 한 체인으로 맞물립니다:
- 위젯 코드(React/Next.js)
- Next.js Dev 서버
- 터널(ngrok/Cloudflare)
- ChatGPT가 iframe을 만들고 그 안에 여러분의 위젯을 채팅 메시지로 로드
좋은 스모크‑테스트의 기준은 다음과 같습니다.
- 위젯이 오류 없이 ChatGPT 내부에 렌더링된다.
- 기본 인터랙션이 동작한다(예: 버튼을 누르면 외부 링크가 열린다).
- 브라우저 콘솔과 Dev 서버 로그에 치명적인 오류가 쏟아지지 않는다.
중요: 이 단계에서는 MCP 도구를 검증하지도, 부하 테스트를 하지도, 토큰 비용을 계산하지도 않습니다. 우리의 목표는 소박하지만 매우 실용적입니다. “코드 → Next.js → 터널 → ChatGPT → 사용자”라는 체인이 실제로 닫혔음을 증명하는 것이죠.
다음 표처럼 생각하면 이해하기 쉽습니다.
| 검증 항목 | 정상 동작 기준 |
|---|---|
| 위젯 렌더링 | ChatGPT에서 “깨진 iframe”이 아닌 우리의 UI가 보임 |
| ChatGPT ↔ 우리 서버 연결 | “앱을 불러올 수 없음” 류의 오류가 없음 |
| 샌드박스 내 JS 동작 | onClick 핸들러가 실제로 실행됨 |
| 외부 링크 열기 가능 | 버튼이 지정한 URL을 새 탭/창으로 엶 |
2. 우리의 학습용 App: 간단한 “Hello GiftGenius”
이 강의에서는 선물 추천을 도와주는 GiftGenius라는 애플리케이션을 점진적으로 만들어 갑니다. 이 단계에서는 아직 추천은 하지 않지만, 예의 바르게 인사하고 “자세히 보기” 링크를 보여줄 수는 있습니다.
복잡한 로직 없이도 살아 있는 React 코드가 들어간, 최소하지만 정직한 위젯이 필요합니다.
위젯 컴포넌트의 가장 단순한 형태는 다음과 같습니다(이름과 스타일은 취향에 맞게 바꿔도 되지만, 강의의 기본안을 사용해 보겠습니다).
// app/widget/page.tsx
'use client';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
이것이 여러분의 첫 번째 ChatGPT App입니다. 다음 단계에서 선물 추천 기능을 배워볼 것입니다.
</p>
</main>
);
}
몇 가지 중요한 포인트가 있습니다.
첫째, 파일 시작의 'use client'; 지시문은 이 컴포넌트를 클라이언트 컴포넌트로 만듭니다. 이것이 없으면 Next.js는 파일을 서버 컴포넌트로 취급하므로 window, onClick 핸들러, 그리고 어떤 브라우저 API도 사용할 수 없습니다.
둘째, 이건 평범한 React 컴포넌트입니다. 여기서는 “Apps SDK의 마법” 같은 건 보이지 않습니다 — 그게 정직한 방식이죠. 이 컴포넌트가 ChatGPT 내부에 나타나는 마법은 MCP 서버 설정과 위젯 URL을 반환하는 도구 구성에 숨어 있습니다. 그 부분은 나중에 다루고, 지금은 UI만 신경 씁니다.
3. 템플릿에 위젯을 넣고 실행하기
Apps SDK용 공식 Next.js 템플릿에는 위젯 페이지가 대개 이미 있습니다. 해당 페이지를 수정하거나, 필요한 라우트(예: /widget)로 새 페이지를 만들면 됩니다.
예를 들어 app/widget/page.tsx가 있고, 그 내용을 위의 코드로 교체한다고 가정해 봅시다. 그 다음 체인은 다음과 같습니다.
- 파일을 저장합니다.
- Next.js Dev 서버(npm run dev로 이미 실행 중)가 필요한 모듈을 리로드하고, HMR이 페이지를 갱신합니다.
- 터널을 통해 동일한 경로 /widget에서 공개 HTTPS URL이 업데이트된 UI를 제공합니다.
이 과정을 확인하는 방법은 두 가지입니다.
먼저, 로컬 브라우저에서 직접 확인합니다. 다음을 엽니다:
http://localhost:3000/widget
그리고 동일한 Hello from GiftGenius를 봅니다. 아직 ChatGPT는 아니지만, Next.js 애플리케이션의 UI가 살아 있음을 확인한 것입니다.
그 다음은 터널을 통해 확인합니다. 발급받은 URL(예: https://witty-cat.ngrok-free.app)에 /widget을 덧붙여 일반 브라우저에서 엽니다:
https://witty-cat.ngrok-free.app/widget
모든 것이 정상이라면, 페이지는 동일하게 보일 것입니다. 즉, “Next.js → 터널 → 여러분의 브라우저” 체인이 동작합니다. 이제 둘 사이에 ChatGPT만 끼워 넣으면 됩니다.
4. ChatGPT 내부에서 위젯 확인하기
Dev Mode의 ChatGPT는 본질적으로 세 가지를 수행합니다. iframe을 만들고, 그 안에 공용 URL을 src로 설정한 뒤, 채팅 메시지 안에서 그 iframe이 동작하도록 합니다.
이벤트를 단순화하면 다음과 같습니다.
sequenceDiagram
participant Dev as 개발자(Dev)
participant Next as Next.js dev-서버
participant Tun as 터널 (HTTPS)
participant GPT as ChatGPT
participant User as 사용자
Dev->>Next: npm run dev (http://localhost:3000)
Dev->>Tun: 포트 3000으로 터널 시작
GPT->>Tun: GET https://.../widget
Tun->>Next: http://localhost:3000/widget 로 프록시
Next-->>Tun: 위젯의 HTML + JS
Tun-->>GPT: HTML/JS 응답
GPT->>User: 위젯이 포함된 iframe 렌더
결과를 보려면 다음을 진행합니다.
- 브라우저에서 ChatGPT를 열고, 필요한 모델을 선택합니다(보통 GPT‑5.1 또는 Dev Mode 기본값).
- 앱 메뉴/Developer에서 자신의 애플리케이션을 명시적으로 선택하거나 “GiftGenius 앱을 실행해 줘” 같은 문장으로 앱을 호출합니다.
- ChatGPT가 여러분의 App을 호출하고, MCP 서버가 UI 링크(바로 그 /widget)를 포함한 응답을 반환하며, 채팅 메시지에 위젯이 나타납니다.
정상이라면, ChatGPT 내부에서 익숙한 “Hello from GiftGenius” 헤더를 보게 됩니다. 이 시점에서 스모크‑테스트는 거의 끝났습니다. iframe이 렌더링되고 “Next.js → 터널 → ChatGPT” 체인이 살아 있음을 확인했습니다. 이제 표의 마지막 항목, 즉 위젯이 예측 가능하게 외부 링크를 열 수 있는지만 확인하면 됩니다. 이를 위해 openExternal이 필요합니다.
조금 후에 코드를 바꾸기 시작하면, 일반적인 개발 사이클은 대략 다음과 같아집니다.
- JSX를 수정합니다.
- 저장합니다.
- ChatGPT 탭을 새로고침하거나(때로는) 위젯을 “살짝 건드리는” 것 — 예를 들어 새 메시지를 보내거나 App을 다시 실행 — 만으로 충분할 때도 있습니다(템플릿/캐싱 설정에 따라 다름).
변경 사항이 보이지 않는다면, 먼저 세 가지 용의자를 떠올리세요. Dev 서버가 꺼졌거나, 터널이 끊겼거나, ChatGPT가 오래된 URL에 연결되어 있을 수 있습니다. “문제가 생겼을 때 어디를 봐야 하는가” 섹션에서 이 시나리오를 자세히 다룹니다.
5. 왜 그냥 <a href>로 끝내면 안 될까
스모크‑테스트의 마지막 항목 — 외부 페이지를 여는 버튼 — 을 충족하려면 openExternal을 이해해야 합니다. 당연한 질문이 있습니다. “왜 굳이 openExternal이 필요하죠? 일반 링크로 하면 안 되나요?”
여러분의 위젯은 “그냥 브라우저”가 아니라 ChatGPT가 관리하는 iframe 안에서 동작합니다. 이 iframe은 상당히 제한적인 샌드박스에서 실행됩니다. Content Security Policy 제한, sandbox 속성, target="_blank"의 특이점과 팝업 차단 등이 작동할 수 있습니다. 그 결과, 이와 같은 환경에서 <ahref="…">나 window.open()의 동작은 예측 불가능해질 수 있습니다. 아예 무시되거나, 여러분의 코드가 제어할 수 없는 경고가 뜰 수도 있습니다.
또한 UX 관점에서 OpenAI는 여러분이 외부 페이지를 언제 어떻게 여는지 통제하고자 합니다. 그래서 Apps SDK는 표준화된 브리지 window.openai를 제공합니다. 여러분의 코드는 부모 창을 직접 건드리지 않고, 명확히 정의된 API를 통해 호스트 애플리케이션에 동작을 위임합니다.
6. API window.openai.openExternal: 무엇이며 어떻게 동작하나
위젯 샌드박스에는 전역 객체 window.openai가 제공됩니다. 이는 여러분의 UI와 ChatGPT 사이의 핵심 “브리지”로, 이를 통해 도구 호출, 후속 메시지 전송, 표시 모드 변경, 위젯 상태 관리, 그리고 외부 링크 열기 등을 수행할 수 있습니다.
이 강의에서 우리가 다룰 메서드는 하나입니다.
window.openai.openExternal({ href: string }): void;
window.openai.openExternal({ href: 'https://example.com' })을 호출하면, ChatGPT는 다음을 수행합니다.
- URL이 정책에 의해 허용되는지 확인합니다.
- 사용자에게 경고(예: 외부 사이트로 이동)를 표시할 수 있습니다.
- 해당 링크를 사용자의 브라우저 새 탭/창에서 엽니다.
두 가지를 이해하는 것이 중요합니다.
첫째, 이것은 순수한 클라이언트 동작입니다. MCP 도구를 호출하지도, 여러분의 백엔드로 가지도, OpenAI 토큰을 소비하지도 않습니다. 그저 호스트 애플리케이션에 “이 URL을 열어 주세요”라고 신호를 보내는 것입니다.
둘째, 이 방식은 샌드박스와 호환됩니다. ChatGPT는 링크를 어떻게 열지 스스로 결정하여, 여러분의 iframe이 window.open()으로 과도한 행동을 하지 못하게 합니다.
7. 위젯에 openExternal 버튼 추가하기
이제 “Hello GiftGenius”에서 외부 링크를 여는 법을 익혀 봅시다. 가장 단순한 시나리오: “데모 링크 열기” 버튼을 눌러 문서나 서비스 랜딩 페이지로 이동시키는 것입니다.
먼저 작은 헬퍼를 작성해 TypeScript 경고를 잠재우고, /widget을 브라우저에서 직접 열었을 때(window.openai가 아직 없는 환경) 위젯이 다운되지 않도록 합시다.
// app/widget/openExternalSafe.ts
export function openExternalSafe(href: string) {
if (typeof window !== 'undefined' && (window as any).openai?.openExternal) {
(window as any).openai.openExternal({ href });
} else {
// ChatGPT 없이 로컬로 볼 때를 위한 폴백
window.open(href, '_blank', 'noopener,noreferrer');
}
}
여기서는 (window as any)를 의도적으로 사용했습니다. window.openai의 타입 정의로 여러분을 괴롭히지 않으려는 선택입니다. 강의 뒷부분에서 이 객체의 인터페이스를 깔끔하게 정의할 것입니다. 지금은 코드가 컴파일되고 동작하기만 하면 충분합니다.
이제 헬퍼를 위젯에 연결하고 버튼을 추가합니다.
// app/widget/page.tsx
'use client';
import { openExternalSafe } from './openExternalSafe';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
이것이 여러분의 첫 번째 ChatGPT App입니다. 다음 단계에서 선물 추천 기능을 배워볼 것입니다.
</p>
<button
type="button"
onClick={() => openExternalSafe('https://example.com')}
style={{
padding: '8px 16px',
borderRadius: 8,
border: '1px solid #ccc',
cursor: 'pointer',
}}
>
데모 링크 열기
</button>
</main>
);
}
클릭 시 일어나는 일은 다음과 같습니다.
위젯이 ChatGPT 내부에서 실행 중이라면, window.openai.openExternal이 존재하고, ChatGPT가 https://example.com을 정책에 맞게 엽니다.
반면 http://localhost:3000/widget을 일반 브라우저에서 직접 열었다면 window.openai가 없고, 폴백이 작동합니다. 브라우저의 기본 방법으로 새 탭이 열립니다. 이 경우 window.open은 오직 일반 브라우저에서 /widget을 직접 열었을 때만 쓰이며, 이미 ChatGPT 샌드박스 바깥이므로 문제를 일으키지 않습니다.
openExternal에 대해서는 모듈 3(위젯과 샌드박스 전용 강의)에서 더 자세히 다룹니다. 지금은 앱 실행을 이어가면 됩니다.
8. 미니 end‑to‑end 스모크‑테스트
이제 실제에 가까운 전체 실행을 해 봅시다. 다음 단계를 따라가세요.
- Dev 서버가 실행 중인지 확인합니다(npm run dev). 그리고 http://localhost:3000/widget에서 Hello from GiftGenius가 보이는지 확인합니다.
- 3000 포트로 터널이 열려 있고, 공용 URL이 외부 브라우저에서 열리는지 확인합니다.
- ChatGPT를 열고 Dev Mode를 활성화한 뒤, App이 올바른 URL(공용 URL, localhost가 아님)에 연결되어 있는지 확인합니다.
- 채팅을 열고 App을 선택(또는 모델에 실행을 요청)합니다.
- 임베디드 위젯에서 “Hello from GiftGenius”가 보이는지 확인합니다.
- “데모 링크 열기” 버튼을 눌러 https://example.com(또는 여러분의 주소)이 열리는지 확인합니다.
모두 정상 동작했다면 다음을 의미합니다.
- 위젯의 HTML/JS가 Next 서버에서 올바르게 빌드되고 제공되고 있다.
- HTTPS 터널이 요청을 올바르게 프록시하고 있다.
- ChatGPT가 여러분의 URL을 신뢰하며 위젯을 불러올 수 있다.
- window.openai가 동작하여 외부 링크 열기 명령이 전달된다.
이것이 우리가 첫 스모크‑테스트에서 목표한 바입니다.
9. 문제가 생겼을 때 어디서 원인을 찾을까
“일반적인” 프런트엔드와 달리, 여기서는 진단해야 할 주요 위치가 세 군데뿐입니다. 어디에서 문제가 발생했는지 빠르게 좁히는 것이 중요합니다.
- 먼저 ChatGPT의 UI를 확인합니다. 위젯 대신 “Error loading app”이나 “We had trouble talking to your app” 같은 오류 메시지가 보인다면, 문제는 대개 터널이나 Dev 서버의 접근성에 있습니다. 공용 URL을 브라우저에서 직접 열어 보세요. 열리지 않거나 Next.js 오류가 뜬다면 그 문제부터 해결해야 합니다.
- 다음으로 ChatGPT가 동작 중인 브라우저 탭에서 DevTools를 엽니다. 거기에는 여러분의 위젯을 위한 별도의 iframe이 있고, 그 안에 익숙한 Console 탭이 있습니다. openExternal 버튼을 눌러도 아무 일도 일어나지 않는다면, “window.openai is undefined” 같은 오류나 다른 JS 오류가 없는지 보세요. 이런 오류가 있다면, 아마 ChatGPT가 아닌(터널 URL을 직접 여는) 환경에서 위젯을 테스트하고 있거나 'use client'; 지시문을 빠뜨렸을 가능성이 큽니다.
- 동시에 npm run dev가 실행 중인 터미널을 확인합니다. 빌드 오류(TypeScript, ESLint, 컴파일)가 쏟아지는 경우, ChatGPT는 잘해야 이전 버전의 코드를 보고, 최악의 경우 아무것도 보지 못합니다. 오류가 없는데도 갱신이 보이지 않는다면 터널이 아직 살아 있는지 확인하세요. 많은 터널 서비스가 유휴 타임아웃으로 세션을 끊습니다.
또 다른 전형적인 사례가 있습니다. localhost에서는 잘 되는데, 터널을 통해 접근하면 404나 이상한 페이지가 보이는 경우입니다. 이때는 기본 경로(/widget vs /), basePath/assetPrefix 설정(이미 손댔다면), Dev Mode에 설정한 주소를 세심하게 점검하세요.
10. 약간의 “정리”: 프로세스 중지
사소해 보이지만, 실제로 매우 유용한 팁입니다. 초보자는 Dev 서버와 터널이 각각 별도의 프로세스로 백그라운드에서 계속 실행된다는 사실을 종종 잊습니다.
갑자기 “포트 3000이 이미 사용 중”이라면, 어딘가의 터미널에 옛날 npm run dev가 숨어 있을 수 있습니다. Windows에서는 작업 관리자에서 꽤 번거로운 삽질이 될 수 있고, macOS와 Linux에서는 해당 터미널에서 Ctrl + C로 종료하는 것이 보통 쉽습니다.
터널도 마찬가지입니다. 여러 터널을 연달아 실험하거나 예전 터널을 닫지 않았다면, Dev Mode에서 현재 App이 어떤 URL에 묶여 있는지 헷갈리기 쉽습니다. 세션을 끝낼 때는 “터널 끄기 → Dev 서버 중지”를 습관화하고, 다음 실행은 깨끗한 상태에서 시작하는 것이 좋습니다.
11. 첫 스모크‑테스트에서 흔한 실수
오류 №1: 공용 HTTPS URL 대신 localhost를 사용함.
자주 있는 일입니다. Dev Mode에 http://localhost:3000을 넣거나 아예 터널을 잊어버리는 경우죠. 여러분의 컴퓨터에서는 잘 동작하지만, 클라우드의 ChatGPT는 localhost에 접근할 방법이 없습니다. 해결책은 간단합니다. App 설정에 터널의 공용 HTTPS 주소가, 그리고 올바른 경로(/mcp인지 루트인지 — 템플릿에 따라 다름)까지 포함되어 있는지 확인하세요.
오류 №2: 위젯 파일에 'use client'; 지시문을 빼먹음.
멋진 React 코드를 쓰고 onClick을 추가하며 window.openai에 접근했는데, Next.js가 묵묵히 페이지를 서버 컴포넌트로 만듭니다. 운이 좋아도 “window is not defined”를 맞고, 운이 나쁘면 컴포넌트가 아예 빌드되지 않습니다. 브라우저 API에 접근하려면 위젯이 클라이언트 컴포넌트여야 하며, 이는 첫 줄의 'use client';가 말해 줍니다.
오류 №3: openExternal 대신 window.open()을 직접 호출함.
가끔은 window.open('https://example.com')이 더 쉬워 보입니다. 일반 브라우저에서는 돌아갈 수도 있지만, ChatGPT 샌드박스 내부에서는 예측할 수 없는 동작(무시, 차단 등)을 보게 될 겁니다. ChatGPT Apps의 올바른 경로는 호스트에 링크 열기를 위임하고 모든 보안 정책을 준수하는 window.openai.openExternal({ href })입니다.
오류 №4: TypeScript가 window.openai에 붉은 줄을 긋자, 타입을 통째로 끄며 “해결”함.
절망한 나머지 파일 맨 위에 // @ts-nocheck를 적는 경우가 있습니다. 컴파일 오류는 사라지겠지만, 동시에 해당 파일의 TypeScript가 전부 꺼집니다. 훨씬 안전한 방법은 window 주변에 국소적으로 as any를 쓰거나, 별도 파일에 window.openai를 위한 최소 인터페이스를 정의하는 것입니다. 우리는 이 모듈에서 openExternalSafe 헬퍼와 (window as any)를 선택했고, 깔끔한 타입 정의는 나중에 추가합니다.
오류 №5: 결과를 localhost에서만 확인하고, ChatGPT 내부에서는 보지 않음.
http://localhost:3000/widget이 열리는 것만 보고 끝내고 싶은 유혹이 생깁니다. 하지만 이 모듈의 요지는 App을 실제로 ChatGPT 인터페이스 안에서 보는 것입니다. 일반 브라우저에서 잘 된다고 해서 ChatGPT가 iframe을 바르게 만들고, 터널을 통해 리소스를 가져오며, CORS/CSP에 막히지 않는다는 보장은 없습니다. 제대로 된 스모크‑테스트에는 항상 ChatGPT에서 App을 실제로 실행하는 단계가 포함됩니다.
오류 №6: 터널이 꺼졌거나 잊고 있음.
코드를 업데이트했는데 ChatGPT에는 예전 위젯 버전이 보이거나, 아예 아무것도 로드되지 않을 수 있습니다. 종종 터널이 타임아웃으로 닫혔지만 Developer Mode는 여전히 옛 URL을 보고 있는 상황입니다. 터널 URL을 일반 브라우저에서 열었을 때 오류가 보인다면 — 먼저 터널을 복구한 뒤 Apps SDK를 의심하세요.
오류 №7: iframe의 콘솔을 무시함.
SPA 경험이 있는 개발자는 자신의 앱 DevTools에서 console.log를 확인하는 데 익숙하지만, ChatGPT 내부에서는 iframe이므로 DevTools에서 올바른 프레임을 선택해야 합니다. 최상위 레벨만 보면, 위젯 내부는 이미 붉게 물들었는데도 아무 오류도 보지 못할 수 있습니다. “iframe 위젯 프레임에서 DevTools 열기” 습관은 많은 시간을 절약해 줍니다.
GO TO FULL VERSION