Skip to main content
Summary

위젯을 앱으로 만드는 연결 지점

이전 장까지의 위젯은 주로 "도구 결과를 보기 좋게 보여 주는 화면"에 가까웠습니다. 하지만 실제 ChatGPT 앱에서 위젯은 단순한 결과 카드로 끝나지 않습니다. 사용자는 카드 안에서 필터를 바꾸고, 항목을 선택하고, 새 데이터를 다시 조회하고, 외부 페이지로 이동하고, 이어지는 질문까지 만들 수 있습니다.

이 흐름의 중심에 window.openai가 있습니다. ChatGPT는 샌드박스 iframe 안에서 실행되는 위젯에 window.openai라는 브릿지를 제공합니다. 공식 Reference에서도 이를 Apps SDK 호환 계층이자 ChatGPT 확장 기능을 제공하는 인터페이스로 설명합니다.1 쉽게 말해 window.openai는 위젯과 ChatGPT 호스트 사이의 통신 창구입니다.

구조는 이렇게 볼 수 있습니다.

  1. 사용자가 ChatGPT에 요청합니다.

  2. 모델이 MCP 도구를 선택하고 입력값을 만듭니다.

  3. MCP 서버가 도구를 실행하고 structuredContent와 _meta를 반환합니다.

  4. ChatGPT가 위젯 리소스를 iframe 안에 렌더링합니다.

  5. 위젯은 window.openai를 통해 입력, 결과, 테마, 표시 모드, 저장된 UI 상태를 읽습니다.

  6. 필요하면 위젯이 다시 도구를 호출하거나, 표시 모드 변경을 요청하거나, 후속 메시지를 보냅니다.

여기서 중요한 점은 위젯이 MCP 서버에 직접 마음대로 붙는 독립 웹앱이 아니라는 것입니다. 위젯은 ChatGPT 호스트가 관리하는 대화 흐름 안에서 동작합니다. OpenAI의 UI 문서도 컴포넌트가 ChatGPT의 iframe 안에서 실행되고, 호스트와는 MCP Apps 브릿지를 통해 통신한다고 설명합니다.2

window.openai 브릿지와 호스트 API 활용

호스트 상태 읽기: 처음 렌더링의 재료

위젯이 처음 열릴 때 가장 먼저 확인할 값은 toolInput과 toolOutput입니다.

toolInput은 도구가 호출될 때 모델이 만든 입력값입니다. 사용자가 직접 입력한 문장 전체가 아니라, JSON Schema에 맞게 정리된 도구 인자입니다. 예를 들어 사용자가 "3만 원 이하 무선 마우스 추천해 줘"라고 말했다면 toolInput에는 다음처럼 정리된 값이 들어올 수 있습니다.

{
  query: "무선 마우스",
  maxPrice: 30000
}

toolOutput은 MCP 서버가 반환한 구조화된 결과입니다. 이전 장에서 다룬 structuredContent를 위젯이 읽는다고 이해하면 쉽습니다. 화면의 카드 목록, 차트 데이터, 요약 통계, 선택 가능한 옵션은 보통 이 값에서 나옵니다. 구현이나 어댑터에 따라 toolOutput 자체가 구조화 데이터일 수도 있고, 방어적으로 structuredContent 속성을 한 번 더 확인하는 코드가 쓰일 수도 있습니다. 중요한 것은 위젯이 자연어 문장을 파싱하지 않고 구조화된 객체를 기준으로 렌더링해야 한다는 점입니다.

const openai = window.openai;

const input = openai?.toolInput ?? {};
const output = openai?.toolOutput?.structuredContent ?? openai?.toolOutput ?? {};
const savedState = openai?.widgetState ?? {};

displayMode는 위젯이 현재 어떤 크기와 맥락에서 보이는지를 알려 줍니다. 인라인 카드인지, 더 넓은 영역인지, 전체 화면에 가까운 모드인지에 따라 UI 밀도를 조절할 수 있습니다. 인라인에서는 핵심 3개만 보여 주고, 확장 모드에서는 필터와 상세 표를 함께 보여 주는 식입니다.

