> For the complete documentation index, see [llms.txt](https://onestore-dev.gitbook.io/dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onestore-dev.gitbook.io/dev/tools/web-sdk/lifecycle.md).

# 03. Lifecycle 관리

#### **SDK 로드 순서: createSDK부터 startGameAsync까지**

게임은 아래 순서를 반드시 지켜야 합니다. 순서를 위반하면 `sdk_not_initialized` 오류가 발생합니다.

1. `createSDK()` : SDK 싱글턴 획득
2. `initializeAsync()` : 초기화. 앱과 핸드셰이크, 게임·환경 정보 수신
3. `setLoadingProgress(n)` : 게임 리소스 로딩 진행률 보고 (0\~100, 반복 호출)
4. `startGameAsync()` : 로딩 완료 선언. 앱이 로딩 화면을 제거하고 게임을 노출

```javascript
import { createSDK } from "https://h5sdk.onestore.net/lib/{{version}}/onestore-h5-sdk.min.js";

var sdk = createSDK();

// 리스너는 initializeAsync() 이전에 등록해 두는 것을 권장합니다 ('이벤트 리스너' 절 참고)
sdk.on("pause",  onPause);
sdk.on("resume", onResume);
sdk.on("exit",   onExit);

sdk.initializeAsync()
  .then(function (info) {
    if (info.err) {
       // 미지원 환경 — 초기화 실패에 대한 고객 알림 후 종료 처리
       return;
    }
    applyEnvironment(info);          // locale · ringerSilent · safeArea 반영
    startLoading();
  })
  .catch(function (e) {
    // 초기화 실패(앱 응답 없음 등)
    console.error("initializeAsync 실패", e.reason, e.message);
  });

function startLoading() {
  var timer = setInterval(function () {
    var progress = loadResources();  // 0 ~ 100
    sdk.setLoadingProgress(progress);

    if (progress >= 100) {
      clearInterval(timer);
      sdk.startGameAsync()
        .then(function () {
          // 네이티브 로딩 UI 제거 완료 — 이 시점부터 게임 화면이 보여지게 됩니다.
          showGame();
        })
        .catch(function (e) {
          console.error("startGameAsync 실패", e.reason, e.message);
        });
    }
  }, 100);
}
```

`{{version}}` 자리에 넣을 버전은 [08. 릴리즈 노트](https://onestore-dev.gitbook.io/dev/tools/web-sdk/releasenote)에서 확인하시기 바랍니다.

**동작 특성**

<table><thead><tr><th width="200">항목</th><th>설명</th></tr></thead><tbody><tr><td><code>createSDK()</code></td><td>SDK 객체를 반환합니다. 이 객체를 이용하여 로딩 상태/실행/종료/이벤트 수신 등을 설정할 수 있습니다. 몇 번을 호출해도 같은 인스턴스가 반환됩니다.</td></tr><tr><td><code>initializeAsync()</code></td><td><p>성공 시 <strong>멱등</strong>입니다. 재호출하면 같은 결과를 반환합니다. 실패 시에는 내부 상태가 초기화되므로 다시 호출해 재시도할 수 있습니다.</p><p>미지원 환경(일반 브라우저 등)인 경우 <code>{ err: "Platform Not Supported" }</code>를 반환합니다. 에러가 없을 경우 <code>err</code> 필드는 생략됩니다.</p><p>초기화 실패(앱 응답 없음 등)인 경우 오류가 발생하며, 오류의 <code>e.reason</code>은 <a href="https://onestore-dev.gitbook.io/dev/tools/web-sdk/errors">오류 사유</a>입니다 (예: <code>timeout</code>).</p></td></tr><tr><td><code>startGameAsync()</code></td><td><p><code>initializeAsync</code> 완료 전에 호출하면 에러를 반환합니다. <code>e.reason === "sdk_not_initialized"</code></p><p>정상인 경우 앱 로딩 화면이 제거되고 게임 화면이 보여집니다.</p></td></tr></tbody></table>

{% hint style="info" %}
**오류 객체 형태**

Promise가 reject될 때 전달되는 오류는 `e.reason`(분기용 사유 문자열) · `e.message`(로그용 문구)를 갖습니다. **숫자 오류 코드는 전달되지 않습니다.** 상세는 [06. 오류 사유 (reason)](https://onestore-dev.gitbook.io/dev/tools/web-sdk/errors)을 참고하시기 바랍니다.
{% endhint %}

#### **로딩 상태 표시: setLoadingProgress**

게임 리소스를 내려받는 동안 진행률을 보고하면, 앱의 네이티브 로딩 화면에 진행률이 표시됩니다. 별도 로딩 화면을 만들 필요가 없습니다.

* 로딩 화면의 제거 시점은 `setLoadingProgress(100)`이 아니라 `startGameAsync()` 성공 시점입니다.
* 응답을 기다리지 않는 단방향 통지입니다. 호출이 게임 성능에 영향을 주지 않으며 반환값이 없습니다.
* `progress`는 0\~100 범위의 숫자입니다. 범위 밖 값은 0\~100으로 잘라서 반영됩니다.

#### **initializeAsync 응답**

`initializeAsync()`가 돌려주는 값은 아래 항목뿐입니다.

<table><thead><tr><th width="160">필드</th><th width="110">타입</th><th>설명</th></tr></thead><tbody><tr><td>playerId</td><td>string</td><td>게임별 가명 유저 ID. 게임사 서버가 진행 데이터를 저장하는 키</td></tr><tr><td>locale</td><td>string</td><td>단말 UI 언어 (BCP-47, 예: ko-KR)</td></tr><tr><td>ringerSilent</td><td>boolean</td><td>단말 무음 모드 여부. BGM 초기 상태 결정에 활용합니다. 이후 변경은 <code>resume</code> 이벤트로 통지됩니다 ('이벤트 리스너' 절 참고)</td></tr><tr><td>safeArea</td><td>object</td><td>시스템바를 피해야 하는 인셋(px): <code>{top, bottom, left, right}</code></td></tr><tr><td>err</td><td>string?</td><td>미지원 환경에서만 존재. 값이 있으면 위 필드들은 제공되지 않습니다</td></tr></tbody></table>

* 이 정보는 초기화 시점의 1회 스냅숏입니다. 단, `ringerSilent`는 이후 `resume` 이벤트에 변경값이 실려 올 수 있습니다.
* `safeArea`는 변경할 수 없는(동결된) 객체이며, 같은 응답 객체에서 여러 번 읽어도 항상 같은 값을 돌려줍니다.
* 내부 운영 필드(세션 정보 · 앱 버전 등)는 제공되지 않습니다.

{% hint style="info" %}
기능 지원 여부는 **기능별 API**로 확인합니다. 광고는 `sdk.ads.isSupported()`([04. 인앱 광고 (IAA)](https://onestore-dev.gitbook.io/dev/tools/web-sdk/iaa)), 결제 · 상품 정보 조회는 `sdk.iap.isSupported(feature)`([05. 인앱 결제 (IAP)](https://onestore-dev.gitbook.io/dev/tools/web-sdk/iap))입니다. 앱 버전에 따라 지원 기능이 다를 수 있으므로 광고 버튼 · 상점 가격 표시 등 UI 노출 전에 확인하십시오.
{% endhint %}

#### **이벤트 리스너: pause / resume / exit**

앱 전환·광고 표시·종료 등 게임 밖에서 발생하는 상황은 SDK 이벤트로 전달됩니다. `on` / `once` / `off`로 리스너를 등록·해제합니다. 리스너는 `initializeAsync()` 호출 전에 등록해 두는 것을 권장합니다. 초기화 직후 이벤트가 연달아 도착할 수 있습니다.

등록 이벤트는 `"pause"` · `"resume"` · `"exit"`입니다.

<table><thead><tr><th width="90">이벤트</th><th width="220">페이로드</th><th>발생 시점</th><th>게임이 해야 할 일</th></tr></thead><tbody><tr><td>pause</td><td><code>{reason: "background" | "ad" | "payment"}</code></td><td>앱이 백그라운드로 전환되거나, 광고 노출을 위해 게임을 멈춰야 할 때</td><td>게임 루프 · BGM · 효과음을 즉시 정지합니다</td></tr><tr><td>resume</td><td><code>{ringerSilent?: boolean}</code></td><td>게임이 다시 실행되어야 할 때. <code>ringerSilent</code>는 pause~resume 사이 무음 모드가 변경된 경우에만 포함됩니다 (변경 없으면 필드 생략)</td><td>게임 재개</td></tr><tr><td>exit</td><td><code>{}</code></td><td><p>앱이 게임 종료를 요청할 때. 백키 종료 확인 팝업에서 사용자가 '종료'를 선택한 경우, 앱이 게임 화면을 닫는 경우 등</p><p>게임이 <code>restart()</code>를 호출한 경우 (재시작 직전 저장 기회)</p><p>※ 게임이 <code>quit()</code>을 직접 호출한 경우에는 발생하지 않습니다</p></td><td>진행 상황을 저장합니다. <code>quit()</code>을 다시 호출할 필요는 없습니다. 저장 후 종료 마무리는 SDK가 자동 처리하고, 종료·재시작 절차는 앱이 계속 진행합니다.</td></tr></tbody></table>

```javascript
sdk.on("pause", function (ev) {
  // ev.reason 은 "background" | "ad" | "payment"
  pauseGameLoop();
  muteAll();
});

sdk.on("resume", function (ev) {
  // ev.ringerSilent — 무음 모드가 변경된 경우에만 존재
  if (typeof ev.ringerSilent === "boolean") applyRingerState(ev.ringerSilent);
  resumeGameLoop();
});

sdk.on("exit", function () {
  saveProgress();   // 저장 — 동기적으로, 0.5초 안에 끝나는 작업만 수행하십시오
});
```

{% hint style="warning" %}
**주의 사항**

* `exit`은 게임이 호출하지 않아도 발생합니다. 사용자가 백키·앱 조작으로 게임을 닫는 경우가 가장 흔한 경로이므로, **저장 로직은 exit 리스너에 두어야** 진행 데이터가 유실되지 않습니다.
* 저장은 exit 수신 후 **0.5초** 안에, 동기적으로 끝내야 합니다. 앱이 그 안에 게임 화면을 닫으므로 오래 걸리는 작업은 피하십시오.
* `sdk.quit()`을 게임이 직접 호출한 경우에는 `exit`이 발생하지 않습니다. 이미 게임이 종료를 결정한 상황이기 때문입니다. 이 경로에서는 `quit()` 호출 전에 저장을 마치십시오.
* `exit` 리스너를 등록하지 않은 게임은 종료 요청 시 즉시 종료됩니다.
* 광고 표시 구간에는 `pause` / `resume` 이벤트가 전달됩니다. 광고 API를 호출한 쪽에서 별도로 게임을 멈출 필요 없이, pause 리스너 한 곳에서 일관되게 처리하면 됩니다.
* Android 백키는 `on()` 이벤트가 아니라 `onBackPressed()` 핸들러로 처리합니다 ('백키 처리' 절 참고). 반환값으로 소비 여부를 즉시 회신해야 하기 때문입니다.
  {% endhint %}

#### **종료 / 재시작: quit / restart**

<table><thead><tr><th width="150">API</th><th width="140">파라미터 / 반환</th><th>동작</th></tr></thead><tbody><tr><td><code>sdk.quit()</code></td><td>없음 / 없음</td><td>게임이 스스로 종료하겠다는 요청입니다. 앱의 종료 확인 팝업을 거치지 않고 즉시 종료 절차가 진행됩니다. 이 경우 <code>exit</code> 이벤트는 발생하지 않으므로, 저장이 필요하면 호출 전에 마치십시오.</td></tr><tr><td><code>sdk.restart()</code></td><td>없음 / 없음</td><td>게임 새로고침(재시작) 요청입니다. 호출하면 <code>exit</code> 이벤트가 도착해 저장 기회가 주어지고, 앱이 게임 페이지를 다시 로드합니다. exit 처리에서는 저장만 하면 되며 <code>quit()</code> 재호출은 불필요합니다. 리로드 후에는 <code>createSDK()</code> → <code>initializeAsync()</code>부터 다시 시작합니다.</td></tr></tbody></table>

{% hint style="danger" %}
`restart()`는 페이지를 새로 로드합니다. `playerId`는 유지되지만, 저장하지 않은 진행 데이터는 유실됩니다. `exit` 이벤트에서 반드시 저장하십시오.
{% endhint %}

#### **백키 처리: onBackPressed**

Android 백키를 게임이 우선 처리할 수 있습니다. 핸들러가 `true`를 반환하면 게임이 처리한 것으로 간주하고(예: 인게임 팝업 닫기), `false`를 반환하거나 핸들러가 없으면 앱의 기본 동작(종료 확인 팝업)이 진행됩니다.

```javascript
sdk.onBackPressed(function () {
  if (isPopupOpen()) {
    closePopup();
    return true;    // 게임이 소비 — 앱은 아무 동작도 하지 않습니다
  }
  return false;     // 미소비 — 앱의 종료 확인 팝업이 진행됩니다
});
```

* 핸들러는 **동기 함수**여야 합니다. 앱이 200ms 안에 회신을 기다리므로 async 함수·오래 걸리는 작업은 사용할 수 없습니다. 제때 회신하지 못하면 미소비로 처리됩니다.
* 핸들러에서 예외가 발생하면 미소비(false)로 간주됩니다.
* 반환값이 정확히 `true`일 때만 소비로 처리됩니다.
* 미소비로 처리되어 사용자가 종료 확인 팝업에서 '종료'를 선택하면 `exit` 이벤트가 도착합니다 ('이벤트 리스너' 절 참고).

#### **외부 링크 이동: openDeeplink**

외부 링크로 이동합니다. `sdk.openDeeplink(url)`로 호출하며 Promise를 반환합니다.

<table><thead><tr><th width="120">파라미터</th><th width="90">타입</th><th width="80">필수</th><th>설명</th></tr></thead><tbody><tr><td>url</td><td>string</td><td>필수</td><td>이동 대상 URL. 허용 도메인 검증은 앱이 수행하며, 검증 실패는 <code>reason: "invalid_params"</code>로 reject됩니다 (화이트리스트 밖 · 형식 오류를 구분해 주지 않습니다)</td></tr></tbody></table>

```javascript
sdk.openDeeplink("https://onestore.co.kr/event/summer")
  .catch(function (e) {
    console.warn("이동 실패", e.reason, e.message);   // 예: "invalid_params"
  });
```
