개발 문서

HTML5·WebGL·WASM 게임을 네코랜드에 올리는 데 필요한 내용을 모았습니다. neko CLI, 설정 파일 nekogame.json, 게임 SDK(결제·클라우드 저장·로그인·친구), 게임 서버용 SDK까지 차례로 설명합니다.

개요

네코랜드는 웹 게임 플랫폼입니다. neko CLI 로 빌드 폴더를 올리면, 플레이어는 설치 없이 브라우저나 네코랜드 앱에서 바로 게임을 엽니다. 계정, 큐브(플랫폼 화폐), 결제, 클라우드 저장, 친구·채팅은 플랫폼이 맡습니다.

  • 게임은 자기만의 출처(origin)를 가진 iframe 안에서 돌아갑니다: https://{gameId}.games.nekoland.net/{version}/. 바깥의 헤더·대화상자·로딩 화면은 플랫폼이 그립니다.
  • 게임과 플랫폼을 잇는 통로는 SDK 하나뿐입니다. SDK 는 postMessage 로 바깥 페이지와 이야기하고, 구매·충전·로그인·친구 선택 창은 플랫폼이 iframe 바깥에 띄웁니다. 그래서 게임이 결제 창을 흉내 낼 수 없습니다.
  • 플레이어는 큐브로 결제합니다(1 큐브 = $0.01). 상품 가격은 큐브로 정하고, 결제 대행사를 따로 붙일 필요가 없습니다.
  • 같은 사람이라도 playerId 는 게임마다 다릅니다. 게임끼리 플레이어를 추적할 수 없게 하기 위해서입니다.
  • 네코랜드에는 리더보드가 없습니다. 순위표, 서버에서 관리하는 재화, 멀티플레이가 필요하면 게임의 자체 서버에 두세요(게임 서버 참고).

빠른 시작

Node.js 18.17 이상이 필요합니다. 게임 폴더에서:

bash
npm i -g @launchercapital/nekoland-cli
neko login
neko init --id gem-rush --title "Gem Rush" --category casual --build dist
# 게임에 SDK 를 붙이고 빌드한 뒤
neko validate
neko dev        # http://localhost:8080 에서 모의 플랫폼으로 테스트
neko deploy     # 테스트 버전: 미리보기 주소는 본인만
neko submit --checked  # 출시 전 점검 뒤 공개 (첫 출시는 심사)
  1. CLI 를 설치합니다. 새 버전이 있다고 나오면 같은 설치 명령을 한 번 더 실행하면 됩니다.
  2. neko login 을 실행하면 브라우저가 열리고 터미널에 8자리 코드(예: BCDF-GHJK)가 나옵니다. 로그인한 뒤 코드가 같은지 확인하고 [허용]을 누르세요. 브라우저를 열 수 없는 환경이면 neko login --no-browser 로 주소만 받아 다른 기기에서 열면 됩니다.
  3. neko init 으로 nekogame.json 을 만듭니다. 분류는 neko categories 에서 고르세요. 게임 ID 는 게임 주소가 되니 신중하게 정하세요.
  4. SDK 를 붙이고(SDK 설치) sdk.init() 을 호출합니다.
  5. neko validate 는 올리지 않고 설정·빌드 폴더·이미지만 검사합니다. 오류와 경고를 모두 고치세요.
  6. neko dev 는 빌드를 http://localhost:8080 에 띄우고 모의 플랫폼을 붙여 줍니다. 결제와 저장을 로컬에서 그대로 시험할 수 있습니다.
  7. neko deploy 로 테스트 버전을 올려 미리보기로 확인하고, neko submit 으로 공개합니다.
neko deploy 는 테스트 버전으로 올립니다. 공개되지 않고, 알려 주는 previewUrl 은 본인(과 심사자)만 열 수 있으며 거기서 한 구매는 테스트 구매입니다. 확인했으면 neko submit --checked 로 공개합니다(올리면서 바로 공개하려면 neko deploy --release --checked). --checked 없이 실행하면 출시 전 점검 목록(모바일 안전 영역·지원 언어·SDK 로딩·모바일 터치)을 보여 주고 멈춥니다. 웹 게임은 neko validate·neko deploy 가 코드에서 빠진 듯한 것을 "Release check" 경고로 알려 줍니다. 첫 출시는 심사를 거칩니다. 결과는 neko status 로 확인하고, 반려되면 reviewNote 에 이유가 나옵니다. 한 번 공개된 뒤에는 공개하는 즉시 바로 반영됩니다. 업데이트가 잘못됐으면 neko rollback 으로 이전 버전으로 되돌릴 수 있습니다.

AI 에이전트로 올리기

Claude Code·Codex·Cursor 같은 AI 코딩 에이전트로 개발한다면 올리는 과정 전체를 에이전트에게 맡길 수 있습니다. 게임 만들기 페이지(또는 개발자 콘솔의 AI로 게임 만들기 버튼)에서 프롬프트를 복사해, 게임 폴더에서 연 에이전트에 붙여 넣으세요.

프롬프트는 에이전트에게 이렇게 시킵니다.

  1. CLI 를 설치하고 neko login 실행 — 에이전트가 8자리 코드를 알려 주면 브라우저에서 허용합니다.
  2. neko skill install 로 네코랜드 스킬(SKILL.md)을 받아 그대로 따르기 — 이 문서를 에이전트용으로 압축한 안내서입니다.
  3. nekogame.json 만들기(게임 ID 는 여러분에게 물어봅니다), SDK 붙이기, 빌드 후 neko validate·neko dev 로 확인하기
  4. neko deploy 로 올리고 미리보기 주소 알려 주기
