블로그기술

모바일 ChatGPT에서 MCP App을 돌려 보며 배운 것

iPhone 시뮬레이터에서 ChatGPT 모바일 웹사이트 안에 악보 패널을 띄워 봤어요. 안전 영역(safe-area) 인셋에 무엇이 들어 있는지, 패널을 다시 만들 때 어떤 일이 생기는지, 그리고 이 둘을 휴대폰 없이 테스트하는 방법을 정리했어요.

글쓴이
게시일
분량
8분 분량

짧은 답

휴대폰에서 ChatGPT는 제목 표시줄과 메시지 입력창을 전체 화면 MCP App 위에 겹쳐 놓고 이를 안전 영역 인셋으로 보고해요. 저희가 돌려 본 바로는 각각 약 47px와 127px이고, 앱이 열려 있는 동안 하단 인셋을 움직여요. 또 전체 화면에 들어가거나 나올 때마다 대화의 원래 도구 결과로 패널을 다시 만들었어요. 인셋만큼은 비워 두되 직접 만든 여유 공간을 더하지 말고, 스크립트로 위치를 잡은 요소는 컨텍스트가 바뀔 때마다 다시 배치하고, 로드할 때 결과가 오래됐는지 확인하세요. 저희는 네이티브 앱이 아니라 iPhone 시뮬레이터의 ChatGPT 모바일 웹사이트에서 테스트했어요.

휴대폰의 ChatGPT에서 MCP App을 열자 무엇이 깨졌나요?

네 가지이고, 모두 전체 화면과 관련 있어요. ScoreStarling의 패널은 채팅 안에 악보를 보여 줘요(연결 방법). 전체 화면에서는 음표를 탭하고, 휴대폰에서 플레이어 위에 붙는 선택 카드로 그 음표에 대해 채팅에 물어볼 수 있어요. 2026년 10월 7일에 iOS 시뮬레이터의 iPhone 17(402 × 874포인트)에서 Safari로 ChatGPT 모바일 웹사이트를 열어 돌려 봤어요. 패널의 프레임은 교차 출처(cross-origin)라서 스크린샷으로 값을 읽었어요.

ChatGPT 모바일 웹사이트에서 발견, 2026년 10월 7일
관찰한 현상원인수정
플레이어가 화면의 약 3분의 1 높이에 떠 있었음메시지 입력창을 이미 덮는 하단 인셋(약 127px) 위에 고정 96px을 더했음인셋과 96px 중 큰 쪽
작성 중인 메시지가 길어지거나 “Thinking” 표시줄이 뜨면 플레이어만 올라가고 붙어 있던 카드는 그대로였음ChatGPT가 인셋을 움직이면 플레이어는 CSS로 따라가지만 카드는 스크립트로 배치함컨텍스트가 바뀔 때마다 카드를 다시 배치
편집한 뒤 전체 화면에 들어가거나 나오면 예전 악보가 읽기 전용으로 보였음ChatGPT가 원래의 도구 결과로 새 패널을 만듦결과가 뒤처져 있으면 현재 버전을 읽기
채팅에서 편집한 뒤 카드가 플레이어 위 약 40px에 떠 있었음저희의 “Updated from the chat”(채팅에서 업데이트됨) 알림이 카드의 위치를 재는 기준 영역을 움직였음그 영역의 크기가 바뀔 때 카드를 배치

휴대폰에서 전체 화면 MCP App이 ChatGPT의 메시지 입력창 뒤에 가려지는 이유는 무엇인가요?

ChatGPT가 메시지 입력창을 내 프레임 위에 그리고, 얼마나 덮는지를 하단 안전 영역 인셋으로 알려 주기 때문이에요. 인셋을 무시하면 컨트롤이 입력창 아래에 놓이고, 저희가 했듯이 그 위에 여유 공간을 따로 더하면 컨트롤이 떠 버려요. MCP Apps 명세는 safeAreaInsets를 “Safe area boundaries in pixels”라고만 설명하고, OpenAI 문서는 전체 화면에서도 메시지 입력창이 위에 겹쳐 있다고 하면서 크기는 알려 주지 않아요(둘 다 2026년 10월 7일 확인). 저희가 말한 47px와 127px은 스크린샷에서 어림한 값이에요.

