2. inline이 있는데 왜 fullscreen이 필요한가?
이전 inline 강의에서 이미 합의했습니다. 작업이 짧고 5–7개 항목 혹은 한 화면에 들어간다면 inline 카드가 최적입니다. 몇 가지 선물 목록, 간단한 필터 두어 개, 버튼 한두 개 — 이런 것들은 메시지 스트림 안에서 훌륭히 동작합니다.
하지만 어떤 앱이든 “카드 하나 더”로는 더 이상 해결되지 않는 순간이 옵니다:
- 많은 파라미터를 수집해야 함(수신자 프로필, 배송 제약, 결제 방식 등);
- 여러 단계로 이루어진 위저드가 필요함;
- 큰 테이블, 차트, 지도, 긴 설명이 있음.
이런 경우 inline은 갑갑해집니다. 가로폭은 채팅 컬럼으로 제한되고 세로도 제한적이며, 탐색은 없고 채팅 스크롤은 하나뿐이죠. 바로 이런 시나리오를 위해 Apps SDK에는 fullscreen 모드가 있습니다. 이는 여러분의 위젯이 화면 대부분을 차지하여 복잡한 레이아웃을 보여줄 수 있는 “몰입형” 인터페이스입니다.
오늘의 또 다른 주인공은 PiP입니다. 채팅 위에 떠 있는 작은 플로팅 창으로, 배경 작업 상태, 미니 플레이어, 타이머, 진행률 표시 같은 역할에 적합합니다. 무언가 시간이 걸리는 작업이 “백그라운드”로 진행되는 동안 사용자가 GPT와 대화를 계속할 때 PiP가 이상적입니다.
기억하세요. fullscreen과 PiP는 inline의 대체물이 아니라 상위 레이어입니다. 먼저 inline으로 시작하고, inline이 비좁아질 때 fullscreen으로 확장합니다. 모든 흥미로운 것이 이미 시작되어 상태만 “눈앞에” 두고 싶을 때는 PiP로 전환합니다.
3. 기술적 기반: displayMode와 모드 전환
Apps SDK 관점에서 여러분의 위젯에는 현재 표시 상태, 즉 displayMode가 있습니다. 이 강의 시점의 주요 모드는 세 가지입니다: "inline", "fullscreen", "pip"(picture-in-picture).
호스트(ChatGPT)는 window.openai의 전역 데이터와 SDK의 전용 훅을 통해 현재 모드를 위젯에 전달합니다. 전형적인 React 템플릿에서는 다음과 비슷합니다:
// Apps SDK 템플릿의 별칭
const mode = useDisplayMode(); // 'inline' | 'fullscreen' | 'pip'
if (mode === "fullscreen") {
// 우리 위저드를 렌더링
} else {
// 컴팩트한 inline UI를 렌더링
}
SDK는 window.openai.requestDisplayMode({ mode }) 및/또는 useRequestDisplayMode 훅도 제공합니다. 이를 통해 호스트에 모드 전환을 요청할 수 있습니다. 이 메서드는 실제로 설정된 모드가 담긴 프로미스를 반환합니다. 플랫폼이 요청을 거부하거나 수정할 수 있기 때문입니다(예: 모바일에서는 PiP가 거의 항상 fullscreen으로 바뀝니다).
모드의 생명주기를 도식화하면 다음과 같습니다:
stateDiagram-v2
[*] --> Inline
Inline --> Fullscreen: requestDisplayMode('fullscreen')
Fullscreen --> Inline: requestDisplayMode('inline') / 뒤로 가기 버튼
Fullscreen --> PiP: requestDisplayMode('pip')
PiP --> Fullscreen: "확대하기"
PiP --> Inline: 작업 완료
실제 명칭과 모드의 정확한 구성은 SDK 버전에 따라 달라질 수 있으니, 프로덕션에서는 강의 내용에만 의존하지 말고 항상 문서를 다시 확인하세요.
4. 첫 전환: “전체 화면으로 확장” 버튼 만들기
작게 시작해 봅시다. 지난 모듈의 학습용 App인 GiftGenius라는 기존 inline 위젯이 있고, 현재 3–5개의 선물 카드를 보여준다고 가정합니다. 여기에 fullscreen으로 전환하는 “자세한 추천 열기” 버튼을 추가합니다.
템플릿에 다음 두 훅이 있다고 가정해 봅시다:
import { useDisplayMode, useRequestDisplayMode } from "@/sdk/display";
export const GiftGeniusWidget: React.FC = () => {
const mode = useDisplayMode();
const requestDisplayMode = useRequestDisplayMode();
if (mode === "fullscreen") {
return <GiftFullscreenWizard />;
}
return (
<InlineGiftPreview
onExpand={async () => {
await requestDisplayMode({ mode: "fullscreen" });
}}
/>
);
};
여기서 InlineGiftPreview는 현재의 inline UI이며, GiftFullscreenWizard는 지금 설계할 새 위저드 컴포넌트입니다. onExpand 핸들러에서는 requestDisplayMode를 호출하는 것에 그치지 않고, 프로미스를 대기합니다. 이렇게 하면 나중에 거절에 대응할 수 있습니다(예: 어떤 이유로든 fullscreen이 불가할 때 메시지를 보여주기).
InlineGiftPreview 자체는 충분히 단순합니다:
type InlineGiftPreviewProps = {
onExpand: () => void;
};
const InlineGiftPreview: React.FC<InlineGiftPreviewProps> = ({ onExpand }) => {
return (
<div>
<h3>선물 추천</h3>
{/* ...선물 카드... */}
<button onClick={onExpand}>자세한 추천 열기</button>
</div>
);
};
지금까지는 “모달 열기”와 유사해 보이지만, 차이는 이것을 제어하는 주체가 여러분의 React가 아니라 호스트 앱(ChatGPT)이라는 점입니다. 호스트는 제목이나 시스템 “뒤로” 버튼 등을 표시할 수 있습니다.
5. GiftGenius fullscreen 위저드 설계
이제 선물 추천을 위한 fullscreen 위저드를 설계해 봅시다. UX 관점에서 절차를 여러 논리적 단계로 나누는 것이 합리적입니다. 예를 들면:
- 선물 수신자와 계기(이벤트) 정의
- 예산과 선물 유형(실물, 경험, 디지털)
- 검토 및 선택 확정
코드에서는 이를 단계 기반의 간단한 상태 머신으로 표현할 수 있습니다:
type WizardStep = "recipient" | "preferences" | "review";
type WizardState = {
step: WizardStep;
recipient?: { ageRange: string; relation: string };
preferences?: { budget: number; categories: string[] };
};
이 상태를 React에 저장하고 적절한 화면을 렌더링하는 GiftFullscreenWizard 컴포넌트를 만들어 봅시다.
const GiftFullscreenWizard: React.FC = () => {
const [state, setState] = useState<WizardState>({ step: "recipient" });
const goNext = (partial: Partial<WizardState>) => {
setState((prev) => ({ ...prev, ...partial }));
};
if (state.step === "recipient") {
return <RecipientStep state={state} onNext={goNext} />;
}
if (state.step === "preferences") {
return <PreferencesStep state={state} onNext={goNext} />;
}
return <ReviewStep state={state} />;
};
각 단계는 작은 폼 컴포넌트입니다. 예를 들어 첫 번째 단계:
type StepProps = {
state: WizardState;
onNext: (partial: Partial<WizardState>) => void;
};
const RecipientStep: React.FC<StepProps> = ({ state, onNext }) => {
const [relation, setRelation] = useState(state.recipient?.relation ?? "");
const [ageRange, setAgeRange] = useState(state.recipient?.ageRange ?? "");
return (
<div>
<h2>누구에게 선물을 고르나요?</h2>
<input
placeholder="이 사람은 누구인가요?"
value={relation}
onChange={(e) => setRelation(e.target.value)}
/>
<input
placeholder="나이 (예: 25–34)"
value={ageRange}
onChange={(e) => setAgeRange(e.target.value)}
/>
<button
onClick={() =>
onNext({
recipient: { relation, ageRange },
step: "preferences",
})
}
>
다음
</button>
</div>
);
};
두 번째 단계에서는 예산과 카테고리를 모으고, 세 번째 단계에서는 이러한 파라미터로 선물을 추천하는 callTool / MCP 도구를 호출하고 결과를 표시합니다.
fullscreen 화면에서는 다음을 위한 공간이 충분합니다:
- 진행 표시줄 또는 스텝퍼(stepper);
- 더 풍부한 입력 필드와 도움말;
- 오류 상태(“문제가 발생했습니다. 다시 시도해 보세요.”).
UX 가이드라인의 권장 사항: 각 단계는 가능한 한 단순하게 유지하세요. 입력 필드를 과도하게 넣지 마세요. 하나의 거대한 폼보다 3–4개의 명확한 단계가 낫습니다.
6. fullscreen 위저드의 UX: 진행, 오류, 되돌아가기
그냥 폼을 전체 화면에 띄우는 것만으로는 충분하지 않습니다. 사용자는 다음이 필요합니다.
- 현재 어느 단계에 있는지 이해할 수 있어야 합니다.
- 뒤로 돌아갈 수 있어야 합니다.
- 오래 걸리는 작업 중에 무슨 일이 일어나는지 볼 수 있어야 합니다.
가장 단순한 스텝퍼는 시각적으로만 구현해도 됩니다:
const Stepper: React.FC<{ step: WizardStep }> = ({ step }) => {
const index = step === "recipient" ? 1 : step === "preferences" ? 2 : 3;
return <p>단계 {index} / 3</p>;
};
그리고 각 화면에 Stepper를 삽입합니다. 더 발전시키려면 가로 “사다리” 형태로 렌더링할 수도 있지만, 이 강의에서는 마크업 스쿨을 열지는 않겠습니다.
중요 포인트는 오류 처리입니다. 예를 들어 마지막 단계에서 search_gifts 도구를 호출한다고 합시다:
const ReviewStep: React.FC<StepProps> = ({ state }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleConfirm = async () => {
setLoading(true);
setError(null);
try {
await callTool("search_gifts", {
recipient: state.recipient,
preferences: state.preferences,
});
// 결과는 이후 채팅 / 위젯에 나타납니다
} catch (e) {
setError("선물을 찾지 못했습니다. 다시 시도해 보세요.");
} finally {
setLoading(false);
}
};
return (
<div>
{/* 파라미터 요약 표시 */}
{error && <p style={{ color: "red" }}>{error}</p>}
<button disabled={loading} onClick={handleConfirm}>
{loading ? "선물 추천 중…" : "확인하고 추천 받기"}
</button>
</div>
);
};
접근성 관점에서는 다음을 신경 써야 합니다.
- fullscreen에서 “다음”, “뒤로”, “취소” 같은 주요 버튼을 쉽게 클릭할 수 있어야 합니다.
- 텍스트 대비가 충분해야 합니다.
- Tab 키로 모든 인터랙티브 요소를 순서대로 탐색할 수 있어야 합니다.
가능하다면 비표준 컨트롤(예: 커스텀 카테고리 토글)에 aria-label을 추가하는 것이 좋습니다. 이 강의가 WCAG 시험 준비는 아니지만, 기본적인 a11y를 챙기면 나중에 Store 리뷰를 수월하게 통과하는 데 도움이 됩니다.
결국 fullscreen 위저드는 복잡한 다단계 시나리오를 해결합니다. 폼과 진행, 오류를 위한 공간을 제공합니다. 그러나 앱의 삶은 거기서 끝나지 않습니다. 많은 작업이 “백그라운드”에서 계속됩니다. 이를 위해 두 번째 모드인 PiP가 있으며, 곧 이어서 다룹니다.
7. ChatGPT에서의 PiP란 무엇이며 왜 “까다로운가”
복잡한 시나리오에 fullscreen을 사용하는 방법을 알아봤습니다. 이제 반대 케이스를 보겠습니다. 중요한 것들은 이미 시작되었고, 진행만 “관리”하면 되는 경우입니다. 여기서 PiP가 등장합니다.
웹 세계에서 “picture-in-picture”는 보통 콘텐츠 위 구석에 떠 있는 비디오를 떠올리게 합니다. ChatGPT의 PiP는 작은 플로팅 위젯 창으로, 채팅을 스크롤해도 보이면서 상태, 진행률 또는 컴팩트한 UI를 표시할 수 있습니다.
문서와 초기 adopters의 경험에서 반드시 알아야 할 몇 가지 특징:
- PiP는 공간이 매우 협소합니다. 폼과 복잡한 레이아웃을 위한 자리가 아니라, 두세 개의 핵심 지표와 한두 개 버튼을 위한 공간입니다.
- 데스크톱에서는 PiP가 상단에 “붙어” 있고 스크롤과 관계없이 보이는 반면, 모바일에서는 자주 자동으로 fullscreen으로 바뀝니다.
- requestDisplayMode에서 mode를 "pip"로 요청해도 진짜 PiP가 보장되지는 않습니다. 플랫폼이 다른 모드(예: fullscreen)를 반환할 수도 있고, 오래된 SDK 버전에서는 이상하게 동작할 수도 있습니다. 따라서 항상 프로미스 결과를 확인하고 fallback을 준비하세요.
여기서 간단한 UX 결론이 나옵니다. PiP에는 가장 중요한 것만 넣으세요. 타이머, 배송 지표, 작업 상태, “확대” 버튼 정도입니다. 12개의 체크박스, 10열짜리 테이블, “커피도 내려줘” 같은 건 넣지 마세요.
8. GiftGenius + PiP: 오래 걸리는 검색과 백그라운드 진행
GiftGenius 시나리오로 돌아갑시다. 사용자가 fullscreen 위저드를 거쳐 “확인”을 눌렀고, 이제 여러분의 백엔드가 제법 무거운 추천 작업을 시작합니다. MCP 서버를 통해 여러 외부 API를 호출하고, 가격을 재계산하고, 많은 필터를 적용할 수도 있습니다. 10–20초가 걸릴 수 있겠죠.
UX 관점에서 20초 동안 스피너만 보이며 fullscreen에 머물게 하고 싶지는 않습니다. 더 나은 접근은 다음과 같습니다.
- 추천 작업 시작.
- 인터페이스를 PiP로 축소하고 진행률 표시.
- 사용자가 채팅을 계속할 수 있게 하기(예: 추가 질문).
- 완료 후 결과를 inline으로 반환하거나 선물 목록이 담긴 새로운 fullscreen 열기.
이런 동작을 관리하는 간단한 훅을 만들어 봅시다:
const useLongGiftJob = () => {
const [status, setStatus] = useState<"idle" | "running" | "done">("idle");
const requestDisplayMode = useRequestDisplayMode();
const startJob = async (payload: any) => {
setStatus("running");
const resultMode = await requestDisplayMode({ mode: "pip" });
console.log("실제 모드:", resultMode.mode);
await callTool("run_gift_job", payload);
setStatus("done");
await requestDisplayMode({ mode: "inline" });
};
return { status, startJob };
};
이제 ReviewStep에서 직접 callTool을 호출하는 대신 이 훅을 사용합니다:
const ReviewStep: React.FC<StepProps> = ({ state }) => {
const { status, startJob } = useLongGiftJob();
return (
<div>
{/* ...요약... */}
<button
disabled={status === "running"}
onClick={() => startJob(state)}
>
{status === "running" ? "선물 추천 중…" : "추천 시작"}
</button>
</div>
);
};
백그라운드 작업 상태가 fullscreen 위저드와 PiP 창 모두에서 접근 가능하도록, 실제 코드에서는 useLongGiftJob을 컨텍스트로 분리하고 useLongGiftJobContext로 읽는 것이 좋습니다. 컨텍스트 구현(Provider, createContext) 세부는 생략하겠습니다. 핵심은 job 상태가 한 곳에 존재하고, 서로 다른 UI 계층이 해당 상태를 구독한다는 점입니다.
PiP 표시를 위한 별도 컴포넌트:
const GiftPipView: React.FC<{ status: string }> = ({ status }) => {
return (
<div>
<p>GiftGenius가 작동 중…</p>
<p>상태: {status === "running" ? "진행 중" : "완료"}</p>
<button
onClick={() => window.openai.requestDisplayMode({ mode: "fullscreen" })}
>
확대하기
</button>
</div>
);
};
이제 전체 위젯에서 PiP도 고려하도록 렌더링을 바꿉니다:
const GiftGeniusWidget: React.FC = () => {
const mode = useDisplayMode();
const { status } = useLongGiftJobContext(); // 위에서 논의한 대로 컨텍스트 사용
if (mode === "pip") {
return <GiftPipView status={status} />;
}
if (mode === "fullscreen") {
return <GiftFullscreenWizard />;
}
return <InlineGiftPreview onExpand={/* 이전과 동일 */} />;
};
이 시나리오는 음성 모드(voice)와도 잘 어울립니다(이에 대해서는 voice 강의에서 다룹니다). 음성으로 추천을 시작하고, PiP가 진행을 보여주며, 아래의 채팅은 그대로 계속됩니다.
9. 비디오 + 채팅: fullscreen과 PiP를 미디어 플레이어로 쓰는 경우
역사적으로 PiP는 콘텐츠 위 구석에 떠 있는 비디오와 자주 연관됩니다. 그래서 “video + chat” 시나리오를 별도로 살펴보는 것이 자연스럽습니다. 여기에도 마법은 없습니다. 대부분의 경우 비디오를 fullscreen이나 PiP 창에 표시하면 됩니다. OpenAI 문서에서도 fullscreen과 PiP의 전형적 사용 예로 미디어 시나리오를 직접 제시합니다.
GiftGenius에서는 예를 들어 다음과 같은 의미일 수 있습니다.
- 선물의 프로모션 영상을 보여주기;
- “선물을 예쁘게 포장하는 법” 같은 짧은 튜토리얼;
- 여러 상품의 영상 리뷰.
fullscreen에서는 설명과 추천을 곁들인 완전한 <video>를 렌더링하고, PiP에서는 플레이어 자체와 작은 제목 정도만 남깁니다.
아주 간단한 래퍼 컴포넌트는 다음과 같습니다.
const GiftVideoPlayer: React.FC<{ src: string; title: string }> = ({
src,
title,
}) => (
<div>
<h3>{title}</h3>
<video
src={src}
controls
style={{ width: "100%", borderRadius: 8 }}
/>
</div>
);
fullscreen 위저드에서 사용자에게 “이 선물의 영상 리뷰 보기”를 제안하고, 이후 PiP로 축소할 수 있습니다.
const WatchVideoStep: React.FC = () => {
const requestDisplayMode = useRequestDisplayMode();
return (
<div>
<GiftVideoPlayer src="/videos/gift-wrap.mp4" title="선물 포장하는 법" />
<button
onClick={() => requestDisplayMode({ mode: "pip" })}
>
비디오를 화면 구석에 띄우고 채팅으로 돌아가기
</button>
</div>
);
};
미디어 시나리오를 위한 몇 가지 실용적인 팁:
- 소리 켠 자동 재생은 절대 사용하지 마세요 — 대표적인 UX 안티 패턴입니다.
- 자막과 키보드로 일시 정지(스페이스, 방향키)가 가능한지 확인하세요.
- PiP 창에서는 부가 텍스트를 모두 보여주려 하지 말고, 비디오 자체에 집중하세요.
10. 상태, 위젯 재마운트, 모바일 특성
이 시점에서 흔히 나오는 불편한 질문: “inline에서 fullscreen으로 갔다가 다시 돌아오면 React 상태는 보존되나요?”
짧은 답: 그렇게 믿지 마세요.
기술적으로는 SDK 버전과 호스트 구현에 따라 다릅니다. 어떤 경우에는 모드 전환이 iframe 재생성 없이 일어나지만, 다른 경우에는 위젯이 언마운트되었다가 다시 마운트됩니다. 문서에서도 모드 전환 시 컨텍스트 보존은 SDK의 구체 구현과 버전에 따라 달라지며, 개발자에게 보장되지 않는다고 명시합니다.
실용적 접근:
- 모든 크리티컬 상태(위저드 단계, 입력 데이터, 백그라운드 작업 식별자)는 다음 중 한 곳에 저장하세요.
- 백엔드(MCP 서버와 세션 토큰 활용),
- ChatGPT 컨텍스트(예: “현재 워크플로 상태”를 반환하는 도구),
- 근거가 충분히 안전하다면 URL 파라미터/로컬 스토리지.
- React state는 캐시/UI 레이어로 사용하되, 모드 전환 시 초기화될 수 있음을 염두에 두세요 — 그때는 더 신뢰할 수 있는 소스에서 복원합니다.
두 번째 미묘함은 반환값 requestDisplayMode에 관한 것입니다. 앞서 언급했듯 mode를 "pip"로 요청했는데 "fullscreen"으로 돌아올 수 있습니다. 특히 모바일에서는 진짜 PiP가 지원되지 않거나 자동으로 전체 화면으로 확장되곤 합니다.
전형적인 패턴:
const requestDisplayMode = useRequestDisplayMode();
const openPipSafe = async () => {
const result = await requestDisplayMode({ mode: "pip" });
if (result.mode !== "pip") {
// Fallback: 예를 들어 메시지를 표시하거나 UI를 fullscreen에 맞게 조정
console.log("PiP를 사용할 수 없어 다음 모드로 동작합니다:", result.mode);
}
};
이렇게 하면 작은 창을 기대했는데 “PiP 전용” 버튼이 달린 전체 화면 UI를 받는 어색한 상황을 피할 수 있습니다. 그런 상태에서는 인터페이스가 어색하게 보일 수 있습니다.
마지막으로 maxHeight와 내부 스크롤을 기억하세요. fullscreen에서도 호스트가 컨테이너 높이를 제한할 수 있으므로, 스크롤을 적절히 구성해 중첩된 스크롤바가 여러 개 생기지 않도록 해야 합니다.
11. fullscreen과 PiP 작업에서 흔한 실수
실수 1: 기본 모드로서의 fullscreen.
일부 개발자는 “fullscreen”이라는 단어를 보자마자 자신의 App을 채팅 안의 독립 SPA로 만들고 싶어 합니다. 그 결과, 선물 이야기가 나오기만 하면 사용자 의도와 상관없이 곧장 전체 화면 위저드로 이동합니다. OpenAI 가이드라인은 inline으로 시작하고, 객관적인 필요가 있을 때만 fullscreen으로 확장할 것을 거듭 권장합니다.
실수 2: PiP를 작은 fullscreen으로 취급.
PiP는 면적이 매우 제한적임에도, 때로는 탭, 폼, 필터 등 모든 것을 구겨 넣으려 합니다. 그러면 사용자는 마우스로 클릭하기조차 힘든 초소형 인터페이스를 받게 됩니다. 올바른 접근은 PiP에 상태와 한두 개의 핵심 버튼(예: “확대”, “취소”)만 표시하는 것입니다.
실수 3: 설명 없는 모드 전환.
사용자 클릭이나 GPT의 텍스트 안내 없이 위젯이 갑자기 fullscreen으로 확장되면 당황스럽습니다. PiP로 자동 축소되거나 다시 inline으로 돌아갈 때도 마찬가지입니다. 각 전환에는 짧은 모델 메시지로 설명을 덧붙이세요. fullscreen 전에는 “지금 상세 위저드를 열게요”, PiP 전에는 “계산되는 동안 작은 창으로 줄여 둘게요”처럼요.
실수 4: 모바일과 플랫폼 차이를 무시.
개발자가 데스크톱에서만 테스트하면 PiP가 기대대로 동작하지만, 모바일에서는 모든 것이 fullscreen으로 바뀌고 레이아웃이 무너지며 버튼이 안전 영역(safe area) 밖으로 나가 버릴 수 있습니다. 문서에는 모바일에서 PiP가 fullscreen으로 구현될 수 있고, SDK 버전에 따라 동작이 달라질 수 있다고 분명히 경고합니다. 따라서 타깃 디바이스에서의 테스트와 requestDisplayMode의 신중한 사용은 필수입니다.
실수 5: 모드 전환 시 상태 보존을 맹신.
서버/영속화 없이 React state만 의존하면 우스운 상황이 생깁니다. 사용자가 위저드 두 단계를 진행하고 “PiP로 축소”를 눌렀는데, 돌아오니 첫 단계로 초기화되어 있는 식이죠. 모드 전환 시 컴포넌트가 언마운트될 수 있다고 가정하고, 그 리스크를 고려해 상태 관리를 설계하세요.
실수 6: fullscreen 위저드의 접근성을 잊음.
큰 화면의 멋진 폼이 항상 저시력 사용자나 키보드만 사용하는 사람들에게 편한 것은 아닙니다. 너무 작은 텍스트, 낮은 대비, 읽기 어려운 “다음”과 “뒤로” 버튼은 UX를 망칠 뿐 아니라 Store 리뷰에서 문제를 일으키는 흔한 원인입니다. 최소한 텍스트 대비, 글자 크기, Tab 내비게이션 동작, 버튼에 명확한 텍스트 라벨이 있는지 확인하세요.
GO TO FULL VERSION