에이전트에게 비밀번호를 알려 주거나 토큰을 파일에 적을 필요는 없습니다. CI 에서는 개발자 콘솔에서 배포 전용 토큰을 만들어 NEKO_TOKEN 환경 변수로 넘기세요.

nekogame.json

게임 프로젝트 맨 위에 두는 설정 파일입니다. neko init 이 만들고, neko deploy 가 배포할 때마다 읽습니다.

json
{
  "gameId": "gem-rush",
  "title": "Gem Rush",
  "description": "One or two sentences shown on the game page",
  "category": "casual",
  "orientation": "landscape",
  "build": "dist",
  "thumbnail": "thumbnail.png",
  "products": [
    { "productId": "gems_500", "type": "consumable", "title": "500 Gems", "priceCoins": 100 },
    { "productId": "no_ads", "type": "non_consumable", "title": "Remove Ads", "priceCoins": 300 }
  ],
  "media": [
    { "type": "image", "file": "shots/1.png" },
    { "type": "youtube", "url": "https://youtu.be/..." }
  ],
  "links": { "x": "https://x.com/...", "youtube": "https://youtube.com/@...", "discord": "https://discord.gg/..." }
}
필드설명
gameId필수. 게임 주소이자 게임을 가리키는 ID 입니다. 소문자로 시작하는 소문자·숫자·하이픈 4~40자. 첫 배포 뒤에는 절대 바꾸지 마세요 — ID 가 바뀌면 다른 게임이 되고, 저장 데이터와 구매 내역이 넘어가지 않습니다.
title필수. 1~80자.
description게임 페이지에 보이는 한두 문장 소개(2,000자 이하). 심사 때 봅니다.
categoryneko categories 에 나오는 분류 ID (예: casual).
orientationany(기본), landscape, portrait. 가로·세로를 정하면 휴대폰을 반대로 들었을 때 "기기를 돌려 주세요" 안내가 나옵니다. 그래도 게임은 어떤 크기에든 맞아야 합니다.
languages게임이 지원하는 언어 목록(첫 번째가 기본). 예: ["ko", "en"]. 게임 상세 페이지에 표시됩니다. 빼면 지금 값을 그대로 두고, 새 게임은 한국어(ko)만입니다. 고를 수 있는 코드: ko en ja zh-CN zh-TW es fr de pt ru vi th id.
build맨 위에 index.html 이 있는 폴더. 500MB, 파일 1,500개까지.
thumbnail정사각형 이미지. 256×256 이상(512×512 권장), PNG·JPG·WebP, 2MB 이하. 심사 때 봅니다.
products게임 속 상품. productId: a-z 0-9 _ . 1~64자, type: consumable(소모성) 또는 non_consumable(영구, 나중에 못 바꿈), title, 선택 description, priceCoins: 양의 정수(큐브). 배포할 때마다 맞춰지고, 목록에서 뺀 상품은 판매가 중지됩니다. 소모성 상품에 "variablePrice": true 를 주면 가변 금액 상품이 되어 priceCoins 는 1회 최대 금액이고, 게임이 purchase({ productId, priceCoins }) 로 구매마다 금액(1~최대 정수)을 정합니다. 확인 창과 영수증에 그 금액이 나옵니다.
media선택, 10개까지: { "type": "image", "file" } 또는 { "type": "youtube", "url" }. 이미지는 16:9, 960×540 이상, 4MB 이하. 첫 이미지는 홈 화면 카드에도 쓰입니다.
links선택. x, youtube, discord (각 사이트의 https 주소).
threads선택. Unity·Godot 멀티스레드 빌드는 자동으로 감지해 격리(cross-origin isolated)된 페이지로 엽니다. 감지가 틀렸을 때만 true·false 로 지정하세요.

media 와 links 는 적어 두면 서버 내용을 그대로 바꾸고, 빼 두면 서버에 있는 것을 건드리지 않습니다.

게임 SDK

SDK(@launchercapital/nekoland-sdk, 현재 0.6.2)는 게임 안에서 돕니다. 로그인·결제·채팅 같은 나머지는 모두 플랫폼 쪽에서 처리됩니다.

설치

script 태그 — 번들러 없이 씁니다. 플랫폼(과 neko dev)이 /__neko/sdk.js 에서 SDK 를 내려 줍니다. 게임에서 쓸 수 있는 유일한 절대 경로입니다.

html
<script src="/__neko/sdk.js"></script>
<script>
  const { sdk, NekoSdkError } = window.Nekoland;
</script>

npm — 번들러를 쓴다면:

bash
npm i @launchercapital/nekoland-sdk
js
import { NekoSDK, NekoSdkError } from "@launchercapital/nekoland-sdk";

const sdk = new NekoSDK();

Unity·Godot 은 전용 래퍼가 있습니다 — Unity·Godot 참고.

초기화와 생명주기

js
// payments·storage·player 는 init 을 기다린 뒤에 (안 그러면 NOT_INITIALIZED)
// on()·track() 은 먼저 불러도 된다. init 이 끝나면 플랫폼 로딩 화면이 닫힌다
const player = await sdk.init({ gameId: "gem-rush" }); // nekogame.json 의 gameId

