위젯 UI와 Apps SDK UI 컴포넌트 만들기
위젯은 도구 결과의 화면이다
Apps SDK의 가장 큰 장점은 ChatGPT 답변 안에 조작 가능한 UI를 넣을 수 있다는 점입니다. 이전 장에서 도구를 "모델과 서버 사이의 계약"으로 보았다면, 이번 장의 위젯은 그 계약을 사용자가 직접 보고 만지는 화면입니다.
사용자가 "근처 피자집 추천해 줘"라고 말했을 때 텍스트로 열 줄을 나열할 수도 있습니다. 하지만 위젯을 사용하면 카드 목록, 평점 배지, 배달 시간, 지도 버튼, 예약 폼을 한 화면에 보여줄 수 있습니다. 사용자는 다시 긴 문장을 입력하지 않아도 후보를 비교하고, 상세 정보를 열고, 선택을 확정할 수 있습니다.
Apps SDK 위젯은 MCP 도구와 같은 서버 코드 안에 섞여 실행되지 않습니다. 도구는 서버에서 실행되고, 위젯은 별도로 번들링된 HTML·CSS·JavaScript 리소스로 ChatGPT 안의 샌드박스 iframe에서 렌더링됩니다. 샌드박스 iframe은 외부 페이지처럼 마음대로 ChatGPT 전체에 접근하지 못하는 격리된 실행 영역입니다. 이 구조 덕분에 앱은 카드, 리스트, 폼, 지도 같은 풍부한 UI를 제공하면서도 대화 환경과 분리된 안전한 공간에서 동작합니다.1
중요한 원칙은 단순합니다.
도구는 데이터를 가져오거나 변경한다.
structuredContent는 모델과 위젯이 함께 읽을 핵심 데이터를 담는다.위젯은 그 데이터를 사용자가 이해하고 조작하기 쉬운 화면으로 바꾼다.
_meta는 어떤 UI 리소스를 렌더링할지, 화면에 필요한 보조 정보가 무엇인지 알려준다.
즉, 위젯은 "예쁜 응답"이 아니라 도구 결과의 사용자 인터페이스입니다.