theme은 ChatGPT의 현재 테마입니다. 라이트 모드와 다크 모드에 맞춰 배경, 글자색, 테두리, 그림자 강도를 바꿀 때 사용합니다. 위젯이 자체 디자인을 강하게 고집하면 ChatGPT 안에서 낯선 웹페이지처럼 보입니다. 반대로 theme에 반응하면 사용자는 같은 대화 환경 안에서 앱을 쓰고 있다고 느낍니다.

widgetState는 이전에 저장해 둔 위젯 UI 상태입니다. 예를 들어 사용자가 "가격 낮은 순" 정렬을 선택했고 세 번째 상품을 펼쳐 보았다면, 이런 정보는 서버 결과라기보다 UI 상태입니다. 이 값은 setWidgetState로 저장하고, 다음 렌더링에서 widgetState로 복원합니다. OpenAI 예제 저장소의 kitchen-sink-lite 예제도 toolInput, toolOutput, displayMode, theme, widgetState를 함께 읽고, setWidgetState, callTool, requestDisplayMode, openExternal, sendFollowUpMessage 같은 호스트 API를 함께 보여 주는 참고용 예제입니다.3

setWidgetState로 UI 상태 유지하기

위젯 상태를 저장해야 하는 대표적인 값은 다음과 같습니다.

  • 사용자가 선택한 탭

  • 검색어나 필터 값

  • 정렬 방식

  • 펼쳐 본 카드 ID

  • 폼에 입력 중인 임시 값

  • 현재 선택한 상품, 강좌, 문서, 장소

이 값들을 모두 서버에 저장할 필요는 없습니다. 하지만 사용자가 대화를 이어 가는 동안 위젯이 다시 마운트되거나, 새 도구 결과와 함께 화면이 갱신될 수 있습니다. 이때 UI 상태가 사라지면 사용자는 방금 하던 일을 잃어버린 것처럼 느낍니다.

setWidgetState는 이런 불편을 줄이는 장치입니다.

function ProductWidget() {
  const openai = window.openai;

  const initialState = openai?.widgetState ?? {
    sort: "recommended",
    selectedProductId: null,
    filterText: ""
  };

  const [viewState, setViewState] = useState(initialState);

  function updateViewState(patch) {
    const next = { ...viewState, ...patch };
    setViewState(next);
    openai?.setWidgetState?.(next);
  }

  return (
    <select
      value={viewState.sort}
      onChange={(event) => updateViewState({ sort: event.target.value })}
    >
      <option value="recommended">추천순</option>
      <option value="price_low">낮은 가격순</option>
    </select>
  );
}

상태 저장에서 지켜야 할 원칙은 단순합니다. 작게 저장하고, 복원 가능한 값만 저장합니다. 토큰, 비밀번호, 결제 정보, 원본 개인정보처럼 민감한 값은 widgetState에 넣지 않습니다. 또한 대용량 목록 전체를 저장하기보다 선택한 ID, 필터 조건, 스크롤 위치처럼 화면 복원에 필요한 최소 정보만 저장하는 편이 좋습니다.

widgetState는 서버 데이터베이스가 아닙니다. 위젯이 대화 안에서 자연스럽게 이어지도록 돕는 UI 메모리입니다.

callTool로 위젯에서 다시 도구 호출하기

사용자가 위젯 안의 버튼을 누를 때마다 프런트엔드 안에서만 처리할 수는 없습니다. 새 검색, 재계산, 장바구니 변경, 예약 가능 시간 확인처럼 서버 로직이 필요한 행동은 다시 MCP 도구 호출로 연결해야 합니다. 이때 사용하는 API가 callTool입니다.

예를 들어 상품 목록 위젯에서 사용자가 가격 필터를 바꾸고 "다시 검색"을 누른다고 해 보겠습니다.

async function refreshProducts() {
  const result = await window.openai.callTool("search_products", {
    query: viewState.filterText,
    sort: viewState.sort,
    maxPrice: viewState.maxPrice
  });

  const output = result?.structuredContent ?? result?.toolOutput ?? result ?? {};
  setProducts(output.items ?? []);
}