sdk.on("pause", () => { /* 게임 루프를 멈추고 소리를 끈다 */ });
sdk.on("resume", () => { /* 소리를 되돌린다 */ });
sdk.on("player.changed", () => { /* 로그인·계정 전환 → 저장을 다시 불러온다 */ });
  • init(options) 은 플레이어 정보를 돌려줍니다. 옵션: gameId(필수), manualReady, mode("auto" | "platform" | "local"), local(로컬 테스트 참고), timeoutMs(기본 10,000), extraHostOrigins(로컬 개발 전용).
  • pause 는 탭이 가려지거나, 앱이 백그라운드로 가거나, 구매 창이 열릴 때 옵니다. 게임 루프를 멈추고 소리를 끄세요. resume 은 돌아왔을 때 옵니다.
  • player.changed 는 게스트가 로그인하거나 계정이 바뀌면 옵니다. 저장을 다시 불러오세요. playerId 가 달라질 수 있습니다.
  • on() 은 구독을 끊는 함수를 돌려주고, init 전에 불러도 됩니다.

로딩이 긴 게임

기본으로는 init 이 끝나는 즉시 플랫폼 로딩 화면이 닫힙니다. Unity 처럼 큰 게임은 로딩 화면을 붙잡아 두고 진행 막대를 직접 채우세요.

js
await sdk.init({ gameId: "gem-rush", manualReady: true });

for (let i = 0; i < assets.length; i++) {
  await load(assets[i]);
  sdk.loadingProgress((i + 1) / assets.length); // 0..1
}
await sdk.gameReady(); // 첫 화면이 떴다 → 로딩 화면이 닫힌다

60초가 지나면 플랫폼이 로딩 화면을 어쨌든 닫습니다.

플레이 시간

js
sdk.gameplayStart(); // 메뉴를 벗어나 실제 플레이 시작, 일시정지 해제
sdk.gameplayStop();  // 메뉴·일시정지·게임 오버

플레이 시간을 정확히 재고 싶을 때만 쓰면 됩니다. 한 번도 부르지 않으면 게임이 화면에 보이는 시간 전체를 플레이 시간으로 셉니다. 둘 다 init 전에 불러도 됩니다.

플랫폼 메뉴와 화면 가장자리

js
// 플랫폼 메뉴 버튼(왼쪽 위)을 피해 HUD 배치
layoutHud(sdk.insets.menu); // { x, y, width, height } 또는 null
sdk.on("insets.changed", (i) => layoutHud(i.menu));

// Esc·메뉴 버튼 → 게임 일시정지 (pause + menu.open)
sdk.on("menu.open", () => showPausedOverlay());
sdk.on("menu.close", () => hidePausedOverlay());

// Esc 를 직접 쓰면 끄고, 게임의 일시정지 화면에서 메뉴 열기
await sdk.init({ gameId: "my-game", escapeMenu: false });
pauseScreen.onMenuClick = () => sdk.openMenu();
  • 게임 화면 왼쪽 위에 플랫폼 메뉴 버튼(나가기·친구·신고·도움말)이 떠 있습니다. 자리는 sdk.insets.menu({ x, y, width, height }, CSS 픽셀)이고, 화면이 돌면 insets.changed 이벤트가 옵니다. HUD 버튼은 이 자리를 피하세요.
  • 휴대폰의 노치·홈 표시줄 같은 안전 영역은 sdk.insets.safeArea({ top, right, bottom, left }, 각 가장자리에서 CSS 픽셀)입니다. 게임 iframe 안에서는 CSS env(safe-area-inset-*) 가 0 이라 이 값을 쓰세요. HUD·글자는 이 안쪽에 두고, 배경은 끝까지 그려도 됩니다.
  • 플레이어가 Esc 를 누르면 메뉴가 열리고 게임에 menu.open 과 pause 가, 닫히면 menu.close 와 resume 이 옵니다. 게임이 Esc 를 직접 써야 하면 init({ gameId, escapeMenu: false }) 로 끄고, 게임의 일시정지 화면에서 sdk.openMenu() 를 부르세요.

플레이어와 로그인

js
const { playerId, displayName, avatarUrl, isGuest, locale } = sdk.player;

if (sdk.player.isGuest) {
  try {
    await sdk.login(); // 플랫폼의 로그인·가입 창
  } catch {
    // 게스트로 남았다
  }
}
  • sdk.player: playerId(게임 안에서는 그대로, 게임마다 다름), displayName, avatarUrl(프로필 사진 전체 주소, 없으면 없음 — 캔버스에 그려도 되게 CORS 허용), isGuest, locale(예: ko-KR).
  • sdk.login() 은 게스트에게 로그인·가입 창을 띄우고, 회원이 되면 그 정보로 끝납니다(이미 회원이면 바로). 거절하면 reject 됩니다. 게스트가 가입해도 클라우드 저장은 그대로 남습니다.
  • sdk.platform: "web"(브라우저), "ios", "android"(네코랜드 앱). sdk.device: "mobile"(휴대폰·태블릿, 터치) 또는 "desktop"(컴퓨터). 터치 버튼·UI 크기를 정할 때 쓰고, 입력은 어느 쪽이든 터치·마우스·키보드를 모두 받으세요.
  • 다국어: sdk.player.locale 의 언어(예: ko-KR → ko)로 문구를 고르고, 지원하지 않는 언어면 기본 언어로 보여 주세요. 지원 언어는 nekogame.json 의 languages 에 적습니다.
  • sdk.sandbox 는 심사 전 미리보기와 로컬 모드에서 true 입니다(구매가 테스트 구매). sdk.isLocal 은 로컬 테스트 모드에서 true 입니다. 어느 쪽이든 게임은 똑같이 동작하면 됩니다.