10월 6일 휴대폰의 대화에서 플레이어가 ChatGPT의 제목 표시줄 아래에 깔려 있는 것을 보고, 헤더를 상단 인셋 아래로 옮기고 플레이어를 아래쪽에 두었어요. 그런데 저희 CSS는 하단 인셋 위에 메시지 입력창용 96px도 따로 남겨 두고 있었어요. 34px 인셋으로 한 로컬 테스트에서는 괜찮아 보였지만, ChatGPT의 127 + 96에서는 아니었어요. 이제 패널은 둘 중 큰 쪽만 비우고, ChatGPT의 window.openai.safeArea도 읽어요:

// panel.js, simplified: the host's inset or 96 px, never both
const bottom = Math.max(context.safeAreaInsets?.bottom || 0, window.openai?.safeArea?.insets?.bottom || 0);
root.style.setProperty('--safe-bottom', `${bottom}px`);
root.style.setProperty('--composer-space', fullscreen ? `${Math.max(0, 96 - bottom)}px` : '0px');
/* panel.css */
.stage > .player { bottom: calc(10px + var(--safe-bottom) + var(--composer-space)); }
402 × 874포인트 휴대폰에서의 저희 패널이에요. 대략 실제 비율로 그렸고, ChatGPT의 제목 표시줄(약 47px)과 메시지 입력창(약 127px)이 그 위에 겹친 모습이에요. 수정 전에는 인셋 위로 96px를 더 비웠고, 지금은 인셋만큼, 호스트가 그보다 작게 보고하면 96px를 비워요.

휴대폰에서 전체 화면 MCP App이 열려 있는 동안 무엇이 바뀌나요?

인셋이 바뀌어요. 저희가 돌려 본 바로는 작성 중인 메시지가 두 줄로 늘어날 때, 그리고 “Thinking” 표시줄이 메시지 입력창을 대신할 때 하단 가장자리가 움직였어요. 명세는 호스트가 컨텍스트 필드가 바뀔 때마다 바뀐 필드만 담아 ui/notifications/host-context-changed를 보내도록 허용하고, 뷰는 그것을 병합해요.

저희 플레이어는 CSS 변수에 걸려 있어서 따라 움직였어요. 선택 카드는 스크립트로 배치하기 때문에 뒤처졌고, 그래서 이제는 컨텍스트가 바뀔 때마다 다시 배치해요. 저희 알림 때문에 두 번째 어긋남도 생겼어요. “Updated from the chat”이 20초 동안 악보 위에 표시되면서 악보 영역을 아래로 밀어요. 그 영역의 윗부분을 기준으로 위치를 재는 카드는 플레이어 위로 미끄러져 들어갔고, 그동안 다른 무언가가 카드를 다시 배치했다면 알림이 사라진 뒤 플레이어 위에 떠 버렸어요. 이제는 그 영역에 건 ResizeObserver도 카드를 배치하며, 애니메이션 프레임당 한 번만 해요:

this.context = {...this.context, ...params};  // host.js, simplified: merge the partial update
function hostContext(next) { applyContext(next); editor.queuePlace(); }
new ResizeObserver(() => { updateClip(); editor.queuePlace(); }).observe($('stage'));

전체 화면을 나오면 MCP App에 예전 버전이 보이는 이유는 무엇인가요?

휴대폰의 ChatGPT가 전체 화면에 들어가거나 나올 때마다 새 패널을 만들고, 편집이 일어나기 전에 만들어진 대화의 원래 도구 결과를 거기에 넘겼기 때문이에요. 명세는 호스트가 언제든 뷰를 없앨 수 있도록 허용하고, 도구 결과는 도구가 실행된 순간만 기록해요.

저희 패널은 반은 스냅샷이고 반은 라이브였어요. 자기 리비전의 페이지 이미지와 음표는 결과에 들어 있고, 현재 리비전 필드는 패널이 가져올 때 읽어요. 다시 만들어진 패널은 리비전 0을 그렸다가 악보가 리비전 3인 것을 보고 읽기 전용이 됐어요. 채팅에서의 편집을 확인하는 로직은 서버의 리비전을 이미 현재인 그 필드와 비교했기 때문에 따라잡지 못했어요. 이제는 호스트가 악보보다 뒤처진 결과를 넘긴 패널이 현재 리비전을 한 번 읽어요. 다만 결과에 승인을 기다리는 제안 변경이 들어 있으면, 원래 악보와 달라야 하므로 읽지 않아요.