이 패턴의 장점은 클릭 이벤트가 단순한 클라이언트 이벤트로 끝나지 않는다는 점입니다. 도구 이름과 입력값은 MCP 계약을 따릅니다. 서버는 같은 JSON Schema 기준으로 값을 검증합니다. 결과도 다시 구조화된 출력으로 돌아옵니다. 따라서 모델, 서버, UI가 같은 작업 단위를 공유할 수 있습니다.

단, 모든 버튼을 callTool로 만들 필요는 없습니다. 카드 접기, 탭 전환, 로컬 정렬처럼 이미 받은 데이터 안에서 끝나는 조작은 위젯 내부 상태로 처리하면 됩니다. 반대로 외부 API 조회, 쓰기 작업, 인증이 필요한 작업, 모델이 후속 대화에서 알아야 할 작업은 도구로 분리하는 편이 안전합니다.

또 하나의 실무 포인트가 있습니다. 위젯에서 호출될 도구는 애초에 "위젯에서 접근 가능한 도구"로 설계되어야 합니다. 도구 설명, 입력 스키마, 권한, 쓰기 작업의 확인 절차가 준비되지 않은 상태에서 UI 버튼만 먼저 만들면 런타임에서 실패하거나, 사용자가 어떤 일이 일어나는지 이해하지 못합니다.

requestDisplayMode는 요청이지 명령이 아니다

작은 인라인 카드로 충분한 UI가 있고, 더 넓은 화면이 필요한 UI가 있습니다. 지도, 비교표, 캘린더, 편집기, 결제 전 확인 화면은 인라인 카드 안에 억지로 넣으면 사용성이 나빠집니다. 이럴 때 위젯은 requestDisplayMode로 더 적절한 표시 방식을 요청할 수 있습니다.

async function openLargeView() {
  try {
    const result = await window.openai.requestDisplayMode?.({ mode: "fullscreen" });
    const grantedMode = result?.mode ?? window.openai.displayMode;
    console.log("granted display mode", grantedMode);
  } catch {
    // 호스트가 지원하지 않아도 인라인 UI는 계속 사용할 수 있어야 한다.
  }
}

여기서 핵심은 "요청"이라는 점입니다. 최종 표시 방식은 ChatGPT 호스트가 결정합니다. 사용자의 기기, 화면 크기, 현재 대화 맥락, 런타임 지원 여부에 따라 요청이 거절되거나 다른 모드로 조정될 수 있습니다. 예를 들어 PiP를 요청해도 모바일에서는 전체 화면으로 바뀌거나, 호스트 정책에 따라 인라인 상태가 유지될 수 있습니다. 따라서 위젯은 확장 모드가 되면 더 풍부하게 보여 주되, 확장되지 않아도 핵심 작업은 인라인에서 가능해야 합니다.

좋은 기준은 다음과 같습니다.

  • 인라인: 요약, 대표 결과, 빠른 선택

  • 확장/전체 화면: 상세 비교, 긴 폼, 지도, 편집, 다단계 작업

  • PiP: 진행 상태, 타이머, 라이브 추적처럼 작게 떠 있어야 하는 정보

  • 실패 시: 같은 작업을 좁은 UI에서도 완료할 수 있게 유지

표시 모드는 UX 향상을 위한 선택지이지, 기능 동작의 필수 조건이 되어서는 안 됩니다.

openExternal과 sendFollowUpMessage로 흐름 이어가기

모든 일을 ChatGPT 안에서 끝낼 수는 없습니다. 로그인, 결제, 약관 확인, 원문 문서 열람, 외부 대시보드 이동처럼 브라우저가 필요한 작업이 있습니다. 이때 위젯은 openExternal을 사용할 수 있습니다.

function openDocs() {
  window.openai.openExternal?.({
    href: "https://example.com/docs/order-123"
  });
}

직접 window.location을 바꾸거나 새 창을 강제로 여는 방식보다 호스트 API를 쓰는 편이 낫습니다. ChatGPT가 외부 이동을 더 안전하고 일관된 방식으로 처리할 수 있기 때문입니다. 특히 인증, 결제, 개인정보가 걸린 페이지는 사용자가 어디로 이동하는지 명확히 보여 주어야 합니다.