결제

먼저 모든 상품을 nekogame.json 에 적으세요. neko dev 와 플랫폼은 거기 있는 상품만 팝니다. 그다음 구매가 사라지거나 두 번 지급되지 않도록 지급 → 저장 → consume 순서를 지키세요.

js
const CONSUMABLES = new Set(["gems_500"]); // 구매 기록에는 type 이 없다
const owned = new Set();                   // 영구 상품

async function grant(p) {
  if (!CONSUMABLES.has(p.productId)) { owned.add(p.productId); return; } // 영구 상품: 열어 주기만, consume 하지 않는다
  if (p.state !== "completed") return;                                   // 이미 지급 끝
  if (!save.grantedIds.includes(p.purchaseId)) {
    save.gems += 500;
    save.grantedIds = [...save.grantedIds, p.purchaseId].slice(-50);
    await sdk.storage.set({ save: JSON.stringify(save) });               // ① 지급을 먼저 저장
  }
  await sdk.payments.consume(p.purchaseId);                              // ② 그다음 consume
}

// 시작할 때마다: 결제됐지만 아직 지급하지 않은 것
for (const p of await sdk.payments.getPurchases()) await grant(p);

// 구매 버튼
try {
  await grant(await sdk.payments.purchase({ productId: "gems_500" }));
} catch (e) {
  if (e instanceof NekoSdkError && e.code === "USER_CANCELLED") return;
  // NOT_LOGGED_IN · INSUFFICIENT_COINS · ALREADY_OWNED · PURCHASE_PENDING · PRICE_CHANGED · INVALID_PRICE · PRODUCT_NOT_FOUND · TIMEOUT
}
  • purchase({ productId, developerPayload? }) 를 부르면 플랫폼이 iframe 바깥에 확인·충전·로그인 창을 띄웁니다. 게임에서 확인 창을 따로 그리지 마세요. 게스트에게는 로그인 안내가 자동으로 나옵니다. developerPayload 는 구매 기록과 영수증에 그대로 돌아옵니다.
  • getPurchases() 는 아직 consume 하지 않은 소모성 상품과 플레이어가 가진 모든 영구 상품을 돌려줍니다. 시작할 때마다 불러서 보유 상태를 되살리세요.
  • consume(purchaseId) 는 소모성 상품을 지급 완료로 표시합니다. 부르기 전까지는 getPurchases() 에 계속 나옵니다. 영구 상품은 consume 하지 마세요.
  • 구매 기록은 { purchaseId, productId, state, purchasedAt, developerPayload?, signedReceipt } 이고, state 는 completed, consumed, refunded 중 하나입니다.
  • "부활"처럼 사자마자 쓰는 아이템도 위와 같이 지급(개수 +1, 저장, consume)한 뒤, 게임 쪽 개수에서 하나 빼고 저장하세요.

상점 화면의 이름과 가격은 플랫폼에서 읽어 오세요.

js
const products = await sdk.payments.getProducts(); // 특정 상품만: getProducts(["gems_500"])
// [{ productId, type, title, description, priceCoins, variablePrice?, iconUrl? }]
오류 코드대응
USER_CANCELLED플레이어가 창을 닫았습니다. 무시하세요.
NOT_LOGGED_IN게스트가 로그인하지 않았습니다.
INSUFFICIENT_COINS충전 화면이 떴지만 큐브를 사지 않고 닫았습니다.
ALREADY_OWNED이미 가진 영구 상품입니다. 열어 주면 됩니다.
PURCHASE_PENDING결제가 아직 처리 중입니다. 나중에 getPurchases() 로 확인하세요.
PRICE_CHANGED확인 창을 띄운 뒤 가격이 바뀌었습니다. 상품을 다시 불러와 다시 시도하세요.
INVALID_PRICE가변 금액 상품에 priceCoins 가 없거나 1~최대 범위의 정수가 아닙니다.
PRODUCT_NOT_FOUND판매 중이 아닌 상품입니다(consume 에서는 없는 구매 ID). nekogame.json 을 확인하고 배포하세요.
TIMEOUT플랫폼이 제때 답하지 않았습니다.
게임 서버가 있다면 지급은 서버에서, 검증한 웹훅이나 영수증을 보고 하세요. 클라이언트 말만 믿고 지급하면 안 됩니다. 서버에서 구매 처리 참고.

클라우드 저장

js
// 값은 문자열이라 JSON 으로 저장
const raw = (await sdk.storage.get(["save"])).save; // 없는 키는 null
const save = raw ? JSON.parse(raw) : { gems: 0, grantedIds: [] };

save.gems += 10;
await sdk.storage.set({ save: JSON.stringify(save) }); // 다른 키는 그대로 두고 합쳐진다
  • 값은 문자열이라 JSON 으로 저장하세요. get(keys) 은 없는 키에 null 을 돌려주고, set(entries) 는 기존 키와 합칩니다.
  • 게임·플레이어마다 1MB. 넘기면 INVALID_ARGUMENT 로 거절됩니다.
  • localStorage 대신 sdk.storage 를 쓰세요. 기기를 바꿔도 따라오고, 게스트가 가입해도 남습니다. 미리보기의 저장 공간은 공개 게임과 따로입니다.

친구와 초대

친구는 플레이어의 네코랜드 계정에 있습니다. 친구 신청, 1:1 채팅, 접속 상태 표시는 게임 바깥에서 플랫폼이 맡고, 게임은 친구를 이 게임 기준 playerId 로만 봅니다(SDK 0.6.0 부터).

