Skip to content

게임플레이 JS SDK UI 1.0.0-beta.3 ​

게임플레이 JS SDK 는 베타 버전입니다. 베타 테스트 기간 동안 스펙이 변경될 수 있습니다.

베타 기간 중 변경 사항은 릴리즈 노트의 SDK 탭에서 확인하세요.
연동 중 문제가 생기거나 궁금한 점이 있으면 데브톡으로 문의해 주세요.

이 문서는 Gameplay.UI 가 제공하는 5개 컴포넌트의 옵션과 표시 정책을 다룹니다.
SDK 설치와 init() 은 설치·초기화, Gameplay.UI 외의 메서드는 전체 메서드를 참고해 주세요.

게임플레이 JS SDK는 managed UI를 SDK 번들 안에 포함합니다.
Gameplay.init() 을 호출하면 별도 스크립트를 추가로 로드하지 않아도 managed UI가 함께 준비되며, 기본적으로 활성 상태입니다.

navigation의 닫기 버튼, 더보기 메뉴, 이탈 확인 modal은 SDK가 managed UI로 제공하는 필수 범위입니다.
게임웹뷰에서 반복적으로 필요한 이 3가지 요소는 SDK가 전담하므로, 게임이 직접 만들지 않습니다.

managed UI를 직접 호출해야 하는 경우 Gameplay.UI 를 사용합니다.
컴포넌트별 네임스페이스 5개로 구성됩니다.

ts
type GameplayUiFacade = {
  Navigation: GameplayNavigationApi;
  Toast: GameplayToastApi;
  Modal: GameplayModalApi;
  BottomSheet: GameplayBottomSheetApi;
  Loader: GameplayLoaderApi;
};

호출 형태는 다음과 같습니다.

ts
Gameplay.UI.Toast.show('저장되었습니다.');
Gameplay.UI.Loader.show();
Gameplay.UI.Loader.hide();

공통 규칙 ​

핸들 — open()·show() 는 그 오버레이만 조작하는 핸들을 돌려줍니다.
네임스페이스의 close() 는 현재 열린 것을 닫고, 핸들은 자기가 연 것만 다룹니다.

ts
type GameplayUiHandle<TOptions> = {
  close(): void;
  destroy(): void;
  update(next: TOptions): void;
};

update() 는 부분 갱신입니다.
주지 않은 필드는 현재 값이 그대로 남으므로, 로딩만 끄거나 높이만 바꾸는 호출이 나머지 옵션을 지우지 않습니다.

close() 는 닫힘 애니메이션을 재생한 뒤 사라지고, destroy() 는 즉시 제거합니다.
화면 전환처럼 애니메이션을 기다릴 수 없는 상황에만 destroy() 를 쓰세요.

버튼 — bottom sheet와 modal의 버튼은 같은 타입을 받습니다.

ts
type GameplayUiActionColor = 'gray' | 'yellow';

type GameplayUiAction<TOptions> = {
  label: string;
  color?: GameplayUiActionColor;
  onClick?: (handle: GameplayUiHandle<TOptions>) => Promise<void>;
};

type GameplayUiSecondaryAction<TOptions> = Omit<GameplayUiAction<TOptions>, 'color'>;

콜백은 자기 오버레이의 핸들을 받습니다 — 인자로 들어오므로 handle 을 변수에 담아 두지 않아도 됩니다.

ts
Gameplay.UI.BottomSheet.open({
  title: '보상 받기',
  primaryAction: {
    label: '받기',
    onClick: async (sheet): Promise<void> => {
      sheet.update({loading: true});
      await claimReward();
    },
  },
});

인자를 쓰지 않아도 됩니다.
콜백 안에서 그 오버레이를 건드릴 일이 없으면 그냥 받지 않으세요.

콜백은 Promise<void> 를 반환합니다 — onClick, onConfirm, onCancel, 더보기 항목의 onClick 이 모두 같습니다.
한 줄짜리 동기 처리에도 async 를 붙여 주세요.

SDK는 콜백이 끝날 때까지 기다린 뒤 다음 동작을 이어갑니다.
저장이나 로그 전송처럼 시간이 걸리는 작업을 넣어도 됩니다.

primaryAction 과 secondaryAction 을 각각 주거나 빼서 버튼 하나만, 둘 다, 또는 버튼 없이 구성합니다.
secondaryAction 은 color 를 받지 않습니다: 강조는 화면당 하나여야 하므로 보조 버튼은 항상 gray 입니다.

버튼 색은 꾸밈이 아니라 위계입니다