휴대폰 없이 ChatGPT의 휴대폰 동작을 테스트하려면 어떻게 하나요?

레플리카로 실제 호스트를 흉내 내고, 각 테스트가 수정 없이는 실패한다는 것을 증명하세요. 저희는 Playwright 테스트용으로 ChatGPT와 Claude를 복제해 주는 sunpeak을 써요. sunpeak의 모바일 ChatGPT 셸(버전 0.20.91)은 제목 표시줄과 메시지 입력창을 그리고, 입력창을 92px 인셋으로 보고해요. 이 버그들을 위해 저희가 쓴 테스트 세 개가 402 × 874 터치스크린에서 나머지를 채워요. 실제 ChatGPT의 프레임처럼 패널의 부모인 셸의 샌드박스 프레임을 통해 127, 160, 70, 127px의 새 인셋을 보내고, 편집 뒤에는 패널을 다시 불러와 원래의 도구 결과를 넘겨요. 다른 테스트 하나는 채팅이 하듯 패널 바깥에서 악보를 편집해요. 플레이어는 인셋과 96px 중 큰 쪽보다 0~24px 위에, 카드는 플레이어보다 0~16px 위에 놓여야 해요.

  1. 실제 호스트 실행시뮬레이터에서 ChatGPT 모바일 사이트
  2. 원인 하나 수정배포하고 앱의 도구를 새로고침한 뒤 다시 확인
  3. 호스트 흉내 내기레플리카의 프레임에서 호스트의 메시지를 보냄
  4. 수정 되돌리기새 테스트는 수정이 없으면 실패해야 함
휴대폰에서 발견한 문제가 로컬 테스트가 되는 과정이에요.
수정을 하나씩 되돌린 결과, 2026년 10월 7일
되돌린 수정실패한 테스트
인셋 또는 96px, 둘을 합치지 않음메시지 입력창 테스트: “player close to the message box”
컨텍스트가 바뀔 때 카드 배치메시지 입력창 테스트: “card clear of the player”
다시 만들어진 패널에 현재 버전 적용다시 만들기 테스트: 실행 취소가 비활성 상태로 남음
영역 크기가 바뀔 때 카드 배치알림 테스트: “card docked on the player”

이 세 개를 한 번 돌리는 데 59.9초가 걸렸어요. 같은 날 추가한 네 번째 테스트는 휴대폰 편집기의 한 줄짜리 헤더와 플레이어를 확인하고, 저희 기여자 규칙은 이제 전체 화면 레이아웃, 인셋, 카드, 결과 처리를 바꾼 뒤에는 휴대폰 테스트를 요구해요. 휴대폰에서 돌려 보니 ChatGPT가 “이 음을 C로 바꿔 주세요”라는 요청에 선택된 B4를 7도 아래의 C4로 바꾸는 문제도 잡혔어요. 도구 설명에 가장 가까운 옥타브를 고르라는 규칙을 넣자, 실제 모델로 하는 로컬 테스트의 통과가 3번 중 2번에서 6번 중 6번이 됐어요.

아직 확인하지 못한 것

저희의 휴대폰 확인은 iOS 시뮬레이터와 ChatGPT 모바일 웹사이트에서 이뤄졌어요. 실제 휴대폰도, 네이티브 앱도 아니에요. 아직 남은 것은 이래요:

  • ChatGPT가 보내는 정확한 인셋과 그 시점. 저희 값은 어림한 값이고 레플리카가 그것을 재사용해요;
  • Safari의 확장된 도구 막대. 메시지 입력창과 플레이어의 아래쪽 가장자리를 반쯤 가려요;
  • 키보드가 닫힌 뒤 Safari가 남긴 약 156포인트의 페이지 스크롤. 교차 출처 패널은 되돌릴 수 없어요;
  • ChatGPT가 아직 답하는 중에 전체 화면을 열었을 때 생기는 최대 약 777포인트의 불투명한 띠;
  • ChatGPT의 답변 시트와 자체 iframe 해체. 레플리카는 이를 흉내 내지 않아요;
  • 컴퓨터의 ChatGPT. 전체 화면이 옆 패널이고 메시지 입력창은 그 바깥에 있어요. 그 인셋은 읽지 않았어요.