js
// [{ playerId, displayName, avatarUrl?, online, inGame, hasPlayed }] — 게스트는 빈 목록
const friends = await sdk.friends.list();
const playingNow = friends.filter((f) => f.inGame);

// 플랫폼 친구 선택 창 → 고른 친구의 네코랜드 채팅에 초대 카드
try {
  const { sent } = await sdk.friends.invite({ data: roomCode }); // data: 500자까지
} catch (e) {
  // USER_CANCELLED: 창을 닫음 · NOT_LOGGED_IN: 게스트가 로그인하지 않음
}

// 같이 플레이한 사람에게 친구 신청 (플랫폼이 확인 창을 띄움)
const { status } = await sdk.friends.add(otherPlayerId); // "sent"(상대 수락 대기) 또는 "friends"

// 친구의 초대로 열렸는지
if (sdk.invite) {
  const { fromPlayerId, fromName, data } = sdk.invite; // data 를 안 보냈으면 null
  joinRoom(data);
}
  • friends.list() → [{ playerId, displayName, avatarUrl?, online, inGame, hasPlayed }]. online: 지금 네코랜드에 접속 중(접속 상태는 기본 공개이고, 친구가 숨기면 false), inGame: 지금 이 게임을 하는 중, hasPlayed: 이 게임을 열어 본 적이 있음. 게스트는 빈 목록입니다.
  • friends.invite({ data? }) 는 플랫폼의 친구 선택 창을 엽니다. 고른 친구마다 네코랜드 채팅에 이 게임을 여는 초대 카드가 갑니다. 결과는 { sent }. 게스트는 먼저 로그인 안내를 받고(거절하면 NOT_LOGGED_IN), 창을 닫으면 USER_CANCELLED 입니다.
  • friends.add(playerId) 는 이 게임에서 만난 플레이어에게 친구 신청을 보냅니다. 플랫폼이 먼저 확인 창을 띄우고, 상대가 이미 나에게 신청해 둔 상태면 확인하는 순간 친구가 됩니다. 결과는 { status: "sent" | "friends" }("sent" 면 상대가 네코랜드에서 수락할 때까지 대기). 자기 자신이나 신청을 받을 수 없는 플레이어면 INVALID_ARGUMENT, 창을 닫으면 USER_CANCELLED 입니다.
  • sdk.invite 는 초대를 눌러 게임을 열었을 때 { fromPlayerId, fromName, fromAvatarUrl?, data }, 아니면 null 입니다.
  • data(500자까지)는 친구의 게임에 그대로 전달됩니다. 방 코드 같은 데 쓰고, 서버가 믿어야 하는 값은 넣지 마세요.
친구 기능은 게임에 도움이 될 때만 넣으세요(협동, 방 만들기, "지금 하는 친구" 등). 친구 목록과 채팅은 이미 플랫폼이 보여 줍니다. 게임 서버에서도 getFriends 로 같은 목록을 읽을 수 있습니다.

분석 이벤트

js
sdk.track("level_complete", { level: 3, stars: 2 });
sdk.track("tutorial_complete");
  • 이름: 소문자로 시작하는 소문자·숫자·_ 40자 이하. neko_ 로 시작하는 이름은 플랫폼 전용입니다.
  • 속성: 평평한 객체, 키 20개까지(이름 규칙 같음). 문자열(100자 이하)·유한한 숫자·불리언만, 전체 2KB 이하.
  • init 전에 불러도 되고(100개까지 쌓아 둠) 절대 예외를 던지지 않습니다. 규칙에 어긋난 이벤트는 버려지고, 로컬 모드에서는 콘솔에 경고가 나옵니다.
  • 추천 이름: level_start / level_complete / level_fail, tutorial_complete, item_earn / item_spend. 구매(플랫폼이 이미 앎)나 개인정보는 보내지 마세요.
  • neko events(-f 로 계속 보기)나 개발자 콘솔에서 볼 수 있습니다. 로딩 시간·오류·이탈은 자동으로 모입니다.

플레이어 토큰

내 게임 서버에 플레이어가 누구인지 증명하려면 플레이어 토큰을 보내고 서버에서 검증하세요.

js
// 게임에서
const { token, expiresAt } = await sdk.getPlayerToken(); // 1시간 유효
await fetch("https://api.example.com/session", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ token }),
});

토큰은 1시간 유효한 ES256 JWT 이고, 게스트도 받습니다. 만료되면, 그리고 player.changed 뒤에는 새로 받으세요. neko dev 에서 받은 토큰은 서명이 없어 검증에 실패하니, 서버 연동은 배포 후 미리보기에서 시험하세요.

게임 서버

@launchercapital/nekoland-server 는 순위표, 서버에서 관리하는 재화, 멀티플레이처럼 게임의 자체 백엔드가 있을 때 씁니다. 의존성이 없고 Web Crypto 로 동작합니다(Node 18+, Bun, Deno, 엣지 런타임).

bash
npm i @launchercapital/nekoland-server

플레이어 확인

ts
import { NekoServer, NekoServerError } from "@launchercapital/nekoland-server";

const neko = new NekoServer({
  gameId: "gem-rush",                             // nekogame.json 의 gameId
  apiKey: process.env.NEKO_GAME_API_KEY,          // 개발자 콘솔 → 서버 연동 (서버에만!)
  webhookSecret: process.env.NEKO_WEBHOOK_SECRET, // `neko webhook set` 이 한 번 보여 준다
});