도구와 위젯을 연결하는 outputTemplate
Apps SDK에서 특정 도구 호출 결과를 특정 위젯 화면과 연결하려면 도구 정의의 _meta["openai/outputTemplate"]에 ui://... 형태의 UI 리소스 URI를 지정합니다. ChatGPT는 도구가 호출된 뒤 이 URI가 가리키는 리소스를 가져와 iframe 안에 렌더링합니다.2
예를 들어 식당 검색 도구가 식당 목록 위젯을 띄우게 하려면 다음처럼 설계할 수 있습니다.
const RESTAURANT_LIST_UI = "ui://widgets/restaurant-list.html";
server.registerTool(
"search_restaurants",
{
title: "식당 검색",
description: "지역, 음식 종류, 가격대를 기준으로 식당 후보를 찾습니다.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "음식 또는 식당 키워드" },
location: { type: "string", description: "검색 기준 위치" }
},
required: ["query", "location"],
additionalProperties: false
},
_meta: {
"openai/outputTemplate": RESTAURANT_LIST_UI,
"openai/toolInvocation/invoking": "식당을 찾는 중입니다…",
"openai/toolInvocation/invoked": "식당 후보를 찾았습니다."
}
},
async ({ query, location }) => {
const restaurants = await searchRestaurants({ query, location });
return {
structuredContent: {
restaurants: restaurants.map((r) => ({
id: r.id,
name: r.name,
rating: r.rating,
deliveryTimeMinutes: r.deliveryTimeMinutes,
priceLevel: r.priceLevel
}))
},
content: [
{
type: "text",
text: `${location} 근처에서 ${restaurants.length}개의 후보를 찾았습니다.`
}
]
};
}
);여기서 도구 결과는 화면을 직접 그리지 않습니다. 도구는 목록 데이터를 반환하고, openai/outputTemplate은 그 데이터를 어떤 위젯으로 보여줄지 알려줍니다. 이 분리가 중요합니다. 같은 restaurants 데이터라도 처음에는 리스트로 보여주고, 나중에는 지도형 위젯이나 예약 폼으로 확장할 수 있기 때문입니다.
UI 리소스는 별도 번들로 준비합니다. React와 Vite를 쓴다면 RestaurantList.tsx를 빌드해 HTML 템플릿으로 제공하고, MCP 리소스 등록 단계에서 ui://widgets/restaurant-list.html 같은 URI로 노출합니다. 실제 파일 경로를 사용자에게 보여주는 것이 아니라, ChatGPT와 MCP 서버가 이해하는 리소스 식별자를 제공한다고 생각하면 됩니다.
카드형 UI: 한눈에 판단하게 만들기
카드형 위젯은 Apps SDK에서 가장 자주 쓰는 패턴입니다. 식당, 상품, 강좌, 예약, 문서, 작업 티켓처럼 "하나의 대상"을 요약해 보여줄 때 적합합니다.
좋은 카드는 세 가지 질문에 바로 답해야 합니다.
이것이 무엇인가?
왜 추천되었는가?
사용자가 다음에 무엇을 할 수 있는가?
예를 들어 식당 카드는 이름만 크게 보여주면 부족합니다. 평점, 거리, 예상 시간, 가격대, 주요 액션이 함께 있어야 합니다.
function RestaurantCard({ restaurant }: { restaurant: Restaurant }) {
return (
<article className="rounded-2xl border border-border bg-surface p-4 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm text-secondary">추천 식당</p>
<h2 className="text-lg font-semibold">{restaurant.name}</h2>
</div>
<Badge color="success">★ {restaurant.rating}</Badge>
</div>
<p className="mt-2 text-sm text-secondary">
예상 배달 {restaurant.deliveryTimeMinutes}분 · {restaurant.priceLevel}
</p>
<div className="mt-4 grid grid-cols-2 gap-2">
<Button variant="soft" color="secondary">상세 보기</Button>
<Button color="primary">선택하기</Button>
</div>
</article>
);
}카드의 핵심은 정보 밀도입니다. 대화창 안의 위젯은 일반 웹페이지보다 공간이 좁습니다. 모든 정보를 한 카드에 넣으려 하지 말고, 판단에 필요한 값만 먼저 보여주세요. 긴 설명, 리뷰 목록, 상세 지도는 사용자가 카드를 선택한 뒤 확장하는 편이 좋습니다.
리스트와 캐러셀: 후보를 탐색하게 만들기
후보가 여러 개라면 카드 하나보다 리스트나 캐러셀이 낫습니다. 리스트는 비교에 강합니다. 사용자가 가격, 상태, 날짜, 우선순위를 빠르게 훑어야 할 때 좋습니다. 캐러셀은 이미지나 대표 카드가 중요할 때 유용합니다.
리스트형 위젯을 만들 때는 각 행의 구조를 일정하게 유지하세요. 첫 번째 행은 이미지가 있고 두 번째 행은 없는 식으로 흔들리면 사용자는 비교하기 어렵습니다. 가능하면 다음 구조를 반복합니다.
왼쪽: 이름 또는 대표 이미지
가운데: 핵심 설명과 보조 정보
오른쪽: 상태 배지 또는 주요 버튼
예를 들어 강좌 추천 앱이라면 각 행에 강좌명, 난이도, 예상 학습 시간, 등록 버튼을 둡니다. 장바구니라면 상품명, 수량, 가격, 수량 변경 버튼을 둡니다. Apps SDK 예제 저장소도 리스트, 캐러셀, 지도, 쇼핑 흐름 같은 여러 위젯 패턴을 예제로 제공합니다.3
리스트에서 중요한 것은 "다음 행동"입니다. 사용자가 항목을 고르면 무엇이 일어나는지 명확해야 합니다. 상세 보기, 비교에 추가, 예약하기, 장바구니 담기처럼 동사형 버튼을 사용하세요. 확인, 선택, 실행처럼 맥락 없는 버튼은 피하는 편이 좋습니다.
폼형 위젯: 대화보다 빠르게 입력받기
대화는 유연하지만, 모든 입력에 적합하지는 않습니다. 날짜, 인원, 주소, 옵션, 수량처럼 정확해야 하는 값은 폼이 더 안전합니다.
예를 들어 사용자가 "토요일 저녁에 예약해 줘"라고 말했지만 시간이 빠져 있다면, ChatGPT가 질문을 이어갈 수도 있습니다. 그러나 예약 위젯을 띄워 날짜, 시간, 인원, 요청 사항을 한 번에 수정하게 하면 더 빠릅니다.
폼형 위젯을 설계할 때는 다음 원칙을 지키세요.
사용자가 이미 말한 값은 미리 채운다.
필수값과 선택값을 구분한다.
제출 전에 요약을 보여준다.
실제 변경 작업은 확인 버튼 뒤에 둔다.
서버로 보내는 값은 다시 검증한다.
위젯에서 받은 입력은 신뢰하면 안 됩니다. 사용자가 UI를 조작할 수 있고, 클라이언트 값은 언제든 변조될 수 있습니다. 폼은 사용자 경험을 돕는 장치이고, 최종 검증은 서버 도구가 해야 합니다.
좋은 흐름은 다음과 같습니다.
사용자: 내일 저녁 2명 예약해 줘
→ search_restaurants 호출
→ 식당 리스트 위젯 표시
→ 사용자가 식당 선택
→ 예약 폼 위젯 표시
→ create_reservation_preview 호출
→ 사용자가 확인
→ confirm_reservation 호출이렇게 미리보기와 확정을 나누면 사용자가 실수로 예약을 만들 가능성을 줄일 수 있습니다.
지도형·차트형 위젯: 텍스트가 약한 영역을 맡기기
지도와 차트는 텍스트 답변보다 위젯이 훨씬 강한 영역입니다. "가까운 순서대로 5곳"은 텍스트로도 설명할 수 있지만, 지도 위에 핀으로 표시하면 사용자는 위치 관계를 즉시 이해합니다. "월별 매출 추이"도 숫자 목록보다 선 그래프가 빠릅니다.
다만 지도형 위젯은 과하게 만들기 쉽습니다. 대화창 안에서는 전체 지도 앱을 재현하려 하지 마세요. 기본 화면에서는 현재 선택과 주변 후보만 보여주고, 필요한 경우 fullscreen으로 확장하게 만드는 편이 좋습니다. Apps SDK의 UI 컴포넌트는 사용자가 인라인으로 데이터를 확인하거나 수정하고, 필요할 때 더 넓은 화면으로 전환하는 경험을 전제로 설계됩니다.4
지도형 위젯에서 꼭 필요한 요소는 다음과 같습니다.
현재 기준 위치
후보 핀 또는 영역 표시
선택된 항목의 요약 카드
길찾기, 상세 보기 같은 명확한 액션
지도 로딩 실패 시 대체 리스트
차트형 위젯도 마찬가지입니다. 축, 단위, 기간, 필터 상태를 숨기지 마세요. 모델이 "지난달보다 12% 증가했습니다"라고 말하더라도, 사용자는 차트에서 그 기준이 무엇인지 확인할 수 있어야 합니다.
Apps SDK UI로 ChatGPT와 어울리는 화면 만들기
위젯은 ChatGPT 안에 들어갑니다. 그래서 일반 웹사이트처럼 강한 브랜드 색, 큰 그림자, 넓은 여백을 그대로 가져오면 대화창 안에서 튀어 보입니다. Apps SDK UI는 이런 문제를 줄이기 위해 제공되는 경량 디자인 시스템입니다. 색상, 타이포그래피, 간격, 크기, 그림자, surface 같은 디자인 토큰과 접근성 중심 React 컴포넌트를 제공합니다.5
설치는 간단합니다.
npm install @openai/apps-sdk-ui전역 CSS에는 Tailwind와 Apps SDK UI 스타일을 먼저 불러옵니다.
@import "tailwindcss";
@import "@openai/apps-sdk-ui/css";
@source "../node_modules/@openai/apps-sdk-ui";그다음 Badge, Button, Icon 같은 컴포넌트를 사용하면 ChatGPT 환경과 어울리는 기본 스타일을 빠르게 얻을 수 있습니다.
import { Badge } from "@openai/apps-sdk-ui/components/Badge";
import { Button } from "@openai/apps-sdk-ui/components/Button";
import { Calendar, Maps } from "@openai/apps-sdk-ui/components/Icon";
export function ReservationSummary() {
return (
<section className="rounded-2xl bg-surface p-4 shadow-sm">
<div className="flex items-center justify-between">
<h2 className="font-semibold">La Luna Bistro</h2>
<Badge color="success">예약 가능</Badge>
</div>
<p className="mt-2 flex items-center gap-2 text-sm text-secondary">
<Calendar className="size-4" /> 토요일 · 오후 7:30 · 2명
</p>
<Button className="mt-4" color="primary" block>
<Maps /> 길찾기
</Button>
</section>
);
}여기서 중요한 것은 컴포넌트 자체보다 토큰을 따르는 습관입니다. bg-surface, text-secondary, shadow-sm, rounded-2xl처럼 환경에 맞는 값으로 구성하면 라이트 모드와 다크 모드에서 자연스럽게 보입니다.
다크 모드, 반응형, 접근성은 기본 요구사항이다
위젯은 다양한 폭에서 렌더링됩니다. 사용자가 데스크톱 ChatGPT를 볼 수도 있고, 좁은 패널이나 모바일에 가까운 환경에서 볼 수도 있습니다. 따라서 처음부터 반응형으로 설계해야 합니다.
실무에서는 다음 기준을 기본값으로 두면 좋습니다.
작은 폭에서는 1열 카드 목록을 사용한다.
넓은 폭에서는 2열 그리드나 좌우 분할을 사용한다.
버튼은 손가락으로 누를 수 있을 만큼 충분한 높이를 둔다.
텍스트가 길어질 때 줄바꿈과 말줄임을 모두 고려한다.
다크 모드에서 테두리와 surface 대비가 너무 약하지 않은지 확인한다.
그림자는 약하게 사용하고, 구분은 여백과 테두리로 해결한다.
접근성도 선택 사항이 아닙니다. 버튼은 키보드로 이동할 수 있어야 하고, 아이콘만 있는 버튼에는 보조 설명이 있어야 합니다. 상태는 색상만으로 표현하지 말고 텍스트도 함께 제공하세요. 예를 들어 성공 상태를 초록색 점만으로 표시하지 말고 예약 가능, 결제 완료, 동기화 실패처럼 읽을 수 있는 문구를 함께 둡니다.
Apps SDK UI 컴포넌트는 Radix 기반 접근성 프리미티브와 일관된 스타일을 제공하므로, 기본 컴포넌트를 직접 다시 만들기보다 제공되는 구성 요소를 먼저 검토하는 편이 좋습니다.5
좋은 위젯은 대화 흐름을 끊지 않는다
ChatGPT 앱의 UI는 독립 웹앱이 아닙니다. 사용자는 여전히 대화 중입니다. 따라서 위젯의 목표는 사용자를 다른 세계로 보내는 것이 아니라, 대화 안에서 다음 행동을 더 쉽게 만드는 것입니다.
좋은 위젯은 다음 흐름을 만듭니다.
후보 탐색: 리스트, 캐러셀, 지도
선택 이해: 상세 카드, 비교 카드
입력 보완: 폼, 날짜 선택, 수량 조절
행동 확정: 예약 카드, 장바구니, 결제 전 요약
결과 확인: 상태 배지, 영수증, 추적 카드반대로 나쁜 위젯은 너무 많은 기능을 한 번에 넣습니다. 작은 대화창 안에 사이드바, 탭 6개, 복잡한 필터, 긴 표를 모두 넣으면 사용자는 다시 텍스트로 "뭘 누르면 돼?"라고 묻게 됩니다.
위젯을 설계할 때는 항상 이 질문으로 돌아오세요.
이 화면은 사용자가 다음 결정을 더 쉽게 내리게 하는가?
답이 아니면 줄이세요. 카드 하나를 덜어내고, 버튼 하나를 줄이고, 상세 정보는 접어 두세요. Apps SDK의 힘은 모든 웹앱을 대화창 안에 복사하는 데 있지 않습니다. 대화와 UI가 서로 부족한 부분을 채우게 만드는 데 있습니다.
이제 도구가 데이터를 만들고, 위젯이 그 데이터를 화면으로 바꾸는 기본 구조를 갖췄습니다. 다음 단계는 위젯이 ChatGPT 호스트와 어떻게 대화하는지 이해하는 것입니다. window.openai 브릿지를 사용하면 위젯은 호스트 상태를 읽고, 도구를 다시 호출하고, 표시 모드를 바꾸고, 후속 메시지를 보낼 수 있습니다. 그때부터 위젯은 단순한 결과 화면을 넘어 대화 안에서 살아 움직이는 앱이 됩니다.
참고
3OpenAI Apps SDK Examples Gallery
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.