1. “랜덤” 터널의 문제
처음 ngrok http 3000이나 빠른 Cloudflare Quick Tunnel을 실행해 보면 마치 마법 같습니다. 순식간에 여러분의 http://localhost:3000이 https://random-1234.tunnelprovider.com로 바뀌죠. 그 URL을 ChatGPT Dev Mode에 붙여 넣으면 GPT가 기쁜 마음으로 여러분의 App을 불러옵니다.
그런데 터널을 재시작하면… 새로운 도메인을 받게 됩니다. ChatGPT의 Dev‑앱 설정에 있던 예전 URL은 갑자기 “깨진 링크”가 되고, GPT는 성실하게 “App unavailable”이라고 쓰며, 여러분은 다시 설정으로 가서 URL을 바꾸고 Save를 누르고, 업데이트될 때까지 기다리며 이 모든 스택을 속으로 미워하게 됩니다.
하룻밤 “가볍게 만져보기” 용도로는 참을 만합니다. 하지만 여러분이 다음과 같은 상황이라면:
- 매일 App을 계속 다듬고;
- 중간 버전을 동료/매니저에게 보여주고 싶고;
- 동시에 staging과 production도 세팅한다면,
매번 새 랜덤 URL에 맞춰 Dev Mode를 다시 연결하는 일은 고통 그 자체가 됩니다.
게다가 이미 도메인에 의존하는 무언가(예: OAuth redirect URI나 웹훅)를 앱에 추가했다면, URL이 바뀔 때마다 그것들도 같이 깨집니다. “터널을 바꿈 → ChatGPT App 설정, OAuth 제공자의 redirect‑URL, webhook 수신 설정까지 전부 수정”이라는 연쇄 반응이 생깁니다.
여기서 이 강의의 핵심 아이디어가 나옵니다: 안정적인 dev‑URL은 사치가 아니라, 개발자의 정신 건강을 지켜주는 도구입니다.
Insight
ChatGPT는 여러분의 앱과 통신할 때 매우 엄격한 타임아웃을 적용하며, 이를 과소평가하기 쉽습니다. MCP tool 호출에는 시간 제한이 있습니다: 최대 2분 — 그 이후 플랫폼은 서버가 내부에서 뭔가를 계속하더라도 호출을 실패로 간주합니다.
앱 등록(Store 또는 Dev Mode)은 더 엄격합니다. 매니페스트, 리소스, 도구 설명을 읽는 데 ChatGPT가 주는 시간은 대략 20초입니다. 이 시간 안에 MCP 서버가 초기화와 tools/resources 목록 제공 등을 끝내지 못하면, App 등록은 타임아웃으로 실패합니다.
권장 사항: 모든 무거운 초기화는 Dev Mode나 Store로 가기 전에 끝내야 합니다. DB 커넥션 워밍업, 큰 설정 로드, 지연 캐시 로딩 — 이런 것들은 미리 수행하세요. 예를 들어 MCP Jam이나 내부 스크립트로 서버를 한 번 호출해 두는 식입니다. 플랫폼 관점에서 MCP 서버는 “따뜻한” 상태여서 수 초 내 응답해야 하며, 등록 도중에 “깨어나는” 것은 허용되지 않습니다.
2. “성숙한” 터널이란
이제 “성숙한” 터널이 초반에 실행해 보던 것과 어떻게 다른지 정리해 보겠습니다.
초기 모드(모듈 2)는 다음과 같았습니다:
# ngrok 예시
ngrok http 3000
# 결과: https://random-abc123.ngrok-free.app
그 일회성 URL을 Dev Mode에 넣었습니다. 다음에 ngrok을 실행하면 URL이 달라지고, ChatGPT 설정은 이미 낡아버립니다.
“성숙한” 접근에서는 다음을 갖춥니다:
- 정적 서브도메인(또는 자체 도메인)이 터널 제공자에게 있고;
- 항상 같은 도메인이 여러분의 localhost:3000으로 포워딩되며;
- 터널, 머신, 라우터를 재시작해도 URL은 그대로입니다.
이러한 정적 서브도메인은 예를 들어 다음에서 제공합니다:
- ngrok — 계정당 무료 static domain;
- Cloudflare Tunnel — 이름 있는 터널과 자체 도메인 연결.
그리고 ChatGPT Dev Mode의 앱 설정은 오직 그 하나의 URL만 가리키므로, 더 이상 여러분을 괴롭히지 않습니다.
형식적으로 “성숙한” 터널의 요건은 다음과 같습니다:
- 항상 동일한 공개 HTTPS 도메인;
- 유효한 TLS 인증서(제공자가 처리);
- “https://dev.yourdomain.com으로 온 모든 요청을 http://localhost:3000으로 포워딩” 같은 구성이 있을 것;
- 선택 사항 — 최소한의 보안 조치(예: URL을 StackOverflow 등에 함부로 노출하지 않기).
3. 안정적인 dev‑URL 설정: Cloudflare Tunnel 예시
이 강의에서는 Cloudflare Tunnel을 기본 도구로 권장합니다. dev에도 잘 맞고, 더 진지한 시나리오에도 확장하기 쉽기 때문입니다. 모듈 2에서 기본 설정을 이미 봤으니, 이제 이를 상시 dev‑URL로 “완성”해 봅니다.
학습용 앱 GiftGenius가 있고, giftgenius-dev.yourdomain.com이라는 안정적인 URL을 원한다고 가정합시다.
최소 단계(Cloudflare UI에 종속되지 않게 단순화):
- 도메인을 Cloudflare 계정에 연결합니다(한 번, 대시보드에서).
- 로컬에 cloudflared를 설치하고 로그인합니다.
brew install cloudflare/cloudflare/cloudflared # macOS
cloudflared login # 인증을 위해 브라우저가 열립니다
3. 이름 있는 터널을 생성합니다:
cloudflared tunnel create giftgenius-dev
4. ~/.cloudflared/config.yml에 라우팅을 설정합니다:
tunnel: giftgenius-dev
credentials-file: /Users/you/.cloudflared/giftgenius-dev.json
ingress:
- hostname: giftgenius-dev.yourdomain.com
service: http://localhost:3000 # 우리 Next.js dev 서버
- service: http_status:404
5. 터널을 실행합니다:
cloudflared tunnel run giftgenius-dev
이제 npm run dev와 cloudflared tunnel run이 실행 중인 동안, 여러분의 로컬 Next.js는 고정 URL https://giftgenius-dev.yourdomain.com로 접근 가능합니다. 그리고 ChatGPT Dev Mode 설정에는 바로 이 URL을 지정합니다.
우리 앱과의 연결
ChatGPT에서 Dev‑앱을 연결할 때 입력하는 앱의 URL을 브라우저에서 열어 보세요:
https://giftgenius-dev.yourdomain.com/mcp
다음과 같은 응답(에러)을 보게 됩니다:
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Method not allowed."},"id":null}
이는 완전히 정상입니다. /mcp 서버는 GET 요청을 기대하지 않기 때문입니다. 그 외 위젯, MCP 엔드포인트 /mcp, API 라우트 등도 같은 터널을 통해 접근하므로, 매번 새로운 도메인을 기억할 필요가 없습니다.
4. 대안: ngrok의 고정 서브도메인
이미 ngrok에 익숙하다면, static domain을 사용해 비슷하게 “성숙하게” 만들 수 있습니다. 2023년부터 ngrok은 무료 플랜에서도 myapp-dev.ngrok-free.app 형태의 정적 서브도메인을 하나 고정할 수 있도록 제공합니다.
최소 구성은 다음과 같습니다:
# ~/.config/ngrok/ngrok.yml
authtoken: <your-token>
tunnels:
giftgenius-dev:
addr: 3000
proto: http
domain: giftgenius-dev.ngrok-free.app
실행:
ngrok start giftgenius-dev
결과적으로 https://giftgenius-dev.ngrok-free.app URL이 고정되며, 이를 ChatGPT Dev Mode의 앱 기본 URL로 지정하면 됩니다.
철학은 동일합니다:
- “랜덤” 주소는 금지;
- 변하는 것은 터널의 내부 상태(실행/중지)뿐이고, 도메인은 변하지 않음;
- Dev Mode를 다시 연결할 필요가 없음.
Cloudflare와 ngrok은 이 점에서 단지 다른 맛의 아이스크림일 뿐입니다. 자신만의 도메인과 세밀한 DNS 제어를 선호하면 Cloudflare, “YAML 하나로 끝”을 선호하면 ngrok을 고르면 됩니다. 이 강의에서는 두 접근 모두 유효합니다. 중요한 건 안정적인 URL입니다.
5. 스키마: ChatGPT Dev Mode ↔ 터널 ↔ 로컬 스택
상황을 조금 더 형식화해 그림으로 나타내 봅시다.
flowchart TD
ChatGPT["ChatGPT (Dev Mode)"]
AppCfg["Dev App (설정: https://giftgenius-dev...)"]
Tunnel["Cloudflare/ngrok 터널 (giftgenius-dev...)"]
Next["Next.js dev 서버 localhost:3000 + MCP handler"]
ChatGPT --> AppCfg
AppCfg -->|"설정에 https://giftgenius-dev.../.well-known/openai-app 가 명시됨"| Tunnel
Tunnel -->|"HTTPS → HTTP 프록시"| Next
ChatGPT는 여러분이 노트북에서 무엇을 돌리는지 알지 못합니다. 그에게는 오직 하나의 HTTPS 엔드포인트만 있습니다. 그 뒤가 Vercel이든, 로컬 터널이든, Kubernetes든 여러분의 선택일 뿐입니다. 그리고 이 강의에서 우리가 관심 갖는 것은 로컬 개발을 위한 그 HTTPS 엔드포인트의 안정성입니다.
이제 앱 내부에서도 이 주소가 하드코딩된 문자열로 여기저기 흩어지지 않고, 하나의 “진실의 원천”이 되도록 만들어야 합니다 — 다음 섹션이 그 이야기입니다.
6. 환경 변수와 코드의 baseURL
문제없이 동작하도록 하려면, Next.js 코드에서 “앱의 외부 기본 URL”을 한 번 정의하고 이후로는 오직 그것만 참조하는 것이 좋습니다.
예를 들어, GiftGenius 앱의 app/lib/config.ts에 다음을 둘 수 있습니다:
// app/lib/config.ts
export const baseUrl =
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"; // fallback
export const mcpEndpoint = `${baseUrl}/mcp`; // MCP 서버 URL
그리고 개발 시 .env.local에 다음을 지정합니다:
NEXT_PUBLIC_APP_URL=https://giftgenius-dev.yourdomain.com
그러면:
- 위젯 내부와 모든 링크에서 항상 baseUrl을 사용하고;
- ChatGPT Dev Mode와 브라우저 입장에서도 일관되게 보이며;
- 내일 https://giftgenius-staging.vercel.app 도메인의 Vercel staging으로 옮겨도 환경 변수만 바꾸면 됩니다.
이는 특히 다음에 중요합니다:
- callback‑URL(예: OAuth, webhook 처리기);
- 위젯에서 사용자에게 보여주는 링크(예: openExternal로 여는 “브라우저에서 열기” 버튼);
- 앱 로직의 모든 절대 URL.
지금은 dev‑URL만 이야기하지만, “baseUrl의 단일 진실의 원천”이라는 아키텍처 아이디어는 staging/production으로 그대로 확장됩니다.
7. ChatGPT Dev Mode에서 URL 업데이트
좋습니다. 멋진 고정 도메인을 만들었습니다. 이제 Dev Mode에서는 어떻게 쓰면 될까요?
로직은 이렇습니다:
- Dev‑앱 설정에서 한 번 루트 URL을 지정합니다: https://giftgenius-dev.yourdomain.com/
- ChatGPT는 이 URL로 매니페스트(.well-known/openai-app)를 읽고, 이후 MCP(/mcp), 정적 파일 등도 같은 루트를 사용합니다.
- 코드만 변경한다면(React 위젯, MCP 핸들러, 스타일), URL을 전혀 바꿀 필요가 없습니다. 터널이 실행 중이고 Next.js 서버가 응답하기만 하면 됩니다.
- 도메인 자체를 바꿀 때만(드물게, 예: ngrok에서 Cloudflare로 변경) Dev Mode에 들어가 endpoint를 한 번 바꾸면 됩니다.
경우에 따라 ChatGPT가 매니페스트를 캐싱해 변경이 즉시 반영되지 않을 수 있습니다. Dev Mode UI에는 보통 “Reload configuration / Refresh App” 같은 버튼이 있으며, 최악의 경우 같은 URL로 한 번 끊었다가 다시 연결해도 도움이 됩니다.
중요: URL을 바꾸지 않는 한, Dev Mode는 코드의 새 버전을 자동으로 감지합니다. App의 핵심 트리거는 도메인이지 commit‑hash가 아닙니다.
8. Dev Mode에서 dev / staging / prod 전환
안정적인 dev 도메인은 첫걸음일 뿐입니다. 프로젝트가 커질수록 URL 혼돈에 빠지지 않으려면, dev 터널이 환경(dev/staging/prod) 및 Dev Mode 전체 구도에서 어떻게 맞물리는지 미리 이해하는 게 좋습니다. staging과 prod는 다음 Vercel 강의의 주제지만, Dev Mode는 이미 여러 환경을 다룰 수 있습니다.
다음 표가 이해에 도움이 됩니다:
| 환경 | 기본 URL | 코드 실행 위치 |
|---|---|---|
| Local | |
로컬 Next.js + MCP(터널 경유) |
| Staging | |
Vercel Preview / 스테이징 배포 |
| Prod | |
Vercel 프로덕션 |
Dev Mode를 쓰는 방식은 두 가지입니다.
첫째 — Dev‑App 하나만 두고 가끔 URL을 바꿔서 staging이나 prod를 테스트합니다(주의 필요). 초기에는 가능하지만 쉽게 혼동됩니다. 오늘은 로컬을 테스트하고, 내일은 staging, 모레는 전환을 깜빡해 Dev‑App이 prod로 요청을 보내는 식입니다.
둘째 — 더 건강한 방식: 환경마다 Dev‑앱을 분리합니다.
- GiftGenius Dev → giftgenius-dev.yourdomain.com;
- GiftGenius Staging → giftgenius-staging.vercel.app;
- GiftGenius(실서비스, Store 경유) → giftgenius.vercel.app.
이번 강의에서는 우선 dev‑URL부터 정리합니다. 다음 강의에서는 Vercel과 preview 배포를 staging/production에 어떻게 논리적으로 연결할지 살펴봅니다.
9. 팀 작업: 여러 개발자와 하나의 터널
개인 dev 도메인과 1인 개발이라면 터널은 든든한 친구입니다. 하지만 팀이 합류하면 터널과 환경이 겹치기 시작합니다. 여기서 중요한 것은 “하나의 서브도메인을 두고 벌이는 전쟁”을 피하는 것입니다.
두 개발자가 같은 정적 서브도메인, 예를 들어 giftgenius-dev.ngrok-free.app를 쓰기로 했다고 합시다. 둘 다 로컬에서 ngrok start giftgenius-dev를 실행합니다. 최선의 경우 한 쪽 터널이 안 떠서(도메인 충돌) 끝나고, 최악의 경우 서로의 세션을 번갈아 “덮어쓰며” ChatGPT가 이쪽/저쪽에 번갈아 연결되는 상황이 됩니다.
대응 전략은 몇 가지가 있습니다.
가장 단순한 것은 개인별 dev 도메인입니다:
- alex.dev.giftgenius.app;
- maria.dev.giftgenius.app.
그리고 ChatGPT에도 개인 Dev‑App을 둡니다. 예: GiftGenius Dev (Alex), GiftGenius Dev (Maria). 그러면 각자 로컬을 마음 편히 돌리고 남을 방해하지 않습니다.
좀 더 “팀 지향” 방식은 공용 staging 엔드포인트입니다:
- 모든 개발자에게 개인 dev 터널이 있습니다(개인 디버깅용).
- 거기에 더해, feature 브랜치가 머지되는 Vercel의 staging이 있고, 공용 Dev‑App GiftGenius Staging이 그것을 가리킵니다.
이 방식은 실제 팀에서 자주 봅니다:
- 기능은 로컬에서 태어나 개인 터널로 디버깅되고;
- pull request와 머지 후에는 모두가 staging에서 테스트합니다(터널 없이, 단순히 Vercel URL로).
10. dev 터널 보안(짧고 지나치지 않게)
터널은 로컬 서버를 인터넷으로 끌어내는 편리한 방법입니다. 그리고 인터넷은 대체로 봇, 스캐너, 그리고 여러분이 admin/admin 비밀번호를 잊었는지 확인하는 걸 좋아하는 사람들로 이루어져 있습니다.
dev 단계에서 이미 기억해야 할 기본 사항:
- 터널은 해당 포트에서 떠 있는 모든 것을 외부에 노출합니다. DB 관리자, phpMyAdmin, “비번 없는 테스트 CRM” 등을 같이 올리지 마세요;
- 터널 URL을 공개 저장소나 공개 채널에 올리지 마세요;
- 작업하지 않을 때는 터널을 끄세요(노트북도 가끔 꺼 주세요 — 그 친구도 휴식이 필요합니다).
Basic Auth, 특수 헤더 검사, URL 토큰 같은 더 진지한 조치는 보안 모듈에서 다룹니다. 지금 중요한 건 하나입니다: 터널은 개발 도구이지, 보호된 서버가 아닙니다. 프로덕션은 Vercel 같은 정상적인 호스팅에서 돌아갈 것이고, 다른 보호 메커니즘을 사용하게 됩니다.
11. 실습: 우리 앱에 안정적인 dev‑URL 설정하기
이제 이론과 주의사항을 실제에 연결해 봅시다. Next.js(Apps SDK 템플릿)로 만든 학습용 앱에 적용합니다.
프로젝트 구조가 다음과 같다고 가정합니다:
apps/
web/ # Next.js App + 위젯
mcp-server/ # (선택) 별도 MCP, 또는 web의 /mcp 핸들러
실제로는 Next.js 안에 MCP를 둘 수도 있습니다(app/api/mcp/route.ts). 원리는 동일합니다.
1단계. .env.local 수정
여기에 안정적인 dev‑URL(터널)을 추가합니다:
NEXT_PUBLIC_APP_URL=https://giftgenius-dev.yourdomain.com
개발 코드에서는 이미 이 변수에서 baseUrl을 사용하고 있습니다(위 참고). 아직 아니라면, 지금이 분리할 때입니다.
2단계. dev 서버와 터널 실행
cd apps/web
npm run dev # localhost:3000에서 Next.js 실행
# 별도 터미널
cloudflared tunnel run giftgenius-dev
브라우저에서 https://giftgenius-dev.yourdomain.com을 열어 App이 표시되는지 확인합니다.
3단계. ChatGPT Dev Mode에 연결
ChatGPT UI(개발자 섹션)에서:
- GiftGenius Dev를 생성하거나 편집하고;
- URL/Endpoint 필드에 https://giftgenius-dev.yourdomain.com/을 입력하고;
- 저장합니다.
이후 ChatGPT는 /.well-known/openai-app 경로에서 매니페스트를 읽고, 이 도메인을 기준으로 App을 실행합니다.
이제 여러분은 다음을 할 수 있습니다:
- 위젯 코드, MCP 핸들러, 스타일을 수정하고;
- npm run dev를 재시작하고;
- cloudflared tunnel run giftgenius-dev를 재시작해도;
도메인이 동일한 한, 다시는 Dev‑App 설정을 만질 필요가 없습니다.
12. 코드 로직에서의 예시: openExternal
이전 강의에서 작성한 내용을 이어, 위젯에 “브라우저에서 전체 인터페이스 열기” 버튼을 추가해 봅시다. 이 버튼 역시 안정적인 dev‑URL을 사용합니다.
React 위젯 컴포넌트 GiftWidget이 있다고 가정합니다:
// app/components/GiftWidget.tsx
"use client";
import { baseUrl } from "../lib/config"; // env에서 baseUrl을 가져옵니다
export function GiftWidget() {
const handleOpenFull = () => {
window.openai.openExternal({
// 앱 페이지를 새 탭에서 엽니다
url: `${baseUrl}/full`,
label: "전체 인터페이스 열기",
});
};
return (
<div>
<button onClick={handleOpenFull}>
전체 모드
</button>
</div>
);
}
NEXT_PUBLIC_APP_URL이 터널을 가리킨다면:
- 로컬 개발에서는 https://giftgenius-dev.yourdomain.com/full이 열리고;
- staging 배포 후에는 https://giftgenius-staging.vercel.app/full이 열리며;
- prod에서는 실서비스 도메인이 열립니다.
다시 한번 — 도메인의 단일 진실의 원천: 환경만 바꾸고 코드는 바꾸지 않습니다.
13. 미니 전략: “성숙한” 터널을 생각하는 법
간단한 멘탈 모델로 정리하면:
- 터널은 여러분의 노트북과 안정적인 공개 도메인을 잇는 임시 전선일 뿐이며;
- ChatGPT Dev Mode는 도메인만 알고, 코드가 실제로 어디에서 도는지는 상관하지 않고;
- 도메인을 바꿀수록 ChatGPT와 OAuth 제공자 설정을 만지는 시간이 늘고;
- dev 터널은 전체 환경 맵에서 한 줄일 뿐이며, 옆에는 staging(Vercel preview)과 prod(Vercel production)이 있습니다.
다음 강의에서는 이 전선을 Vercel의 본격 호스팅으로 교체하고, Git 브랜치, preview 배포, 프로덕션과 연결하는 방법을 보여 드립니다.
14. “성숙한” 터널에서 흔한 실수
오류 #1: “정적 도메인을 설정해 놓고도 여전히 랜덤 URL을 쓴다”.
가끔 개발자가 giftgenius-dev.yourdomain.com 같은 멋진 도메인을 만들어 놓고도 습관대로 설정 없이 ngrok http 3000을 실행합니다. 그 결과 ChatGPT는 한 도메인을 보고, 코드는 다른 도메인 뒤에서 돕니다. 이미 안정적인 dev‑URL을 만들었다면, 오직 그것만 사용하고(이름 있는 터널/프로필 등) 구성으로 터널을 실행하세요.
오류 #2: localhost:3000을 코드에 하드코딩.
React 컴포넌트나 MCP 처리기에서 fetch("http://localhost:3000/api/...")처럼 쓰는 경우입니다. 로컬에서는 어떨지 몰라도 Dev Mode, 나아가 staging/prod에서는 즉시 깨집니다. 항상 기본 URL을 구성(baseUrl, NEXT_PUBLIC_APP_URL)으로 빼고, 절대 링크가 필요한 모든 곳에서 그것을 사용하세요.
오류 #3: 안정적인 터널 대신 Dev Mode에서 URL을 계속 고친다.
“이번만 URL을 바꾸면 되지 뭐”라는 생각이 들면 경고 신호입니다. ngrok/Cloudflare에서 정적 서브도메인을 설정하는 데 한 번 10~15분이면 충분하며, 개발 과정에서 많은 시간을 절약해 줍니다.
오류 #4: 규칙 없는 팀 공용 정적 도메인.
두 개발자, 하나의 도메인 giftgenius-dev.ngrok-free.app, 그리고 둘 다 원할 때 터널을 올립니다. 결과는 터널 충돌, Dev Mode에서 “원인 모르게” 사라지는 응답, “내 머신에서는 됐는데”식 디버깅입니다. 팀이라면 항상 개인 dev 도메인 또는 실제 호스팅의 하나의 staging 도메인을 사용하세요.
오류 #5: 터널을 “거의 프로덕션”처럼 사용.
“터널로 안정적인 HTTPS‑URL이 있으니, 실제 사용자/결제를 여기로 받자”고 생각하는 경우가 있습니다. 이는 고통의 지름길입니다. 노트북이 꺼지면 앱도 죽고, 인터넷이 끊기면 역시 마찬가지이며, 보안은 기껏해야 형식적입니다. 터널은 dev 도구입니다. 실서비스 트래픽은 Vercel 같은 인프라에서 처리하며, 이에 대해서는 다음 강의에서 다룹니다.
오류 #6: 환경 변수와 Dev Mode 동기화를 잊는다.
NEXT_PUBLIC_APP_URL을 .env.local에서 바꿔 놓고 Dev Mode의 URL 변경을 잊거나(또는 그 반대) 하는 경우가 많습니다. 그 결과 위젯은 한 도메인으로 링크를 만들고, ChatGPT는 다른 도메인으로 접근하게 됩니다. “환경 ↔ 도메인 ↔ ChatGPT의 App”을 간단한 표로 정리해서 변경 시 업데이트하세요 — 무엇이 진짜 URL인지 추측하는 것보다 훨씬 저렴합니다.
GO TO FULL VERSION