sendFollowUpMessage는 위젯 조작을 다음 대화로 이어 주는 API입니다. 예를 들어 사용자가 비교표에서 세 상품을 선택한 뒤 "이 셋만 비교해 줘" 버튼을 누르면, 위젯이 후속 메시지를 만들 수 있습니다.

function compareSelected() {
  const names = selectedProducts.map((item) => item.name).join(", ");

  window.openai.sendFollowUpMessage?.({
    message: `선택한 상품 ${names}만 기준으로 장단점과 추천 대상을 비교해 주세요.`
  });
}

이 기능은 강력하지만 남용하면 안 됩니다. 사용자가 명확히 누른 버튼에 반응해야 하고, 메시지 내용은 사용자의 의도를 정직하게 반영해야 합니다. 위젯이 몰래 대화를 진행하거나, 사용자가 원하지 않은 구매·예약·삭제 같은 행동으로 이어지게 해서는 안 됩니다.

표준 브릿지와 ChatGPT 확장 기능을 구분하기

Apps SDK는 MCP라는 공통 구조 위에서 동작하면서, ChatGPT 안에서 더 자연스러운 앱 경험을 만들기 위한 전용 메타데이터와 호스트 API도 제공합니다. 새 UI를 만들 때는 먼저 MCP 도구 계약과 structuredContent를 안정적으로 설계하고, ChatGPT에서 필요한 위젯 연결과 호스트 동작은 Apps SDK의 openai/* 메타데이터와 window.openai API로 처리하는 식으로 나누면 이해하기 쉽습니다.4

실무적으로는 이렇게 나누면 됩니다.

  • 도구 계약: MCP 도구 이름, 설명, JSON Schema로 정의

  • 초기 데이터: structuredContent와 필요한 _meta로 전달

  • 화면 렌더링: 위젯 리소스와 UI 컴포넌트가 담당

  • 호스트 상태 읽기: toolInput, toolOutput, displayMode, theme, widgetState

  • UI 상태 저장: setWidgetState

  • 추가 실행: callTool

  • 호스트 동작 요청: requestDisplayMode, openExternal, sendFollowUpMessage

이 구분이 잡히면 위젯 코드는 훨씬 단순해집니다. 서버 데이터와 UI 상태가 섞이지 않고, 로컬 이벤트와 도구 실행이 섞이지 않으며, ChatGPT가 통제해야 할 영역과 위젯이 처리해도 되는 영역이 분리됩니다.

구현할 때의 작은 체크리스트

window.openai를 사용할 때는 다음 질문을 먼저 확인하면 좋습니다.

  • 처음 화면은 toolOutput만으로 그릴 수 있는가?

  • 사용자가 바꾼 필터와 선택값은 setWidgetState로 복원되는가?

  • 서버 실행이 필요한 버튼만 callTool로 연결했는가?

  • callTool 입력값은 도구의 JSON Schema와 맞는가?

  • 인라인 모드에서도 핵심 작업을 끝낼 수 있는가?

  • requestDisplayMode가 거절되거나 다른 모드로 조정될 때도 동작하는가?

  • 외부 이동은 openExternal로 처리하는가?

  • 후속 메시지는 사용자가 명확히 의도한 경우에만 보내는가?

  • widgetState나 위젯 props에 민감한 값을 넣지 않았는가?

window.openai를 이해하면 위젯은 더 이상 정적인 결과 화면이 아닙니다. ChatGPT의 대화 상태를 읽고, 사용자의 조작을 기억하고, 필요한 도구를 다시 실행하고, 다음 대화로 자연스럽게 이어지는 인터랙티브 앱이 됩니다. Apps SDK의 진짜 힘은 바로 이 지점에서 드러납니다.

참고

1Apps SDK Reference

2Build your ChatGPT UI

3OpenAI Apps SDK Examples

4MCP Apps compatibility in ChatGPT

책 목록

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

전체 목차 보기

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

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

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

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

참여하기