gray 가 기본입니다. 위계의 기준이 되는 색이고 어떤 액션에도 쓸 수 있습니다.
달리 고를 이유가 없다면 gray 를 쓰세요.

yellow 는 그 화면에서 우선순위가 분명히 높은 액션에만 씁니다. 사용자가 이어서 해야 할 일이 하나로 좁혀지는 자리입니다.
눈에 띄게 하려고 붙이는 색이 아닙니다: 노란 버튼이 흔해지면 정작 중요한 자리에서 구별되지 않습니다.

텍스트만 받습니다 — title·content·description 은 문자열만 받아 텍스트로 렌더합니다.
HTML 문자열이나 DOM 노드를 넣어도 마크업으로 해석되지 않습니다: 게임이 주입한 값이 그대로 실행되면 웹뷰의 네이티브 브리지가 노출될 수 있기 때문입니다.

겹치는 순서 — 위에서부터 loader → toast → modal → bottom sheet → navigation 입니다.

컴포넌트위치이유
Loader가장 위막는 것이 역할입니다.
maxDuration 이 지나면 스스로 닫혀 화면이 영구히 잠기지 않습니다
Toastmodal 위읽히는 것이 전부인 짧은 알림이라, 가리면 사용자가 못 본 채 사라집니다
Modal · BottomSheet중간사용자의 답을 기다리는 화면입니다
Navigation가장 아래modal이나 bottom sheet가 떠 있는 동안에는 닫기 버튼이 눌리면 안 됩니다

navigation이 가장 아래인 것은 의도입니다.
나가기 확인 팝업 자체가 modal이므로, navigation이 위에 있으면 팝업이 떠 있는 상태에서 닫기 버튼을 다시 눌러 onClick 이 또 실행됩니다.

더보기 목록은 navigation 안에 그려지므로 버튼과 같은 층입니다.

안전영역 — 모든 컴포넌트는 상하 안전영역을 뺀 영역을 기준으로 배치됩니다.
노치·홈 인디케이터 위치를 게임이 계산하지 않아도 됩니다.

게임 페이지의 viewport 메타태그에 viewport-fit=cover 가 필요합니다.
없으면 브라우저가 안전영역 값을 0 으로 보고하므로 컴포넌트가 노치나 홈 인디케이터에 가릴 수 있습니다.

html
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />

문서에서 바로 띄워 보기

아래 버튼은 이 페이지에서 실제 SDK를 실행합니다.
브라우저 창 전체가 게임 화면이라고 생각하고 보세요.

ts
type GameplayNavigationApi = {
  update(options?: GameplayNavigationControlsOptions): void;
  show(): void;
  hide(): void;
  updateDropdownItem(id: string, patch: GameplayNavigationDropdownItemPatch): void;
};

type GameplayNavigationDropdownItemPatch = Partial<Omit<GameplayNavigationDropdownItem, 'id'>>;

type GameplayNavigationControlsOptions = {
  visible?: boolean;
  backgroundColor?: string;
  contentElement?: Element | null;
  exitAction?: {
    onClick?: () => Promise<void>;
    onConfirm?: () => Promise<void>;
    onCancel?: () => Promise<void>;
  };
  keepBrowserAction?:
    | false
    | {
        onClick?: () => Promise<void>;
      };
  dropdown?: {
    items?: Array<GameplayNavigationDropdownItem>;
  };
};

type GameplayNavigationDropdownItem = {
  id: string;
  label: string;
  disabled?: boolean;
  onClick?: (item: GameplayNavigationDropdownItemHandle) => Promise<void>;
};

type GameplayNavigationDropdownItemHandle = {
  update(patch: GameplayNavigationDropdownItemPatch): void;
};

화면 우상단에 떠 있는 제어 UI입니다.
게임이 직접 만들 필요가 없으며, 구성만 바꿉니다.

기본으로 init() 시점에 표시됩니다.
init({ui: {navigation: {visible: false}}}) 으로 숨긴 채 시작할 수 있고, 실행 중에는 show() · hide() 로 바꿉니다.
자세한 것은 아래 표시 제어를 참고해 주세요.

화면 우상단에 더보기와 닫기 버튼이 떠 있는 navigation 예시
옵션타입설명
visibleboolean기본값 truenavigation을 표시할지.
구성과는 다른 축이라 숨긴 채 구성만 해둘 수 있습니다
backgroundColorstring상단 44px 띠의 배경색.
#RGB·#RRGGBB·#RRGGBBAA 만 받습니다
contentElementElement | null게임 영역 요소.
주면 버튼이 기기 화면이 아니라 이 요소 안 우상단에 붙습니다
exitAction{onClick?, onConfirm?, onCancel?}나가기 버튼을 눌렀을 때 실행할 작업
keepBrowserActionfalse | {onClick?}iOS 플로팅 전환 버튼.
false 면 숨기며, 지원하지 않는 환경에서는 자동으로 숨겨집니다
dropdown{items?}더보기 메뉴에 덧붙일 항목