// 게임이 sdk.getPlayerToken() 으로 받아 보낸 토큰
const player = await neko.verifyPlayerToken(token);
// { playerId, displayName, isGuest, sandbox, expiresAt }
  • verifyPlayerToken 은 서명·발급자·게임·종류·만료를 확인합니다. 서버에서는 playerId 를 계정 키로 쓰세요.
  • 토큰은 1시간짜리입니다. 오래 접속하는 게임이면 한 번 확인한 뒤 자체 세션을 쓰거나, 게임이 새 토큰을 보내게 하세요.
  • 미리보기에서 받은 토큰과 구매는 sandbox: true 입니다. 실제 기록과 섞이지 않게 따로 두세요.
  • 게임 API 키와 웹훅 시크릿은 서버 환경 변수에만 두세요. 게임 코드에 넣으면 안 됩니다.

서버에서 구매 처리

검증한 웹훅이나 클라이언트가 보낸 영수증(purchase.signedReceipt)을 보고 지급을 저장한 뒤 consume 하세요.

ts
await neko.verifyReceipt(signedReceipt);              // 클라이언트가 보낸 영수증 검증
await neko.getPurchase(purchaseId);
await neko.listPlayerPurchases(player.playerId);      // 놓친 구매 따라잡기
await neko.refund(purchaseId, "Accidental purchase"); // 개발자가 직접 환불

consume 은 여러 번 불러도 안전합니다. refund 는 구매자에게 큐브를 돌려주고 내 수익에서 빼며, purchase.refunded 웹훅이 뒤따릅니다. listPlayerPurchases(playerId, { all?, sandbox? }) 는 기본으로 아직 지급하지 않은 소모성 상품과 가진 영구 상품을 돌려줍니다.

웹훅

