1. 왜 외형을 관리해야 할까
지금 여러분의 위젯은 아마 “보통의 React 컴포넌트”처럼 보일 겁니다: 어떤 div, 항목 목록, 버튼 몇 개. 일반 웹에서는 이 정도로 충분한 경우가 많습니다. 하지만 ChatGPT에서는 한 가지 차이가 있습니다. 여러분의 UI는 채팅 내부에서 동작합니다. 사용자에게는 이미 많은 시각적 컨텍스트가 있죠 — 메시지, 다른 Apps, 음성 인터페이스, 그리고 컨테이너 크기 제한까지요.
두 가지를 꼭 기억하세요.
첫째, 위젯에는 표시 모드(displayMode)가 있습니다: inline, fullscreen, 때로는 PiP. 모드에 따라 사용 가능한 면적, 스크롤 동작, 사용자의 기대가 달라집니다.
둘째, 플랫폼은 위젯에 높이 제한(maxHeight)과 테마(theme)를 알려줍니다. 이를 무시하고 하나의 메시지 안에 Notion만 한 것을 그려 넣으면, 채팅은 하나의 거대한 iframe 속 “블랙홀”이 되어버립니다. OpenAI는 UI를 간결하게 만들고 시스템 색/타이포그래피를 존중할 것을 분명히 권장합니다.
GiftGenius의 전형적인 시나리오는 실제 동작을 잘 보여줍니다. 사용자가 “친구에게 줄 선물을 $50 이하로 추천해줘”라고 요청합니다. ChatGPT가 GiftGenius를 실행하고, inline 모드에서 선물 카드 몇 장과 버튼 두세 개를 간결하게 보여줍니다. 사용자가 “자세히”를 클릭하면 — 위젯은 fullscreen을 요청하고, 그 안에서 필터, 상세 설명, 리뷰를 보여줍니다. 결제 진행 중에는 전체 채팅을 덮지 않고 “주문 처리 중…” 상태를 보여주는 작은 PiP/모달을 띄울 수 있습니다.
이 강의의 목표는 다음과 같습니다.
- 현재 displayMode가 무엇인지 이해하고, 그에 맞게 동작하기;
- 요청 시 모드 전환하기 (inline ↔ fullscreen, 때로는 PiP);
- maxHeight를 존중하고 “이중 스크롤”을 만들지 않기;
- 라이트/다크 테마와 화면 너비에 맞춰 스타일을 적응시키기;
- ChatGPT 안에서 “자연스러워” 보이는 layout 구성하기.
2. displayMode 모드: inline, fullscreen, PiP
개념부터 시작하죠. displayMode는 ChatGPT에서 여러분의 위젯 컨테이너 상태입니다. 플랫폼으로부터 전달되며(window.openai.displayMode 또는 useDisplayMode 훅), "inline", "fullscreen", "pip" 같은 값을 가질 수 있습니다.
Inline
Inline은 기본 모드입니다. 위젯이 메시지 흐름 속에 또 하나의 “블록”으로 삽입됩니다. 너비는 채팅 컬럼 너비로 제한됩니다(데스크톱에서는 대략 ~700–800px, 모바일에서는 화면 너비). 높이는 동적이지만 무한정 크지 않습니다.
Inline에 이상적인 경우:
- 짧고 자체 완결적인 표현: 선물 카드, 옵션 목록, 검색 요약;
- 한두 개의 액션: “선택”, “취소”, “더 보기”.
GiftGenius에서는 이것이 기본 모드입니다. 사용자가 요청을 작성하면, 여러분은 화면을 다 차지하지 않고 버튼이 있는 선물 카드 3–5개를 보여줍니다.
Fullscreen (Canvas)
Fullscreen(또는 canvas)은 위젯이 보이는 영역 대부분을 차지하는 모드입니다. 채팅이 사라지는 것은 아닙니다. 입력창은 여전히 사용할 수 있지만, 주된 초점은 여러분의 UI에 갑니다.
다음과 같은 경우 fullscreen을 켜는 것이 좋습니다.
- 입력 필드가 많거나 복잡한 마법사(주문서, 복잡한 필터, 설정);
- 큰 테이블, 지도, 수십 개 항목 비교를 보여줘야 할 때;
- inline에 더 이상 담기지 않아 700px 높이의 미니 “엑셀”처럼 보일 때.
GiftGenius에서 fullscreen은 사용자에게 충분한 필터, 정렬, 상세 설명, 아마도 여러 탭까지 제공할 때 필요합니다.
PiP / Modal
PiP(picture-in-picture)와 모달은 본문 위에 떠 있는 작은 창입니다. 현재 Apps SDK 구현에서는 PiP가 displayMode의 특별 모드로 구현되거나, requestModal()로 모달 창을 띄우는 방식으로 구현되곤 합니다.
이런 경우에 유용합니다.
- 긴 프로세스의 상태(주문 처리, 비디오 렌더링)를 보여줄 때;
- 주 흐름을 방해하지 않고 간단히 물어볼 때(빠른 확인 등);
- 채팅을 계속하면서 “위젯을 눈에 띄게” 유지하고 싶을 때.
GiftGenius에서는 “주문을 진행 중… 30%” 같은 작은 패널과 “취소” 버튼이 될 수 있습니다.
간단 비교
시각적 이해를 위한 표:
| 모드 | 위치 | 일반적 사용 사례 | 제약 |
|---|---|---|---|
|
메시지 흐름 안 | 목록, 카드, 한두 개의 버튼 | 제한된 높이, 좁은 너비 |
|
채팅 위/옆 | 마법사, 복잡한 폼, 테이블 | 의미 있는 레이아웃과 내비게이션 필요 |
| PiP / modal | 플로팅 레이어 | 상태, 미니 폼, 비디오 | 매우 작은 공간 — 모든 것이 크고 단순해야 함 |
fullscreen을 “진짜 애플리케이션”으로, inline을 “미리보기”로 여기지 마세요. 같은 App이 단지 다른 “자세”를 취할 뿐입니다.
3. 모드를 다루는 훅: useDisplayMode, useRequestDisplayMode, useRequestModal
이제 UX 관점에서 inline/fullscreen/PiP가 무엇인지 이해했으니, Apps SDK 훅을 통해 코드에서 어떻게 다루는지 보겠습니다.
window.openai.displayMode를 직접 읽는 대신, 템플릿에서 제공하는 훅을 사용해 변경 사항을 구독하고 SDK 이벤트 처리 의식을 피합니다. 전형적인 인터페이스는 다음과 같습니다.
// 의사 타입 — 실제 이름은 템플릿을 참고하세요
type DisplayMode = 'inline' | 'fullscreen' | 'pip';
function useDisplayMode() {
// 현재 모드를 반환
return { displayMode: 'inline' as DisplayMode };
}
function useRequestDisplayMode() {
// 모드 변경 요청 함수
return {
requestDisplayMode: (mode: DisplayMode) => {
/* window.openai.requestDisplayMode 호출 */
},
};
}
현재 모드를 보여주고 “확장 / 접기” 버튼을 제공하는 간단한 컴포넌트를 만들어 봅시다.
import { useDisplayMode, useRequestDisplayMode } from '@/apps-sdk';
export function DisplayModeDebug() {
const { displayMode } = useDisplayMode();
const { requestDisplayMode } = useRequestDisplayMode();
const toggle = () => {
requestDisplayMode(displayMode === 'inline' ? 'fullscreen' : 'inline');
};
return (
<div className="text-xs text-gray-500 flex gap-2 items-center">
<span>모드: {displayMode}</span>
<button onClick={toggle} className="underline">
전환
</button>
</div>
);
}
실제 App에서는 이런 “디버그용” 요소를 보통 숨기지만, Dev Mode에서는 전환 시 위젯이 어떻게 동작하는지 감을 잡는 데 큰 도움이 됩니다.
서로 다른 하위 컴포넌트로 Inline vs Fullscreen
자주 하는 실수는 하나의 레이아웃으로 모든 모드를 처리하려고 하고, JSX에 if(displayMode === ...)를 잔뜩 넣는 것입니다. 표현을 분리하는 편이 훨씬 읽기 쉽습니다.
import { useDisplayMode } from '@/apps-sdk';
import { GiftListInline } from './GiftListInline';
import { GiftListFullscreen } from './GiftListFullscreen';
export function GiftWidget() {
const { displayMode } = useDisplayMode();
if (displayMode === 'fullscreen') {
return <GiftListFullscreen />;
}
return <GiftListInline />;
}
이렇게 하면 코드는 “fullscreen이면 복잡한 마법사, 아니면 컴팩트한 inline”처럼 읽힙니다. 각 하위 컴포넌트는 자신의 제약에 맞춰 별도로 스타일링할 수 있습니다. 이 접근은 이 모듈의 권장 사항과도 같습니다. 하나의 거대한 if/else 대신 모드별로 하위 컴포넌트를 분리하세요.
모달: useRequestModal
템플릿이 useRequestModal 훅을 제공한다면, 인터페이스는 보통 다음과 비슷합니다.
const { requestModal } = useRequestModal();
// requestModal({ title }) 같은 형태를 사용합니다.
모달은 fullscreen과 비슷한 점이 있지만 대체재는 아닙니다. fullscreen은 큰 시나리오에, 모달은 하나의 짧은 단계(동작 확인, 쿠폰 코드 입력 등)에 적합합니다.
4. 크기 제어: maxHeight, 스크롤, notifyIntrinsicHeight()
두 번째 중요한 축은 높이입니다. 플랫폼은 위젯에게 “사용 가능한 최대 높이”를 알려줍니다. 이 제한은 window.openai.maxHeight 또는 useMaxHeight 훅을 통해 읽을 수 있습니다.
왜 그냥 “height: 5000px”로 하면 안 될까
maxHeight를 무시하고 거대한 고정 높이를 지정하면, ChatGPT는 여러분의 콘텐츠를 잘라낼 수밖에 없습니다. 혹은 사용자에게 이중 스크롤을 제공하게 됩니다. 바깥(채팅)의 스크롤과 안쪽(위젯)의 스크롤이 동시에 생기는 거죠. 이건 안 좋은 UX입니다. 사용자는 원하는 버튼에 도달하려면 어디를 스크롤해야 하는지 추측해야 합니다.
올바른 전략은 다음과 같습니다.
- maxHeight 제한을 읽는다.
- 특히 inline에서는 기본 스크롤이 채팅에 남도록 레이아웃을 구성한다.
- fullscreen에서는 약간의 내부 스크롤을 허용할 수 있지만, 신중히 사용한다.
useMaxHeight와 컨테이너 제한
루트 컨테이너에 최대 높이를 설정하는 간단한 래퍼를 작성해 봅시다.
import { useMaxHeight } from '@/apps-sdk';
export function WidgetContainer(props: { children: React.ReactNode }) {
const { maxHeight } = useMaxHeight(); // 예: 600
return (
<div
style={{ maxHeight }}
className="overflow-y-auto p-4 bg-background border border-border rounded-xl"
>
{props.children}
</div>
);
}
여기서는 높이를 정직하게 제한하고, 컨테이너 내부에 수직 스크롤을 켰지만, 적정 수준으로 유지합니다. 실제로 inline에서는 큰 내부 스크롤을 피하는 편이 좋고, 방대한 목록 대신 일부만 보여주며 “더 보기” 버튼을 제공하거나 fullscreen을 제안하는 것이 좋습니다.
동적 높이와 notifyIntrinsicHeight()
또 다른 포인트는 콘텐츠의 크기가 시간에 따라 변할 수 있다는 것입니다. 예를 들어, 처음에는 “선물을 불러오는 중…” 스피너를 보여주다가, 이후 카드 10개 목록을 보여주고, 사용자가 필터를 접었다/펼쳤다 할 수 있죠. ChatGPT가 위젯에 적절한 공간을 할당하고 잘라내지 않도록 하려면, 높이가 변할 때마다 호스트에 새 값을 알려야 합니다. 이를 위한 API가 notifyIntrinsicHeight()입니다.
템플릿에서는 이를 useAutoResize 같은 훅으로 감싸는 경우가 많습니다. 구현은 대략 다음과 같습니다.
import { useEffect, useRef } from 'react';
import { useNotifyIntrinsicHeight } from '@/apps-sdk';
export function useAutoResize() {
const ref = useRef<HTMLDivElement | null>(null);
const { notifyIntrinsicHeight } = useNotifyIntrinsicHeight();
useEffect(() => {
if (!ref.current) return;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
notifyIntrinsicHeight(entry.contentRect.height);
}
});
observer.observe(ref.current);
return () => observer.disconnect();
}, [notifyIntrinsicHeight]);
return ref;
}
사용 예:
export function GiftListInline() {
const containerRef = useAutoResize();
return (
<div ref={containerRef}>
{/* 여러분의 콘텐츠 */}
</div>
);
}
핵심 아이디어는 간단합니다. 루트 div의 높이가 바뀔 때 SDK API를 호출하고, ChatGPT는 컨테이너를 그에 맞춰 조정합니다. 이러한 “자동 리사이즈 래퍼” 패턴은 숙련된 개발자도 적극 권장합니다.
간단한 도식
블록 다이어그램으로 표현하면 다음과 같습니다.
flowchart TD
A[위젯 콘텐츠가 변경됨] --> B[ResizeObserver가 새 높이를 감지]
B --> C["notifyIntrinsicHeight(newHeight) 호출"]
C --> D[ChatGPT가 컨테이너 높이를 조정]
D --> E[사용자는 깔끔한 스크롤을 경험]
이제 크기와 높이는 정리됐습니다. 위젯은 할당된 공간을 넘치지 않아야 하며, 사용자에게 이중 스크롤이라는 퀘스트를 강요해서는 안 됩니다.
5. 테마(theme), 색상, 보더: 위젯을 “자연스럽게” 보이게
displayMode와 maxHeight가 얼마나의 공간이 있는지를 결정한다면, 테마(theme)와 팔레트는 채팅 안에서 이 UI가 어떻게 보일지를 책임집니다.
ChatGPT는 최소한 라이트/다크 테마를 지원합니다. 플랫폼은 이를 window.openai.theme 및/또는 _meta["openai/theme"]를 통해 위젯으로 전달하고, React 템플릿에는 useOpenAiGlobal("theme") 또는 useTheme 같은 훅이 있습니다.
핵심: 여러분의 UI는 테마에 맞춰 적응해야지, 자신의 테마를 강요하면 안 됩니다.
테마 가져오기
간단한 훅 예:
import { useOpenAiGlobal } from '@/apps-sdk';
export function useThemeMode() {
const theme = useOpenAiGlobal<'light' | 'dark'>('theme') ?? 'light';
return { theme };
}
컴포넌트에서:
export function ThemedCard(props: { children: React.ReactNode }) {
const { theme } = useThemeMode();
const className =
theme === 'dark'
? 'bg-slate-900 text-slate-100 border-slate-700'
: 'bg-white text-slate-900 border-slate-200';
return (
<div className={`rounded-xl border p-4 ${className}`}>
{props.children}
</div>
);
}
실제 프로젝트에서는 보통 darkMode: 'class'로 Tailwind를 사용하고, 위젯 루트 컨테이너에 dark 클래스를 달아둡니다. 하지만 본질은 같습니다. 테마는 Apps SDK에서 오고, 제멋대로 존재하지 않습니다.
색상, 보더, 타이포그래피
OpenAI 가이드에 따르면:
- 시스템 폰트와 깔끔한 타이포그래피를 사용하세요.
- 시스템 색상을 공격적으로 재정의하지 마세요.
- 위젯은 채팅의 “자연스러운” 일부여야 하며, 강렬한 그라데이션의 독립 랜딩 페이지처럼 보여서는 안 됩니다.
GiftGenius 컨테이너에 좋은 패턴:
export function GiftCard(props: { title: string; price: string }) {
return (
<div className="rounded-xl border border-border bg-background p-3 flex flex-col gap-2">
<div className="font-medium text-foreground">{props.title}</div>
<div className="text-sm text-muted-foreground">{props.price}</div>
<button className="self-start px-3 py-1 text-sm rounded-full bg-primary text-primary-foreground">
선택
</button>
</div>
);
}
여기서 bg-background, border-border, text-foreground, bg-primary 등은 ChatGPT 테마와 연결된 CSS 변수/유틸리티 클래스라고 가정합니다. 테마에 연결된 변수와 클래스를 사용하는 접근이 권장됩니다. 색상을 하드코딩하지 마세요.
6. 레이아웃과 반응형: desktop, mobile, PiP
세 번째 축은 너비와 디바이스입니다. 아주 단순화하면, 위젯의 외형은 displayMode, 사용 가능한 높이(maxHeight), 그리고 사용 가능한 너비(desktop/mobile/PiP)로 결정됩니다.
이 섹션에서는 세 번째 파라미터를 살펴봅니다. 데스크톱에서 inline 위젯은 한 가지 너비, 모바일에서는 다른 너비를 갖습니다. PiP는 공간이 매우 좁습니다. Apps SDK는 userAgent, safeArea, 때로는 컨테이너 크기 같은 신호를 전달하며, useOpenAiGlobal로 읽을 수 있습니다.
일반 원칙
중요한 원칙을 몇 가지 정리합니다.
첫째, 고정 너비를 가정하지 마세요. 사용자의 화면은 좁을 수도(폰) 넓을 수도(큰 데스크톱) 있습니다. 따라서 레이아웃은 width: 400px 같은 하드 설정보다 flex/grid의 auto-fit 기반으로 구성하는 편이 좋습니다.
둘째, 수평 스크롤을 피하세요. 테이블이나 카드가 다 안 들어가면 fullscreen으로 전환하거나 축약된 버전을 보여주세요. 또는 슬라이드형 캐러셀을 사용할 수도 있습니다.
셋째, PiP/모달은 대개 매우 좁다는 점을 고려하세요. 큰 폼을 넣으면 사용자가 입력하기가 물리적으로 힘들어집니다.
이 포인트들은 문서에서 반복적으로 강조됩니다. 반응형, safeArea, 데스크톱과 모바일의 차이, 과도한 레이아웃의 위험성 말이죠.
inline과 fullscreen에 서로 다른 레이아웃
GiftGenius로 돌아가 봅시다. inline의 선물 목록과 fullscreen의 선물 목록은 꽤 다르게 보일 수 있습니다. 두 개의 컴포넌트를 만들어 봅시다.
컴팩트한 inline: 최대 3개의 카드, 모바일에서는 한 컬럼, 넓은 화면에서는 두 컬럼.
export function GiftListInline() {
const gifts = useGiftData(); // 가상의 훅 — toolOutput에서 가져온다고 가정
return (
<WidgetContainer>
<h2 className="text-base font-semibold mb-3">
선물 추천
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{gifts.slice(0, 3).map(gift => (
<GiftCard
key={gift.id}
title={gift.title}
price={`${gift.price} $`}
/>
))}
</div>
{gifts.length > 3 && (
<p className="mt-3 text-xs text-muted-foreground">
처음 3개만 표시됩니다. 전체 보려면 위젯을 펼치세요.
</p>
)}
</WidgetContainer>
);
}
그리고 fullscreen 버전: 그리드, 필터, 더 많은 카드.
export function GiftListFullscreen() {
const gifts = useGiftData();
const [query, setQuery] = useState('');
const filtered = gifts.filter(g =>
g.title.toLowerCase().includes(query.toLowerCase()),
);
return (
<div className="h-full flex flex-col gap-4 p-4">
<header className="flex gap-2 items-center">
<h1 className="text-lg font-semibold flex-1">
당신을 위한 선물
</h1>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="이름으로 필터링"
className="px-2 py-1 text-sm border rounded-md flex-1"
/>
</header>
<main className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{filtered.map(gift => (
<GiftCard
key={gift.id}
title={gift.title}
price={`${gift.price} $`}
/>
))}
</div>
</main>
</div>
);
}
여기서는 fullscreen 콘텐츠의 내부 수직 스크롤(main에 overflow-y-auto)을 허용합니다. 이는 풀스크린 모드에서 자연스러운 패턴입니다. 반면 inline 버전은 가이드 권장대로 컴팩트하고 “2초 만에 읽히는” 형태를 유지합니다.
도식: 모드별 동작
정리를 위해 간단한 다이어그램을 그려 봅시다.
stateDiagram-v2
[*] --> Inline
Inline: 카드 3개, 최소 텍스트
Inline --> Fullscreen: 클릭 "펼치기" / "모두 보기"
Fullscreen: 그리드, 필터, 많은 데이터
Fullscreen --> Inline: 버튼 "닫기" / 호스트 동작
Fullscreen --> PiP: 긴 작업, 진행률 표시
PiP: 작은 상태 패널
PiP --> Inline: 작업 완료, 최종 메시지 표시
이 시나리오는 앞서 설명한 UX 패턴과 매우 유사합니다. inline은 티저, fullscreen은 작업 도구, PiP는 프로세스 인디케이터입니다.
7. 실습: 같은 위젯의 두 가지 모드
이제 코드를 통해 익혀 봅시다. 이 강의의 실습으로 현재 학습용 애플리케이션에서 두 단계를 구현하는 것을 추천합니다.
1단계. 카드가 있는 Inline 위젯
현재 GiftGenius를 확장해 inline 모드에서 위젯이 다음을 수행하도록 하세요.
- 제목 “선물 추천”을 표시한다;
- toolOutput의 선물 카드 최대 3개를 보여준다;
- 선물이 3개보다 많으면 “전체 보려면 위젯을 펼치세요”라는 힌트를 보여준다;
- useAutoResize와 notifyIntrinsicHeight()로 높이를 깔끔히 맞춘다.
이때 스타일은 테마에 의존해야 합니다. theme에 연결된 클래스나 변수를 사용하고, 색상을 하드코딩하지 마세요.
2단계. 폼이 있는 Fullscreen 버전
그다음 fullscreen 표현을 추가해 다음을 수행하도록 하세요.
- 제목 + 이름 기준 검색을 제공한다;
- 모든 선물을 그리드로 표시한다;
- 메인 영역 내부의 수직 스크롤을 허용한다;
- “대화로 돌아가기” 버튼을 제공한다(클릭 시 requestDisplayMode('inline') 호출).
구성은 다음과 같을 수 있습니다.
export function GiftGeniusWidget() {
const { displayMode } = useDisplayMode();
return (
<>
<DisplayModeDebug />
{displayMode === 'fullscreen' ? (
<GiftListFullscreen />
) : (
<GiftListInline />
)}
</>
);
}
ChatGPT Dev Mode에서 수동으로 모드를 전환하거나, inline 버전의 “모두 보기” 버튼 클릭 시(useRequestDisplayMode를 통해) 프로그래밍 방식으로 fullscreen을 요청할 수 있습니다. 이 연습을 통해 같은 App이 displayMode에 따라 외형과 동작을 어떻게 달리할 수 있는지 체득할 수 있습니다.
8. 위젯 외형 제어 시 흔한 실수
다음 과정으로 넘어가기 전에, displayMode, 크기, 테마, 레이아웃과 관련해 자주 밟는 지뢰를 정리해 둡시다. 초반부터 이것들을 피하면 Apps SDK와의 생활이 훨씬 편해집니다.
오류 №1: displayMode를 무시하고 모든 것을 fullscreen 같은 레이아웃으로 강제함.
개발자가 무거운 레이아웃(거의 독립 SPA)을 하나 그리고, 그것이 겨우 inline에 들어가게 만드는 경우가 있습니다. 결과적으로 사용자는 스크롤과 수많은 요소가 있는 미니 Notion을 보게 됩니다. 올바른 접근은 서로 다른 모드에 서로 다른 표현을 설계하고, inline은 컴팩트한 “한 화면” 형식임을 존중하는 것입니다.
오류 №2: 거대한 고정 높이와 이중 스크롤.
height: 800px만 설정하고 maxHeight를 잊는 것은, 위젯이 잘리거나 내부/외부 스크롤을 동시에 만들어내는 지름길입니다. 사용자는 “어느 스크롤바가 맞는지”를 맞추느라 고생합니다. 대신 maxHeight를 읽고, max-height로 제한하며, 높이가 변할 때 notifyIntrinsicHeight()로 알려야 합니다.
오류 №3: 테마를 무시하고 브랜드 색으로 “재도색”.
자체 폰트, 배경, 강한 그라데이션을 적용하고 ChatGPT의 라이트/다크 테마를 완전히 무시하면, 플랫폼의 시각적 일관성이 깨집니다. 가이드는 명확합니다. 시스템 색과 폰트를 사용하고, 브랜드는 버튼/아이콘/로고 같은 절제된 악센트로 가져오세요. 훅으로 theme를 추적하며 팔레트를 맞추세요.
오류 №4: PiP/모달에 지나치게 복잡한 UI.
작은 PiP 창에 필드가 많은 폼 전체를 넣으려는 시도는 실패로 이어집니다. 거기에는 아주 단순한 경우만 적합합니다. 진행 상태, 한두 개 버튼, 입력 필드 하나 정도요. 나머지는 fullscreen 후보입니다.
오류 №5: 800px에 하드코딩하고 모바일 테스트를 하지 않음.
“800px에 맞춰 짰으니 폰에서도 어떻게든 들어가겠지”라고 생각하는 것. 현실에서 ChatGPT 모바일 클라이언트는 너비와 동작이 꽤 다르고, PiP는 더 좁습니다. userAgent/safeArea를 잊지 말고, grid/flex로 고정 폭 없이 구성하며, 최소한 한 번은 좁은 레이아웃에서 위젯을 확인하세요.
오류 №6: 훅 없이 window.openai를 직접 다루기.
형식적으로는 const mode = window.openai.displayMode처럼 쓸 수 있습니다. 하지만 그러면 여러분이 직접 이벤트에 구독하고, React 업데이트를 고민하고, SDK가 무언가를 바꾸면 버그를 떠안아야 합니다. 훅(useDisplayMode, useMaxHeight, useOpenAiGlobal, useRequestDisplayMode)은 이런 수고를 감추고 코드를 깔끔하게 유지하려고 존재합니다. 훅을 사용해 편하게 개발하세요.
GO TO FULL VERSION