익명 키를 활용한 게임 세션 식별 패턴
게임 미니앱의 흔한 UX 목표는 하나입니다. "일단 플레이부터 시키고, 로그인은 점수를 등록할 때만." getAnonymousKey가 그 패턴을 구현하는 기반입니다. 토스 계정 없이도 디바이스 단위로 진행도를 식별하고, 리더보드 등록 직전에만 appLogin을 호출하면 됩니다.
이 가이드는 익명 키의 성격과 수명, 표준 패턴 두 가지, anonymous→logged-in 마이그레이션, 서버 측 주의 사항, 자주 빠뜨리는 실수를 한 곳에 정리합니다.
getAnonymousKey vs getUserKeyForGame vs appLogin
| 항목 | getAnonymousKey | getUserKeyForGame | appLogin |
|---|---|---|---|
| 식별 범위 | 디바이스/미니앱 단위 | 게임 카테고리 미니앱 한정 | 토스 사용자 계정 |
| 로그인 필요 | 없음 | 없음 | 있음 (동의 다이얼로그 표시) |
| 반환 형태 | { type: 'HASH'; hash: string } | { key: string } | { authorizationCode; referrer } |
| 앱 재설치 후 | 새 키 발급 가능성 있음 | 새 키 발급 가능성 있음 | 동일 계정으로 재로그인 가능 |
| 다른 디바이스 | 키가 다름 | 키가 다름 | 동일 계정으로 로그인 가능 |
| 서버 검증 | 클라이언트 전송 후 rate-limit 등 자체 보호 필요 | 동일 | SDK 인가 코드 교환으로 서버 검증 가능 |
| 상태 | 현행 권장 | Deprecated — getAnonymousKey 사용 | 현행 권장 |
getUserKeyForGame은 더 이상 사용되지 않습니다. 신규 코드는 getAnonymousKey를 사용하세요. 반환 타입은 다르지만({ key: string } vs { type: 'HASH'; hash: string }) 목적과 수명 특성은 동일합니다. 게임 카테고리 미니앱 외에서 getUserKeyForGame을 호출하면 'INVALID_CATEGORY'가 반환되는 점도 차이입니다.
표준 패턴 1 — 익명 진행도 저장
로그인 없이 게임 진행도를 서버에 저장하는 기본 패턴입니다.
[미니앱 실행]
│
▼
getAnonymousKey() → hash
│
▼
서버에 hash를 userId로 등록/조회
│
▼
점수·언락·인벤토리 저장
구현 예제
import { getAnonymousKey } from '@apps-in-toss/web-framework';
import { useEffect, useState } from 'react';
// 앱의 실제 서버 API로 대체하세요.
declare function initGameSession(anonymousId: string): Promise<{
score: number;
level: number;
}>;
type SessionState =
| { status: 'loading' }
| { status: 'ready'; anonymousId: string; score: number; level: number }
| { status: 'unsupported' }
| { status: 'error'; message: string };
function useGameSession() {
const [session, setSession] = useState<SessionState>({ status: 'loading' });
useEffect(() => {
(async () => {
const result = await getAnonymousKey();
if (result === undefined) {
setSession({ status: 'unsupported' });
return;
}
if (result === 'ERROR') {
setSession({ status: 'error', message: '세션 초기화에 실패했어요.' });
return;
}
const { hash } = result;
const gameData = await initGameSession(hash);
setSession({ status: 'ready', anonymousId: hash, ...gameData });
})();
}, []);
return session;
}
hash를 서버의 user_id 대신 사용하면 로그인 없이도 점수, 언락 아이템, 인벤토리를 저장할 수 있습니다. 단, 보안 민감 데이터(결제 정보, 개인정보 등)는 익명 키만 신뢰하지 마세요 — 클라이언트가 다른 hash를 위조해 전송할 수 있습니다.
표준 패턴 2 — 점수 등록 시점에 appLogin 트리거
리더보드에 이름을 올리는 순간, 사용자는 처음으로 신원이 필요해집니다. 그 시점에만 appLogin을 호출합니다.
[점수 달성 → 리더보드 등록 버튼 클릭]
│
▼
appLogin() → authorizationCode
│
▼
서버: authorizationCode → 토스 토큰 교환 → userId 확정
│
▼
서버: 기존 anonymous hash → userId 매핑 (1회성 마이그레이션)
│
▼
리더보드에 닉네임과 점수 등록
구현 예제
import { appLogin, getAnonymousKey } from '@apps-in-toss/web-framework';
// 앱의 실제 서버 API로 대체하세요.
declare function exchangeAndMigrate(params: {
authorizationCode: string;
referrer: string;
anonymousId: string;
}): Promise<{ userId: string }>;
declare function submitScore(params: {
userId: string;
score: number;
}): Promise<void>;
async function handleLeaderboardSubmit(score: number): Promise<void> {
// 1. 현재 익명 ID를 먼저 확보한다.
const keyResult = await getAnonymousKey();
if (keyResult === undefined || keyResult === 'ERROR') {
// 익명 키 조회 실패 — 로그인만 진행하거나 사용자에게 안내.
return;
}
// 2. 로그인 다이얼로그를 표시한다.
const { authorizationCode, referrer } = await appLogin();
// 3. 서버에서 코드 교환 + 익명 진행도 마이그레이션.
const { userId } = await exchangeAndMigrate({
authorizationCode,
referrer,
anonymousId: keyResult.hash,
});
// 4. 확정된 userId로 점수 등록.
await submitScore({ userId, score });
}
authorizationCode는 일회용이며 짧은 유효 기간을 가집니다. 서버 측 토큰 교환 흐름은 Guides — 토스 로그인 흐름을 참고하세요.
getUserKeyForGame의 위치
getUserKeyForGame은 게임 카테고리 미니앱에서만 동작하는 구버전 API입니다. 게임 카테고리가 아닌 환경에서 호출하면 'INVALID_CATEGORY'가 반환됩니다. 기존 코드에서 이미 사용 중이라면 아래와 같이 마이그레이션하세요.
// 이전
import { getUserKeyForGame } from '@apps-in-toss/web-framework';
const result = await getUserKeyForGame();
// result: { key: string } | 'INVALID_CATEGORY' | 'ERROR' | undefined
// 권장
import { getAnonymousKey } from '@apps-in-toss/web-framework';
const result = await getAnonymousKey();
// result: { type: 'HASH'; hash: string } | 'ERROR' | undefined
필드 이름만 달라질 뿐(key → hash), 의미와 수명은 같습니다. 서버에 저장된 기존 key 값과의 매핑이 필요하다면 마이그레이션 엔드포인트를 한 번 만들어 두세요.
수명과 재설치 케이스
익명 키의 수명은 SDK와 플랫폼이 내부적으로 결정합니다. 개발자가 직접 만료 시간을 제어할 수 없습니다. 실제 동작에서 알아야 할 내용은 다음과 같습니다.
- 같은 디바이스, 앱 유지: 키가 안정적으로 유지됩니다.
- 앱 재설치: 새 키가 발급될 가능성이 높습니다. 이전 익명 진행도를 계속 이어갈 수 없습니다.
- 다른 디바이스: 디바이스가 달라지면 키가 달라집니다. 동일인도 다른 사용자로 취급됩니다.
이 특성 때문에 장기적인 사용자 진행도는 익명 키만으로 보호할 수 없습니다. 사용자가 디바이스를 교체하거나 앱을 재설치했을 때 진행도를 잃지 않게 하려면 결국 appLogin을 통한 계정 연동이 필요합니다. 이 지점을 UX에서 자연스럽게 유도하는 것이 패턴 2의 핵심입니다.
서버 측 검증
클라이언트가 전송한 익명 키(hash)를 그대로 신뢰하면 안 됩니다. 클라이언트는 임의의 문자열을 서버에 보낼 수 있습니다.
적용할 수 있는 보호 수단:
| 수단 | 설명 |
|---|---|
| Rate-limit | 동일 IP나 세션에서 단시간에 대량의 요청이 오면 차단합니다. |
| 행동 패턴 모니터링 | 비정상적인 점수 급등, 반복 등록 등을 이상 탐지 로직으로 걸러냅니다. |
의미 있는 작업은 appLogin 필수 | 결제, 리더보드 등록 등 영향도가 큰 작업은 반드시 계정 인증 후에만 허용합니다. |
| 보안 민감 데이터 분리 | 개인정보나 결제 내역은 익명 키 경로에 연결하지 않습니다. |
SDK가 서버 측 검증 엔드포인트를 별도로 제공하지 않으므로, 위 수단을 조합해 자체 보호 계층을 만드세요.
anonymous → logged-in 마이그레이션
사용자가 처음으로 appLogin을 완료한 시점에 서버에서 1회 실행합니다.
서버 수도코드:
function migrateAnonymousToUser(anonymousId, userId):
if user(userId).hasExistingData():
// 충돌: 같은 userId로 다른 디바이스에서 이미 진행한 데이터가 있음.
// 정책 결정 — 예: 더 높은 점수를 채택, 또는 사용자에게 선택권 부여.
mergeOrPickWinner(user(userId).data, anonymous(anonymousId).data)
else:
// 마이그레이션: 익명 데이터를 userId에 귀속.
assign(anonymous(anonymousId).data, userId)
// 익명 레코드는 마이그레이션 완료 후 삭제하거나 만료 처리.
markAsMigrated(anonymousId)
구현 시 주의 사항:
- 멱등성: 같은
(anonymousId, userId)쌍으로 두 번 호출해도 데이터가 중복되거나 손상되지 않아야 합니다. - 충돌 정책 명시: 동일
userId가 이미 다른 디바이스에서 익명 플레이 기록을 가진 경우, 어느 쪽 데이터를 우선할지 미리 결정해 두세요. 자동 병합이 어렵다면 사용자에게 선택하게 하는 것이 가장 안전합니다. - 마이그레이션은 서버에서: 클라이언트는 트리거만 합니다. 실제 데이터 이동은 서버에서 트랜잭션으로 처리합니다.
자주 빠뜨리는 실수 체크리스트
- 익명 키를 영구 식별자로 가정했는가? — 재설치 시 키가 바뀔 수 있습니다.
- 서버에서 클라이언트가 보낸
hash를 검증 없이 신뢰하고 있는가? — rate-limit 등 자체 보호가 필요합니다. getAnonymousKey의'ERROR'와undefined분기를 처리하지 않았는가? — 반환값 세 가지를 모두 처리하세요.getUserKeyForGame에서getAnonymousKey로 마이그레이션할 때 서버 측 필드(key→hash)를 바꾸지 않았는가?- anonymous → logged-in 마이그레이션이 멱등하지 않은가? — 중복 호출 시 데이터 손상이 발생할 수 있습니다.
- 보안 민감 데이터를 익명 경로에 연결해 두었는가? — 계정 인증 없이는 접근하지 못하도록 분리하세요.
환경별 차이
| 환경 | 동작 |
|---|---|
| 실 토스 앱 | getAnonymousKey가 정상 동작합니다. 반환되는 hash는 디바이스와 앱 상태에 따라 안정적으로 유지됩니다. |
| devtools mock | @ait-co/devtools가 stub 키를 즉시 반환합니다. 로컬에서 게임 흐름을 테스트하기에 충분하지만, 실제 SDK와 동일한 수명 특성을 보장하지는 않습니다. |
| 외부 브라우저 | getAnonymousKey가 throw하거나 'ERROR'를 반환합니다. 미니앱 환경 외에서 익명 키 경로를 노출하지 마세요. |
관련 API
getAnonymousKey— 현행 권장 익명 키 API.{ type: 'HASH'; hash: string }반환.getUserKeyForGame— (Deprecated) 게임 카테고리 한정 전신 API.appLogin— 토스 계정 기반 로그인 진입점.getGameCenterGameProfile— 게임센터 닉네임·프로필 이미지 조회.
관련 가이드
- Guides — 토스 로그인 흐름 —
appLogin부터 서버 측 토큰 교환까지 통합 흐름.
외부 참조
@apps-in-toss/web-framework— 상위 SDK 패키지.