bash
neko webhook set https://api.example.com/nekoland  # 처음에 whsec_… 시크릿을 보여 준다
neko webhook test             # ping 을 보낸다
neko webhook                  # 주소와 최근 전송 기록
neko webhook secret --rotate  # 시크릿 다시 만들기
ts
// JSON 으로 바꾸기 전의 본문 문자열을 그대로
const ev = await neko.verifyWebhook(rawBody, req.headers.get("nekoland-signature"));
switch (ev.type) {
  case "purchase.completed":
    // 지급을 저장하고(ev.data.purchaseId 로 중복 제거) consume
    await neko.consume(ev.data.purchaseId);
    break;
  case "purchase.refunded":  /* 지급한 것을 회수 */ break;
  case "player.suspended":   /* 접속 중이면 끊기 */ break;
  case "player.updated":     /* 닉네임·게스트 여부가 바뀜 */ break;
}
  • 종류: purchase.completed, purchase.refunded(reason 포함), player.updated, player.suspended, player.unsuspended, ping. 플레이어 이벤트는 이 게임을 연 적이 있는 플레이어만 옵니다.
  • 10초 안에 2xx 로 답하세요. 아니면 1분, 5분, 30분, 2시간, 6시간 뒤에 다시 보냅니다. ev.id(와 purchaseId)로 중복을 걸러 내세요.
  • 패키지 없이 직접 구현한다면: 헤더는 Nekoland-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>, /v1 API 는 Authorization: Bearer <게임 API 키>, 토큰과 영수증은 ES256 JWT 입니다(공개 키 https://nekoland.net/.well-known/jwks.json, iss = nekoland, aud = gameId, sub = playerId).

서버에서 친구 목록

ts
const player = await neko.verifyPlayerToken(token);
const friends = await neko.getFriends(player.playerId);
// [{ playerId, hasPlayed }] — 내 게임 기준 playerId

getFriends(playerId) 는 플레이어의 네코랜드 친구를 내 게임 기준 playerId 와 hasPlayed 로 돌려줍니다. 플레이어 토큰을 먼저 검증하고 그 playerId 를 넘기세요. 친구 순위표나 "친구 방에 들어가기"에 쓸 수 있습니다.

서버 오류

NekoServerError.code뜻
INVALID_TOKEN서명이나 형식이 잘못됨
TOKEN_EXPIRED만료됨. 게임에서 sdk.getPlayerToken() 으로 새로 받기
WRONG_GAME다른 게임의 토큰·영수증·웹훅
WRONG_KIND플레이어 토큰 자리에 영수증을 넘긴 경우 등
INVALID_SIGNATURE웹훅 서명이 맞지 않음(시크릿과, 본문을 바꾸지 않았는지 확인)
STALE_WEBHOOK웹훅 시각이 너무 오래됨(재전송 공격 가능성)
CONFIGgameId, apiKey, webhookSecret 중 필요한 값이 없음
API_ERROR플랫폼 API 호출 실패 — status, apiCode 확인

로컬 테스트

neko dev 는 빌드를 http://localhost:8080(--port 로 변경)에 띄우고, SDK 는 로컬 모의 모드로 돕니다.

  • 플레이어는 "Local Tester" 입니다.
  • 상품은 nekogame.json 에서 읽습니다(고친 뒤에는 페이지를 새로 고침).
  • 구매·로그인·초대는 작은 테스트 창으로 확인합니다. 실제 결제는 없습니다.
  • 저장 데이터와 구매 내역은 브라우저 localStorage 에 남습니다.
  • 토큰과 영수증에는 서명이 없습니다. 게임 서버 연동은 배포 후 미리보기에서 시험하세요.
  • 왼쪽 위에 플랫폼 메뉴 버튼 자리(≡)가 보입니다. 누르거나 Esc 를 누르면 플랫폼처럼 menu.open·pause, 닫으면 menu.close·resume 이 옵니다. manualReady 인데 60초 안에 gameReady() 를 안 부르면 콘솔에 경고가 나옵니다.
  • 저장 규칙(문자열 값, 키 128자, UTF-8 기준 1MB)도 플랫폼과 같습니다.

다른 언어·휴대폰 화면은 주소 뒤에 붙여서 시험합니다. 예: http://localhost:8080/?neko_locale=en-US&neko_device=mobile&neko_safearea=47,0,34,0 (안전 영역은 위,오른쪽,아래,왼쪽 CSS 픽셀. 아이폰 세로 화면은 대략 47,0,34,0). 브라우저 개발자 도구의 기기 모드와 같이 쓰면 됩니다.

최상위 창에서 localhost, 127.0.0.1, file: 로 열면 로컬 모드가 자동으로 켜집니다. 다른 개발 서버(Vite 등)를 쓰면 옵션을 직접 넘기세요.

js
await sdk.init({
  gameId: "gem-rush",
  local: {
    products: [{ productId: "gems_500", type: "consumable", title: "500 Gems", priceCoins: 100 }],
    guest: true,         // 게스트로 시작 (로그인 흐름 테스트)
    autoApprove: false,  // true: 창 없이 승인 (자동 테스트용)
    friends: [
      { playerId: "p_friend1", displayName: "Mina", online: true, inGame: false, hasPlayed: true },
    ],
    invite: { fromPlayerId: "p_friend1", fromName: "Mina", data: "room-42" },
  },
});

그 밖의 옵션: displayName, 테스트에서 localStorage 대신 쓸 store, 주소 대신 코드로 정하는 locale·device·platform·safeArea, 결제 실패 처리를 시험하는 purchaseError(예: "INSUFFICIENT_COINS"). 모드를 고정하려면 mode: "local" 또는 "platform".

Unity·Godot

두 엔진 모두 같은 JS SDK 를 감싸서 씁니다. 프로젝트 폴더에서 neko sdk add 를 실행하고(업데이트할 때도 같은 명령), 복사된 README 를 따르세요.

bash
npm i -g @launchercapital/nekoland-cli
neko sdk add   # Unity·Godot 프로젝트 폴더에서
Unity·Godot 래퍼는 초기화·생명주기, 플레이어·로그인, 결제, 클라우드 저장, 분석 이벤트를 지원합니다. 친구·초대는 아직 JS SDK 에서만 쓸 수 있습니다.

Unity

Unity 2021.3 이상. 패키지는 Packages/com.nekoland.sdk 에 들어갑니다. 그다음 메뉴 Nekoland ▸ Setup WebGL Project 를 누르면 Nekoland WebGL 템플릿(/__neko/sdk.js 포함)을 설치하고, Brotli 압축과 Decompression Fallback 끄기를 적용합니다. 에디터에서는 모의 플랫폼으로 돌아서 Play 만 눌러도 전체 흐름을 시험할 수 있습니다.

csharp
using Nekoland;

async void Start()
{
    var player = await NekoSDK.InitAsync();             // 항상 맨 먼저
    NekoSDK.OnPause  += () => Time.timeScale = 0;
    NekoSDK.OnResume += () => Time.timeScale = 1;
    NekoSDK.OnPlayerChanged += async p => await LoadSave();

    await LoadSave();
    foreach (var p in await NekoSDK.Payments.GetPurchasesAsync()) await Grant(p);
    await NekoSDK.GameReadyAsync();                     // 플랫폼 로딩 화면을 닫는다
    NekoSDK.GameplayStart();
}

템플릿이 manualReady 로 초기화하므로 GameReadyAsync() 를 부를 때까지 로딩 화면이 남아 있습니다. nekogame.json 에는 "build": "Build/web"(WebGL 출력 폴더)을 적으세요. 한글을 쓰려면 한글 글꼴을 프로젝트에 넣어야 합니다.

Godot 4

애드온은 addons/nekoland 에 들어갑니다. 프로젝트 설정 ▸ 플러그인에서 Nekoland SDK 를 켜면 NekoSDK 오토로드가 등록됩니다. Web 내보내기 프리셋의 Head Include 에 <script src="/__neko/sdk.js"></script> 를 넣으세요. 비동기 메서드는 모두 { ok, data } 또는 { ok: false, code, message } 를 돌려줍니다.

gdscript
func _ready():
	var res := await NekoSDK.init()                 # 항상 맨 먼저
	if not res.ok: return
	NekoSDK.paused.connect(func(): get_tree().paused = true)
	NekoSDK.resumed.connect(func(): get_tree().paused = false)
	NekoSDK.player_changed.connect(func(p): load_save())

	await load_save()
	for p in (await NekoSDK.payments_get_purchases()).data: await grant(p)
	NekoSDK.gameplay_start()

Thread Support 는 꺼 두면 됩니다. 켜도 동작하지만(격리된 페이지로 열림) 오래된 브라우저에서는 실행되지 않습니다.

CLI 명령

모든 명령은 묻지 않고 끝까지 실행됩니다. --json 을 붙이면 결과를 JSON 으로 받습니다(오류는 {"error":{"message","code"}} 와 0 이 아닌 종료 코드). 전체 목록은 neko --help.

명령하는 일
neko login브라우저로 로그인. --no-browser 는 주소만 출력.
neko logout토큰을 취소하고 이 컴퓨터에서 지움.
neko whoami로그인한 계정과 토큰 권한.
neko games내 게임 목록.
neko categoriesnekogame.json 에 쓸 분류 ID.
neko initnekogame.json 만들기: --id --title --category --orientation --build --force.
neko dev모의 플랫폼이 붙은 로컬 테스트 서버. --port(기본 8080).
neko validate올리지 않고 설정·빌드·이미지 검사.
neko deploy테스트 버전으로 올리기 (공개 안 됨, 본인만 미리보기). --release --checked 를 붙이면 올리면서 공개(첫 출시는 심사).
neko status심사 상태, 반려 사유, 공개 중인 버전.
neko submit [versionId] --checked테스트로 올려 둔 버전을 공개 (첫 출시는 심사 요청). --checked 없이는 출시 전 점검 목록만 보여 줍니다.
neko rollback [versionId]공개 버전 바꾸기(기본: 바로 전 승인 버전).
neko products판매 중·중지된 상품.
neko purchases구매 내역. --player, --sandbox.
neko refund <purchaseId> --reason "…"구매 환불.
neko events최근 이벤트. -f 계속 보기, --names 요약, --event 거르기.
neko stats플레이어·리텐션·매출·퍼널·로딩 시간·오류. --days 7|30|90.
neko open게임 페이지 열기. --play, --console, 버전 ID 를 주면 그 버전 미리보기.
neko webhook …게임 서버 웹훅: set, test, secret [--rotate], remove.
neko sdk addUnity·Godot 프로젝트에 SDK 추가·업데이트.
neko skill installAI 에이전트용 SKILL.md 설치. --global 또는 --dir.

게임별 명령은 ./nekogame.json 의 gameId 를 쓰고, 다른 게임은 --game <gameId> 로 지정합니다. 환경 변수: NEKO_TOKEN(예: CI 의 배포 전용 토큰), NEKO_API_URL, NEKO_CREDENTIALS, NEKO_NO_UPDATE_CHECK=1.

문제 해결

증상해결
init 에서 TIMEOUT플랫폼이나 localhost 가 아닌 곳에서 열었습니다. neko dev 나 미리보기 주소를 쓰세요.
NOT_INITIALIZED다른 호출 전에 init 을 await 하세요.
Authentication failedneko login 을 다시 실행하세요.
TWO_FACTOR_SETUP_REQUIRED계정 보안 설정에서 2단계 인증을 켜세요.
"This token can't do that"권한이 제한된 토큰입니다(배포 전용·읽기 전용·한 게임 전용). 맞는 권한의 토큰을 쓰세요.
배포했는데 게임 목록에 없음심사 중입니다 — neko status. 그동안은 previewUrl 로 확인하세요.
업로드가 끊김같은 빌드로 neko deploy 를 다시 실행하면 빠진 부분만 올라갑니다.
게임에 상품이 안 보임nekogame.json 에 넣고 배포하세요. getProducts() 는 판매 중인 상품만 돌려줍니다.
파일을 못 불러옴상대 경로(./assets/x.png)를 쓰세요. 절대 경로는 /__neko/ 만 됩니다.

SDK 오류 코드

호출이 실패하면 NekoSdkError 로 reject 되니 e.code 를 보세요.

코드뜻
NOT_INITIALIZEDinit() 이 끝나기 전에 호출함
TIMEOUT플랫폼이 제때 답하지 않음
USER_CANCELLED플레이어가 창을 닫음(구매·로그인·친구 선택)
INSUFFICIENT_COINS큐브가 모자라고, 충전 화면을 그냥 닫음
PRODUCT_NOT_FOUND없는 상품(consume 에서는 없는 구매 ID)
ALREADY_OWNED이미 가진 영구 상품
PURCHASE_PENDING결제 처리 중. 나중에 getPurchases() 로 확인
PRICE_CHANGED확인 창 뒤에 가격이 바뀜. 다시 불러와 재시도
INVALID_PRICE가변 금액 상품의 priceCoins 가 없거나 범위(1~최대) 밖
NOT_LOGGED_IN회원이 필요한데 게스트로 남음
INVALID_ARGUMENT잘못된 입력(예: 저장 1MB 초과, 초대 data 500자 초과)
UNSUPPORTED플랫폼이 지원하지 않는 호출
INTERNAL플랫폼 내부 오류

빌드 체크리스트

  • 어떤 창 크기에도 맞추세요. 고정된 논리 해상도로 그리고 Math.min(innerWidth / W, innerHeight / H) 로 늘려 가운데에 두고, resize 때 다시 그리며, devicePixelRatio 는 2까지만.
  • 터치·마우스·키보드를 모두 지원하고 touch-action: none 을 주세요. 소리는 사용자 입력 뒤에 시작하세요.
  • 외부 광고, 다른 로그인, 다른 결제 수단은 넣을 수 없습니다.

수익과 정산

플레이어는 네코랜드에서 큐브를 사서 내 상품에 씁니다. 1 큐브 = 판매액 $0.01. 판매액의 70%가 크리에이터 몫이고, 아직 받지 않은 판매액이 $100을 넘으면 지급합니다. 개발자가 직접 환불한 금액은 수익에서 빠지고, 미리보기·로컬 모드의 테스트 구매는 포함되지 않습니다.

판매 현황은 개발자 콘솔, 수익 페이지, neko stats 에서 볼 수 있습니다. 자세한 조건은 수익 정책을 참고하세요.