휴대폰용 MCP App 체크리스트

  • 두 인셋을 모두 비워 두세요. 메시지 입력창용 대비책을 따로 둔다면 둘 중 큰 쪽을 쓰고, 합치지는 마세요.
  • host-context-changed 알림을 받을 때마다 병합하고, 스크립트로 위치를 잡은 요소는 프레임당 한 번 다시 배치하세요.
  • 알림 때문에 움직이는 자기 컨테이너의 크기 변화를 관찰하세요.
  • 예전 도구 결과로 다시 만들어질 수 있다고 보고, 로드할 때 그 결과의 버전을 서버의 버전과 비교하세요.
  • 로컬 테스트 호스트에는 실제 호스트에서 잰 인셋을 주세요.
  • OpenAI 문서의 권고대로 UI 리소스 URI를 캐시 키로 다루고, 새 URI를 게시한 뒤에는 ChatGPT에서 앱의 도구를 새로고침하세요(플러그인 설정, “Manage app”(앱 관리), “Refresh tools”(도구 새로고침), 2026년 10월 7일 확인).
  • 실제 호스트에서 발견한 문제는 하나하나, 수정 없이는 실패하는 레플리카 테스트로 만드세요.

출처

  1. SEP-1865: MCP Apps: Interactive User Interfaces for MCP (stable, 2026-01-26), Model Context Protocol
  2. Add UI to your MCP server, OpenAI
  3. UI guidelines, OpenAI
  4. Reference(window.openai 컴포넌트 브리지), OpenAI
  5. MCP Testing Framework, sunpeak

질문과 답변

휴대폰에서 ChatGPT는 메시지 입력창을 safeAreaInsets에 포함하나요?

저희가 돌려 본 바로는 포함해요. ChatGPT 모바일 웹사이트(iPhone 17 시뮬레이터, 2026년 10월 7일)에서 하단 인셋은 약 127px로 메시지 입력창을 덮었고, 상단 인셋은 약 47px로 제목 표시줄을 덮었어요. MCP Apps 명세는 인셋에 무엇이 들어 있는지 말하지 않아요. 그러니 보고된 만큼의 공간을 비워 두고, 그 위에 직접 만든 여유 공간을 더하지 마세요.

전체 화면 앱이 열려 있는 동안 ChatGPT가 인셋을 바꾸나요?

저희가 돌려 본 바로는 바꿔요. 두 줄로 늘어난 작성 중인 메시지와 ChatGPT의 “Thinking” 표시줄이 둘 다 하단 가장자리를 움직였어요. ui/notifications/host-context-changed가 올 때마다 새 인셋을 적용하되, 알림에 담긴 필드를 갖고 있던 값에 병합하세요. 그리고 스크립트로 위치를 잡은 요소는 다시 배치하세요.

휴대폰에서 전체 화면으로 바꾸면 MCP App에 왜 예전 데이터가 보이나요?

휴대폰의 ChatGPT는 전체 화면에 들어가거나 나올 때마다 대화의 원래 도구 결과로 새 패널을 만들었어요. 그래서 저희 패널은 이후의 편집이 반영되기 전의 악보를 보여 줬어요. 뷰가 로드될 때 결과의 버전을 서버의 버전과 비교하고, 결과가 뒤처져 있으면 현재 상태를 한 번 읽으세요.

휴대폰 없이 ChatGPT의 휴대폰 레이아웃을 테스트할 수 있나요?

일부는 가능해요. 휴대폰에서 본 장면을 Playwright와 sunpeak의 모바일 ChatGPT 셸로 재현해요. 움직이는 인셋, 다시 만들어진 패널, 채팅에서 한 편집이고, 각 테스트는 수정을 되돌리면 실패해요. ChatGPT가 실제로 보내는 값, Safari의 도구 막대, 키보드, ChatGPT의 답변 시트, 네이티브 앱은 여전히 실제 휴대폰이 필요해요.

어떤 음악이든 가져오세요 악보를 챙겨 가세요