개발 문서
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 이상이 필요합니다. 게임 폴더에서:
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 # 출시 전 점검 뒤 공개 (첫 출시는 심사)
- CLI 를 설치합니다. 새 버전이 있다고 나오면 같은 설치 명령을 한 번 더 실행하면 됩니다.
neko login을 실행하면 브라우저가 열리고 터미널에 8자리 코드(예:BCDF-GHJK)가 나옵니다. 로그인한 뒤 코드가 같은지 확인하고 [허용]을 누르세요. 브라우저를 열 수 없는 환경이면neko login --no-browser로 주소만 받아 다른 기기에서 열면 됩니다.neko init으로nekogame.json을 만듭니다. 분류는neko categories에서 고르세요. 게임 ID 는 게임 주소가 되니 신중하게 정하세요.- SDK 를 붙이고(SDK 설치)
sdk.init()을 호출합니다. neko validate는 올리지 않고 설정·빌드 폴더·이미지만 검사합니다. 오류와 경고를 모두 고치세요.neko dev는 빌드를http://localhost:8080에 띄우고 모의 플랫폼을 붙여 줍니다. 결제와 저장을 로컬에서 그대로 시험할 수 있습니다.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로 게임 만들기 버튼)에서 프롬프트를 복사해, 게임 폴더에서 연 에이전트에 붙여 넣으세요.
프롬프트는 에이전트에게 이렇게 시킵니다.
- CLI 를 설치하고
neko login실행 — 에이전트가 8자리 코드를 알려 주면 브라우저에서 허용합니다. neko skill install로 네코랜드 스킬(SKILL.md)을 받아 그대로 따르기 — 이 문서를 에이전트용으로 압축한 안내서입니다.nekogame.json만들기(게임 ID 는 여러분에게 물어봅니다), SDK 붙이기, 빌드 후neko validate·neko dev로 확인하기neko deploy로 올리고 미리보기 주소 알려 주기
NEKO_TOKEN 환경 변수로 넘기세요.nekogame.json
게임 프로젝트 맨 위에 두는 설정 파일입니다. neko init 이 만들고, neko deploy 가 배포할 때마다 읽습니다.
{
"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자 이하). 심사 때 봅니다. |
category | neko categories 에 나오는 분류 ID (예: casual). |
orientation | any(기본), 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 를 내려 줍니다. 게임에서 쓸 수 있는 유일한 절대 경로입니다.
<script src="/__neko/sdk.js"></script>
<script>
const { sdk, NekoSdkError } = window.Nekoland;
</script>
npm — 번들러를 쓴다면:
npm i @launchercapital/nekoland-sdk
import { NekoSDK, NekoSdkError } from "@launchercapital/nekoland-sdk";
const sdk = new NekoSDK();
Unity·Godot 은 전용 래퍼가 있습니다 — Unity·Godot 참고.
초기화와 생명주기
// 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 처럼 큰 게임은 로딩 화면을 붙잡아 두고 진행 막대를 직접 채우세요.
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초가 지나면 플랫폼이 로딩 화면을 어쨌든 닫습니다.
플레이 시간
sdk.gameplayStart(); // 메뉴를 벗어나 실제 플레이 시작, 일시정지 해제
sdk.gameplayStop(); // 메뉴·일시정지·게임 오버
플레이 시간을 정확히 재고 싶을 때만 쓰면 됩니다. 한 번도 부르지 않으면 게임이 화면에 보이는 시간 전체를 플레이 시간으로 셉니다. 둘 다 init 전에 불러도 됩니다.
플랫폼 메뉴와 화면 가장자리
// 플랫폼 메뉴 버튼(왼쪽 위)을 피해 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 안에서는 CSSenv(safe-area-inset-*)가 0 이라 이 값을 쓰세요. HUD·글자는 이 안쪽에 두고, 배경은 끝까지 그려도 됩니다. - 플레이어가
Esc를 누르면 메뉴가 열리고 게임에menu.open과pause가, 닫히면menu.close와resume이 옵니다. 게임이 Esc 를 직접 써야 하면init({ gameId, escapeMenu: false })로 끄고, 게임의 일시정지 화면에서sdk.openMenu()를 부르세요.
플레이어와 로그인
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 순서를 지키세요.
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)한 뒤, 게임 쪽 개수에서 하나 빼고 저장하세요.
상점 화면의 이름과 가격은 플랫폼에서 읽어 오세요.
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 | 플랫폼이 제때 답하지 않았습니다. |
클라우드 저장
// 값은 문자열이라 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 부터).
// [{ 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자까지)는 친구의 게임에 그대로 전달됩니다. 방 코드 같은 데 쓰고, 서버가 믿어야 하는 값은 넣지 마세요.
분석 이벤트
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로 계속 보기)나 개발자 콘솔에서 볼 수 있습니다. 로딩 시간·오류·이탈은 자동으로 모입니다.
플레이어 토큰
내 게임 서버에 플레이어가 누구인지 증명하려면 플레이어 토큰을 보내고 서버에서 검증하세요.
// 게임에서
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, 엣지 런타임).
npm i @launchercapital/nekoland-server
플레이어 확인
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 하세요.
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? }) 는 기본으로 아직 지급하지 않은 소모성 상품과 가진 영구 상품을 돌려줍니다.
웹훅
neko webhook set https://api.example.com/nekoland # 처음에 whsec_… 시크릿을 보여 준다
neko webhook test # ping 을 보낸다
neko webhook # 주소와 최근 전송 기록
neko webhook secret --rotate # 시크릿 다시 만들기
// 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>")>,/v1API 는Authorization: Bearer <게임 API 키>, 토큰과 영수증은 ES256 JWT 입니다(공개 키https://nekoland.net/.well-known/jwks.json,iss=nekoland,aud= gameId,sub= playerId).
서버에서 친구 목록
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 | 웹훅 시각이 너무 오래됨(재전송 공격 가능성) |
CONFIG | gameId, 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 등)를 쓰면 옵션을 직접 넘기세요.
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 를 따르세요.
npm i -g @launchercapital/nekoland-cli
neko sdk add # Unity·Godot 프로젝트 폴더에서
Unity
Unity 2021.3 이상. 패키지는 Packages/com.nekoland.sdk 에 들어갑니다. 그다음 메뉴 Nekoland ▸ Setup WebGL Project 를 누르면 Nekoland WebGL 템플릿(/__neko/sdk.js 포함)을 설치하고, Brotli 압축과 Decompression Fallback 끄기를 적용합니다. 에디터에서는 모의 플랫폼으로 돌아서 Play 만 눌러도 전체 흐름을 시험할 수 있습니다.
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 } 를 돌려줍니다.
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 categories | nekogame.json 에 쓸 분류 ID. |
neko init | nekogame.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 add | Unity·Godot 프로젝트에 SDK 추가·업데이트. |
neko skill install | AI 에이전트용 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 failed | neko 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_INITIALIZED | init() 이 끝나기 전에 호출함 |
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 에서 볼 수 있습니다. 자세한 조건은 수익 정책을 참고하세요.