backgroundColor 를 주지 않으면 배경이 투명이라 버튼만 떠 있고, 게임 화면이 상태바까지 올라옵니다.
색을 주면 상단 띠가 그 색으로 채워집니다.
형식에 맞지 않는 값은 예외를 던지지 않고 무시합니다: 색 하나 때문에 navigation이 사라지는 편이 더 나쁘기 때문입니다.

디자인 가이드

backgroundColor 를 지정하면 태블릿의 좌우 레터박스도 같은 색으로 맞춰 주세요.

모바일 전용 게임을 태블릿에서 실행하면 게임 화면 좌우에 레터박스가 생깁니다.
이때 navigation 띠와 레터박스를 서로 다른 색으로 두면 화면 상단이 색이 갈린 조각들로 보여, 게임 화면이 잘려 붙은 것처럼 읽힙니다.
두 색을 같게 두면 상단이 하나의 면으로 이어집니다.

레터박스 대응 기준은 게임 공통 제작 가이드: 태블릿 대응을 참고해 주세요.

게임 영역 기준 배치 ​

태블릿처럼 가로가 긴 기기에서 게임이 화면 가운데만 쓰면 좌우에 레터박스가 생깁니다.
이때 navigation 은 기기 화면 기준으로 붙습니다: 안전영역 안쪽 우상단입니다.
게임 영역은 그보다 훨씬 안쪽이므로 버튼이 레터박스 위에 놓이고, 디자인 가이드가 레터박스를 검은색(#000000)으로 규정하므로 어두운 톤의 버튼이 검은 배경에 묻혀 눈에 잘 띄지 않습니다.

가로 화면에서 navigation 버튼이 우측 검은 레터박스 위에 놓여 잘 보이지 않는 기본 동작

게임 영역을 감싸는 요소를 contentElement 로 넘기면 navigation 과 더보기 목록이 그 안으로 들어옵니다.

ts
const stage: Element | null = document.querySelector('#stage');

Gameplay.init({
  gameName: '샘플 게임',
  gameCode: 'SampleGame',
  appId: 123456,
  ui: {navigation: {contentElement: stage}},
});

// 실행 중에 바꾸려면
Gameplay.UI.Navigation.update({contentElement: stage});
같은 화면에서 contentElement 를 넘겨 navigation 버튼이 가운데 게임 영역 안 우상단으로 들어온 모습

넘기지 않으면 지금까지처럼 기기 화면 우상단이 기준이므로, 기존 게임의 동작은 달라지지 않습니다.
게임 영역이 화면만큼 넓어지면 자동으로 원래 자리로 돌아갑니다.

숫자가 아니라 요소를 넘깁니다

두 가지를 없애기 위해서입니다.

게임은 보통 캔버스 해상도로 폭을 생각하는데 그 값은 devicePixelRatio 만큼 CSS 픽셀과 다릅니다.
요소를 넘기면 SDK 가 getBoundingClientRect() 로 재므로 언제나 CSS 픽셀이라 이 문제가 사라집니다.

그리고 가로 모드는 높이에 맞춰 만들라는 디자인 가이드에 따라 게임 폭은 화면 높이에서 파생됩니다.
회전하면 값이 달라지므로 한 번 넘긴 숫자는 그 순간 틀린 값이 됩니다.
요소는 ResizeObserver 로 따라가므로 회전·분할 화면에서 다시 넘길 필요가 없습니다.

잘못된 값은 무시합니다

contentElement 로 쓸 수 없는 값이 오면 예외를 던지지 않고 기본 위치(기기 화면 안전영역 안쪽 우상단)로 둡니다.

  • 셀렉터가 빗나가 null 이 온 경우
  • 아직 DOM 에 붙지 않은 요소
  • 화면 전환 중이라 display: none 이거나 면적이 0 인 요소

마지막 경우는 요소가 다시 면적을 가지면 자동으로 게임 영역 기준으로 돌아옵니다.

navigation 이 사라지면 사용자가 게임에서 나갈 길을 잃으므로, 배치를 포기할지언정 UI 는 유지합니다.

좌우 대칭 레터박스를 전제합니다

좌우 여백이 서로 다른 레이아웃은 지원하지 않습니다.
디자인 가이드가 좌우 대칭 레터박스를 규정하므로 일반적인 구성에서는 문제가 되지 않습니다.

개별 버튼은 끌 수 없습니다

나가기 버튼과 더보기를 따로 숨기거나 배치를 바꾸는 옵션은 제공하지 않습니다.
나가는 길과 공유·문의 동선은 SDK가 책임지는 최소 범위이기 때문입니다.
배치도 우상단 하나뿐이라 고를 필요가 없습니다.

navigation 전체의 표시 여부는 아래 표시 제어로 바꿉니다.

표시 제어 ​

visible · show() · hide() 세 가지가 표시를 정합니다.
구성을 바꾸는 update() 는 표시를 건드리지 않습니다.

호출하는 일
init({ui: {navigation: {visible: false}}})숨긴 채 시작합니다
Gameplay.UI.Navigation.show()그때까지 쌓인 구성 그대로 표시합니다
Gameplay.UI.Navigation.hide()숨깁니다.
구성은 남아 있어 다시 show() 하면 그대로 돌아옵니다
Gameplay.UI.Navigation.update({...})구성만 바꿉니다.
숨긴 상태에서 불러도 뜨지 않습니다

표시와 구성이 다른 축이라, 구성은 미리 해두고 숨긴 채 시작할 수 있습니다.

ts
await Gameplay.init({
  gameName: '게임 이름',
  gameCode: 'sample-game',
  appId: 1234567,
  ui: {navigation: {visible: false, backgroundColor: '#000000'}},
});

// 숨긴 동안에도 구성은 바꿀 수 있습니다. 뜨지는 않습니다.
Gameplay.UI.Navigation.update({dropdown: {items: [{id: 'guide', label: '게임 방법'}]}});

// 이때 위에서 준 구성 그대로 뜹니다.
Gameplay.UI.Navigation.show();

숨긴 동안에는 사용자가 게임웹뷰를 벗어날 수 없습니다

나가기(닫기) 버튼이 navigation 안에 있습니다.
숨긴 동안 게임이 자체 종료 동선을 제공하지 않으면 사용자가 게임에서 빠져나갈 방법이 없습니다.

연출 때문에 잠깐 숨겼다면 반드시 되돌리세요.
특히 숨긴 상태에서 오류나 예외로 흐름이 끊기면 show() 가 영영 호출되지 않을 수 있으니, 복구 경로를 함께 두세요.

종료 경로가 있는지는 SDK가 보장하지 못하므로 심사에서 확인합니다.
자체 UI를 이미 가진 게임이 SDK를 단계적으로 들일 때 쓰는 값이며, 새로 연동하는 게임은 visible 을 건드리지 마세요.

나가기 버튼 ​

닫기 버튼을 누르면 SDK가 확인 팝업을 띄운 뒤 게임웹뷰를 닫습니다.
이 흐름 전체를 SDK가 갖습니다.

text
탭 → onClick → 확인 팝업 → 확인 / 취소 → onConfirm / onCancel → 닫기 / 팝업 해제

게임이 정할 수 있는 것은 각 지점에서 실행할 작업뿐입니다.
팝업을 띄우는 것도, 닫는 것도, 웹뷰를 종료하는 것도 SDK가 합니다.

필드타입설명
onClick() => Promise<void>버튼을 누른 직후.
확인 팝업을 열기 전에 실행합니다
onConfirm() => Promise<void>확인을 눌렀을 때.
웹뷰를 닫기 전에 실행하며 끝날 때까지 기다립니다
onCancel() => Promise<void>취소를 눌렀을 때

팝업 문구는 게임이 바꿀 수 없습니다.

요소값
제목init({gameName}) 으로 전달한 게임 이름
본문게임을 그만할까요?
버튼취소 · 확인

모든 게임에서 같은 화면이어야 하는 곳입니다.
게임마다 문구가 다르면 사용자가 무엇을 누르는지 매번 다시 읽어야 합니다.

ts
Gameplay.UI.Navigation.update({
  exitAction: {
    onConfirm: async (): Promise<void> => {
      await saveProgress();
    },
  },
});

onConfirm 이 끝난 뒤에 웹뷰가 닫히므로, 종료 직전의 저장이나 로그 전송을 여기에 넣을 수 있습니다.

플로팅 전환 버튼 ​

keepBrowserAction 은 게임웹뷰를 플로팅 뷰로 바꾸는 버튼입니다.
눌리면 SDK가 keepBrowser() 를 호출합니다.

이 기능을 지원하지 않는 환경에서는 false 를 주지 않아도 SDK가 알아서 숨깁니다.
게임이 톡 버전이나 플랫폼을 확인해 분기할 필요가 없습니다.

플로팅 전환 버튼이 없어 더보기와 닫기 두 개만 있는 navigation 예시

버튼이 빠지면 남은 버튼이 그 자리를 채웁니다.
위 예시가 자동으로 숨겨진 상태입니다.

더보기 메뉴 ​

더보기 메뉴에는 공유하기 와 문의하기 가 항상 들어가고, items 로 준 항목이 그 아래에 붙습니다.
두 기본 항목의 동작은 SDK가 갖습니다.

공유하기, 문의하기, 게임 가이드 항목이 펼쳐진 더보기 메뉴 예시

메뉴는 navigation 의 우측 끝을 기준으로 열립니다.
더보기 버튼 자체가 아니라 맨 오른쪽 버튼의 우측 끝에 맞으므로, 접기 버튼이 있든 없든 열리는 자리가 같습니다.

필드타입필수설명
idstring필수항목 식별자
labelstring필수표시 문구.
10자를 넘으면 말줄임 처리됩니다
disabledboolean비활성 여부
onClick(item: GameplayNavigationDropdownItemHandle) => Promise<void>눌렀을 때

disabled 는 선언할 때만 정하는 값이 아닙니다.
항목 하나만 고치는 방법이 두 가지 있습니다.

콜백 안에서는 인자로 받은 항목을 고칩니다.
진행 중에 같은 항목이 다시 눌리는 것을 막을 때 씁니다.

ts
Gameplay.UI.Navigation.update({
  dropdown: {
    items: [
      {
        id: 'reward',
        label: '보상 받기',
        onClick: async (item): Promise<void> => {
          item.update({disabled: true});
          await claimReward();
          item.update({disabled: false});
        },
      },
    ],
  },
});

메뉴는 항목을 선택하는 순간 닫히므로, 켜 둔 disabled 는 그 자리에서 보이지 않고 사용자가 메뉴를 다시 열었을 때 보입니다.

콜백 밖에서는 updateDropdownItem() 을 씁니다.

ts
Gameplay.UI.Navigation.updateDropdownItem('reward', {disabled: true});

label 을 다시 주지 않아도 남고, 다른 항목은 건드리지 않습니다.
없는 id 를 주면 아무 일도 하지 않으므로, 조건에 따라 항목이 없을 수 있는 화면에서 존재를 먼저 확인하지 않아도 됩니다.

메뉴가 열려 있어도 닫히지 않습니다.
상황에 따라 항목을 잠그는 것이 이 옵션의 쓰임이므로, 잠글 때마다 메뉴가 사라지면 쓸 수 없기 때문입니다.

update() 와의 차이

update({dropdown: {items}}) 는 목록 전체를 다시 선언합니다.
항목을 추가하거나 빼거나 순서를 바꿀 때 사용하세요.
값 하나만 바꿀 때 이 방법을 쓰면 나머지 항목과 label 까지 모두 다시 적어야 합니다.

Toast ​

ts
type GameplayToastApi = {
  configure(options: GameplayToastConfig): void;
  show(message: string, options?: GameplayToastOptions): GameplayToastId;
  dismiss(toastId?: GameplayToastId): void;
};

type GameplayToastConfig = {
  maxVisible?: number;
  position?: 'top' | 'bottom';
};

type GameplayToastOptions = {
  duration?: number;
};

type GameplayToastId = string;

짧은 안내를 화면 하단에 띄웁니다.
지정한 시간이 지나면 스스로 사라지므로 게임이 닫지 않아도 됩니다.

화면 하단에 한 줄 안내가 떠 있는 toast 예시
옵션타입설명
messagestring필수표시할 문구.
show() 의 첫 인자로 전달합니다
durationnumber기본값 5000표시 시간(ms).
최대 5000 이며 더 큰 값을 주면 5000 으로 잘립니다
maxVisiblenumber기본값 1동시에 보이는 개수.
최대 3 이며 더 큰 값을 주면 3 으로 잘립니다.
configure() 로 설정하며 초과분은 오래된 것부터 사라집니다
position'top' | 'bottom'기본값 'bottom'붙는 쪽.
configure() 로 설정합니다

maxVisible 을 늘리면 toast가 아래에서 위로 쌓입니다.
개수를 넘으면 오래된 것부터 사라집니다.

toast 세 개가 화면 하단에 위로 쌓여 있는 예시

개수를 넘겨 밀려나는 toast는 퇴장 연출 없이 바로 사라집니다.
페이드가 남으면 방금 띄운 toast와 사라지는 toast가 그 시간 동안 겹쳐 보입니다.
스스로 시간이 다 되어 닫히는 toast는 그대로 퇴장 연출을 씁니다: 다 읽고 사라지는 것과 새 알림에 밀려나는 것은 다른 사건입니다.

position: 'top' 은 'bottom' 을 축만 뒤집은 것입니다.
화면 위에 붙고, 위에서 내려오며, 아래로 쌓입니다.

ts
Gameplay.UI.Toast.configure({position: 'top'});

toast 하나하나가 아니라 전체 설정입니다.
show() 에는 줄 수 없습니다: toast마다 방향이 다르면 화면에 위아래로 갈린 두 무리가 남고, maxVisible 이 어느 쪽을 세는지 알 수 없어집니다.

값을 바꾸면 떠 있던 toast는 이전 위치에서 닫힙니다.
새 위치로 옮기지 않습니다: 옮기면 다음 show() 에서 그 toast들이 새 스택의 초과분으로 계산돼, 방금 띄운 것도 아닌 toast가 밀려나는 잔상이 보입니다.
게임 시작 시 한 번 정하고 쓰는 것을 권합니다.

디자인 가이드

position 은 'top' · 'bottom' 중 하나만 골라 게임 전체에서 그것만 쓰세요.

두 위치를 상황에 따라 번갈아 쓰면 사용자가 안내를 어디서 봐야 하는지 알 수 없어, 뜬 줄도 모르고 지나치는 toast가 생깁니다.
위치를 하나로 두면 그 자리가 "안내가 뜨는 곳" 으로 학습됩니다.

바꿔야 할 이유가 없다면 기본값 'bottom' 을 그대로 쓰는 것을 권합니다.

'top' 의 기준선은 화면 상단이 아니라 navigation 의 하단입니다.
navigation 이 표시된 상태라면 toast 가 그 아래에서 시작하므로 버튼을 가리지 않습니다.

navigation 상태'top' toast 의 기준선
표시됨 (기본값, 또는 Navigation.show() 호출 후)navigation 띠의 하단
표시되지 않음 (visible: false 이거나 hide() 호출 후)화면 상단 안전영역
navigation 버튼 아래에 한 줄 안내가 떠 있는 top 배치 toast 예시

기준선은 setStatusBarOverlay 값과 무관하게 navigation 과 같은 값을 씁니다.
전체화면을 켜고 끄면 navigation 과 toast가 함께 움직이므로 둘의 간격은 유지됩니다.

navigation 을 띄우지 않는 게임은 상단이 비어 있지 않을 수 있습니다

visible: false 로 navigation 을 띄우지 않으면 toast 가 화면 상단 안전영역까지 올라옵니다.
게임이 그 자리에 자체 UI(점수·재화 표시 등)를 두었다면 toast 가 그 위를 덮습니다: 다만 toast 는 탭을 가로채지 않으므로 가려진 버튼도 그대로 눌리며, 가리는 시간은 길어야 duration 만큼입니다.

show() 가 돌려주는 ID를 dismiss() 에 넘기면 그 toast만 닫습니다.
인자 없이 dismiss() 를 호출하면 열려 있는 toast를 모두 닫습니다.

ts
const toastId: GameplayToastId = Gameplay.UI.Toast.show('보상이 지급되었습니다.');

Gameplay.UI.Toast.dismiss(toastId);
ts
type GameplayModalApi = {
  open(options?: GameplayModalOptions): GameplayUiHandle<GameplayModalOptions>;
  confirm(options?: GameplayConfirmOptions): GameplayUiHandle<GameplayModalOptions>;
  close(): void;
};

type GameplayModalOptions = {
  title?: string;
  description?: string;
  primaryAction?: GameplayUiAction;
  secondaryAction?: GameplayUiSecondaryAction;
  dismissible?: boolean;
};

type GameplayConfirmOptions = Omit<GameplayModalOptions, 'primaryAction' | 'secondaryAction'> & {
  confirmLabel?: string;
  cancelLabel?: string;
  onConfirm?: (handle: GameplayUiHandle<GameplayModalOptions>) => Promise<void>;
  onCancel?: (handle: GameplayUiHandle<GameplayModalOptions>) => Promise<void>;
};

화면 가운데에 뜨는 대화상자입니다.
웹뷰 화면의 한가운데에 놓입니다: setStatusBarOverlay 로 전체화면을 켜면 상태바 영역까지 포함한 가운데, 끄면 그 아래 영역의 가운데입니다.

제목, 설명, 취소와 사용 버튼으로 구성된 modal 예시
옵션타입설명
titlestring제목
descriptionstring설명
dismissibleboolean기본값 false배경을 탭해 닫을 수 있는지.
bottom sheet와 기본값이 반대입니다
primaryActionGameplayUiAction주 버튼.
주지 않으면 닫기만 하는 확인 버튼이 자동으로 들어갑니다
secondaryActionGameplayUiSecondaryAction보조 버튼

modal은 버튼 없이 열 수 없습니다.
primaryAction 을 주지 않으면 SDK가 확인 버튼을 넣습니다: 닫을 방법이 없는 modal이 남는 것을 막기 위함입니다.

본문이 길면 본문 영역만 스크롤되고 제목과 버튼은 제자리에 남습니다.
본문 영역의 최대 높이는 512px 이며, 넘치면 그 안에서 세로로 스크롤됩니다.

본문이 512px 을 넘어 스크롤되고 제목과 하단 버튼은 고정된 modal 예시

title 은 스크롤 영역 밖에 있어 512px 에 포함되지 않고, 본문을 내려도 그대로 보입니다.
무엇에 대한 팝업인지가 사라지지 않게 하기 위함입니다. 제목은 2줄까지만 표시됩니다.

화면이 512px 보다 낮으면 본문 영역이 그만큼 줄어듭니다.
버튼이 화면 밖으로 밀려나지 않게 하기 위함이므로, 짧은 화면에서는 스크롤이 더 일찍 생깁니다.

긴 안내는 bottom sheet가 더 맞습니다

modal은 결정을 묻는 자리입니다.
읽을 것이 많아 스크롤이 필요한 내용이라면 BottomSheet 를 쓰는 편이 사용자에게 자연스럽습니다.

confirm() 은 확인·취소 두 버튼을 갖춘 modal을 짧게 여는 형태입니다.
버튼 객체 대신 문구와 콜백만 넘깁니다.

옵션타입설명
confirmLabelstring기본값 '확인'확인 버튼 문구
cancelLabelstring기본값 '취소'취소 버튼 문구
onConfirm(handle: GameplayUiHandle<GameplayModalOptions>) => Promise<void>확인을 눌렀을 때
onCancel(handle: GameplayUiHandle<GameplayModalOptions>) => Promise<void>취소를 눌렀을 때

confirm() 은 항상 버튼 두 개로 열리므로, 버튼이 하나인 대화상자가 필요하면 open() 을 쓰세요.

ts
Gameplay.UI.Modal.confirm({
  title: '아이템을 사용할까요?',
  description: '보유한 부활권 1개가 소모됩니다.',
  confirmLabel: '사용',
  onConfirm: async (): Promise<void> => {
    await useRevivalTicket();
  },
});

게임을 나가는 확인 창은 직접 만들지 않습니다

닫기 버튼을 누르면 SDK가 확인 팝업을 띄운 뒤 웹뷰를 닫습니다.
같은 화면을 confirm() 으로 또 만들면 문구가 게임마다 달라집니다.
나가기 버튼을 참고해 주세요.

BottomSheet ​

ts
type GameplayBottomSheetApi = {
  open(options?: GameplayBottomSheetOptions): GameplayUiHandle<GameplayBottomSheetOptions>;
  close(): void;
};

type GameplayBottomSheetHeight = 'auto' | 'full' | number;

type GameplayBottomSheetImage = {
  src: string;
  alt: string;
};

type GameplayBottomSheetOptions = {
  title?: string;
  dismissible?: boolean;
  height?: GameplayBottomSheetHeight;
  loading?: boolean;
  image?: GameplayBottomSheetImage;
  content?: string;
  primaryAction?: GameplayUiAction;
  secondaryAction?: GameplayUiSecondaryAction;
};

화면 아래에서 올라오는 시트입니다.
본문이 길면 시트가 커지는 대신 본문 영역 안에서만 스크롤됩니다.

제목, 본문, 취소와 노란색 확인 버튼으로 구성된 bottom sheet 예시
옵션타입설명
titlestring상단 제목.
한 줄을 넘으면 말줄임 처리됩니다
contentstring본문 텍스트
heightGameplayBottomSheetHeight기본값 'auto'아래 표 참고
dismissibleboolean기본값 true배경을 탭하거나 아래로 끌어 닫을 수 있는지
loadingboolean기본값 false본문 위에 스피너를 덮고 버튼을 비활성합니다
imageGameplayBottomSheetImage본문 맨 위에 놓을 이미지
primaryActionGameplayUiAction주 버튼
secondaryActionGameplayUiSecondaryAction보조 버튼.
색을 받지 않습니다

height 는 세 가지 형태를 받습니다.

값동작
'auto'내용이 높이를 정합니다.
아래 최소·최대 범위 안으로 맞춰집니다
'full'최대 높이
number그 픽셀값.
역시 최소·최대 범위 안으로 맞춰집니다

최소 높이는 224px, 최대 높이는 화면 높이 - (상단 안전영역 + 44px) 입니다.
기기마다 안전영역이 달라 실제 픽셀값도 다릅니다.

이 최대 높이는 navigation 표시 여부와 setStatusBarOverlay 값에 관계없이 같습니다.
전체화면을 켜고 끄더라도 시트의 천장은 움직이지 않습니다.

최대 높이로 열린 bottom sheet 예시 420px 고정 높이로 열린 bottom sheet 예시

왼쪽이 'full', 오른쪽이 420 입니다.
어느 쪽이든 본문이 넘치면 시트가 커지는 대신 본문 영역 안에서만 스크롤됩니다.

image 를 주면 본문 맨 위에 이미지가 붙습니다.

이미지 영역과 본문, 노란색 버튼으로 구성된 bottom sheet 예시
필드타입필수설명
srcstring필수이미지 주소.
https:, blob:, data:image/... 만 통과합니다
altstring필수대체 텍스트.
장식용이면 빈 문자열을 넣습니다

이미지는 크기 제한이 없습니다.
시트 폭을 넘으면 비율을 지킨 채 줄어들고, 넘지 않으면 원래 크기로 가운데에 놓입니다.
세로로 길면 본문과 함께 스크롤되며, 버튼 영역은 이미지 유무와 관계없이 같은 자리에 남습니다.

가로로 긴 이미지가 시트 폭에 맞춰 줄어든 예시 세로로 긴 이미지가 본문과 함께 스크롤되는 예시

왼쪽이 1280 x 720, 오른쪽이 720 x 1280 입니다.
두 경우 모두 버튼은 같은 자리에 있습니다.

주의사항

src 는 http: 를 받지 않습니다.
게임 페이지가 HTTPS로 열리므로 혼합 콘텐츠로 차단되기 때문입니다.
형식에 맞지 않으면 예외를 던지지 않고 이미지 영역만 그리지 않으므로, 이미지가 보이지 않으면 주소의 스킴을 먼저 확인하세요.

ts
Gameplay.UI.BottomSheet.open({
  title: '보상 받기',
  content: '일일 미션을 모두 완료했습니다. 지금 보상을 받으시겠습니까?',
  secondaryAction: {label: '다음에'},
  primaryAction: {
    label: '받기',
    color: 'yellow',
    onClick: async (sheet): Promise<void> => {
      sheet.update({loading: true});
      await claimReward();
    },
  },
});

버튼을 누르면 콜백이 끝난 뒤 시트가 스스로 닫힙니다.
콜백 안에서 close() 를 부르지 않아도 됩니다.

loading 을 켠 뒤 다시 끌 때 height 를 주지 않으면 현재 높이가 유지됩니다.
로딩이 끝나는 순간 시트 높이가 튀지 않게 하기 위함입니다.

Loader ​

ts
type GameplayLoaderApi = {
  show(options?: GameplayLoaderOptions): GameplayUiHandle<GameplayLoaderOptions>;
  hide(): void;
};

type GameplayLoaderOptions = {
  maxDuration?: number;
};

화면 가운데에 스피너를 띄웁니다.
네트워크 요청처럼 끝을 알 수 없는 대기 구간에 사용합니다.

modal 위에 원형 스피너가 겹쳐 떠 있는 loader 예시

loader는 모든 UI보다 위에 놓입니다.
modal이 떠 있는 상태에서 호출하면 위 예시처럼 modal 위에 겹칩니다.

옵션타입설명
maxDurationnumber기본값 10000자동으로 닫히는 시간(ms).
최대 10000

loader는 항상 뒤쪽 탭을 막습니다.
끄는 옵션은 없습니다: 막지 않으면 대기 중에도 사용자가 계속 누를 수 있어, 응답이 오는 사이 같은 동작이 여러 번 들어갑니다.

maxDuration 은 상한이자 기본값입니다.
응답이 오지 않아 hide() 를 부르지 못하는 상황에서도 스피너가 화면에 영구히 남지 않도록, 시간이 지나면 SDK가 닫습니다.

참고 문서 ​