이 글은 누구를 위한 것인가
- 자기 사이트에 외부 웹 게임·미니 콘텐츠를 설치 없이 통합하고 싶은 프론트엔드 개발자
- iframe 보안·세이브 영속화·성능을 한 번에 정리하고 싶은 팀
- "사용자가 떠나지 않고 사이트 안에서 바로 플레이하게" 만들고 싶은 콘텐츠 운영자
들어가며
브라우저 게임 시장은 다시 살아났다. WebGL·WebGPU 가속, WebAssembly의 보편화, 모바일 브라우저 성능 개선이 겹쳤다. 그 결과 설치 없이 페이지에서 바로 돌아가는 게임이 다시 주류가 되어가고 있다. 콘텐츠 사이트 관점에서는 외부 게임을 사이트 안에 끌어와 사용자 체류를 늘리는 패턴이 다시 유효해졌다.
테이블플레이의 브라우저에서 바로 플레이 영역이 좋은 사례다. Sudoku.com, Slither.io, Minesweeper Online, Little Alchemy 2 같은 외부 웹 게임을 카드로 노출하고, 클릭하면 사이트 내에서 실행된다. 포커 웹 게임 저장 실패 글과 매치3 모바일 브라우저 체크 글은 통합 시 발생하는 현실 문제를 정리한다.

1. 통합 옵션 세 가지
외부 웹 게임을 사이트에 통합하는 방법은 셋이다.
| 방법 | 장점 | 단점 |
|---|---|---|
| 외부 링크 (새 탭) | 가장 단순 | 사용자 이탈, 복귀율 낮음 |
| iframe 임베드 | 사이트 내 유지, 스타일 분리 | 보안·세이브 한계 |
| 직접 통합 (코드 가져오기) | 완전 제어 | 라이선스 협상 필요 |
대부분의 콘텐츠 사이트는 iframe 임베드가 현실적이다. 외부 게임이 자체 도메인에서 운영되더라도, 사이트 안에서 실행되는 체류 효과를 얻는다.
2. iframe sandbox — 보안 기본기
가장 흔한 실수가 sandbox 없이 외부 페이지를 임베드하는 것이다. 외부 페이지의 스크립트가 부모 페이지를 조작할 수 있다. 다음이 안전한 기본형이다.
<iframe
src="https://example-game.com/play"
sandbox="allow-scripts allow-same-origin allow-pointer-lock"
allow="autoplay; fullscreen; gamepad"
loading="lazy"
width="800"
height="600"
title="외부 웹 게임"
referrerpolicy="no-referrer"
></iframe>
sandbox 속성에 허용할 권한만 명시한다. 게임은 보통 다음이 필요하다.
allow-scripts: JS 실행allow-same-origin: 자체 도메인의 LocalStorage·IndexedDB 접근allow-pointer-lock: 마우스 잠금 (FPS 등)allow-fullscreen: 전체화면 (별도allow속성도 필요)
allow-top-navigation은 절대 추가하지 않는다. 외부 게임이 부모 페이지를 다른 URL로 이동시킬 수 있다.
3. postMessage — 부모-자식 통신
iframe 안 게임과 부모 페이지가 통신해야 할 때가 있다. 점수 표시, 진행도 저장 트리거, 광고 슬롯 신호 같은 경우다. postMessage를 사용하되 오리진을 반드시 검증한다.
// 부모 페이지
window.addEventListener('message', (event) => {
if (event.origin !== 'https://example-game.com') return;
const { type, payload } = event.data;
switch (type) {
case 'GAME_PROGRESS':
saveProgressToBackend(payload);
break;
case 'GAME_OVER':
showShareDialog(payload.score);
break;
case 'REQUEST_PAUSE':
iframe.contentWindow?.postMessage(
{ type: 'PAUSE_ACK' },
'https://example-game.com'
);
break;
}
});
// 자식 게임 (외부 도메인)
window.parent.postMessage(
{ type: 'GAME_PROGRESS', payload: { level: 5, score: 1200 } },
'https://parent-site.com'
);
오리진 검증 누락은 XSS급 취약점이다. 메시지 페이로드는 신뢰할 수 없는 입력으로 다룬다.
4. Canvas/WebGL 게임의 성능 점검
직접 통합 또는 iframe 안 Canvas 게임에서 자주 깨지는 성능 항목은 다음이다.
4.1 메인 스레드 점유
function gameLoop(timestamp) {
update(timestamp);
render();
requestAnimationFrame(gameLoop);
}
update가 무거우면 60fps가 깨지고, 부모 페이지 UI도 함께 멈춘다. 무거운 계산은 Web Worker로 옮긴다.
const worker = new Worker('/game-physics-worker.js');
function gameLoop() {
worker.postMessage({ type: 'TICK', state: currentState });
// 결과는 message 이벤트에서 수신
render();
requestAnimationFrame(gameLoop);
}
물리·AI·길찾기 계산이 워커로 빠지면 메인 스레드가 렌더링과 입력만 담당하고 60fps가 안정적으로 유지된다.
4.2 Canvas 사이즈 vs CSS 사이즈
const canvas = document.querySelector('canvas');
const dpr = window.devicePixelRatio;
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
ctx.scale(dpr, dpr);
Retina·고DPI 환경에서 캔버스 크기를 디바이스 픽셀 비율에 맞추지 않으면 흐릿해진다. 반대로 무조건 비율을 곱하면 모바일에서 GPU가 죽는다. 모바일은 dpr을 1.5 정도로 캡한다.
4.3 텍스트 렌더링
Canvas 안에서 텍스트를 매 프레임 그리면 비싸다. 변하지 않는 텍스트는 오프스크린 캔버스에 한 번 그려두고, 메인 캔버스에 이미지로 합성한다.
const offscreen = new OffscreenCanvas(200, 50);
const offCtx = offscreen.getContext('2d');
offCtx.font = '24px sans-serif';
offCtx.fillText('SCORE', 0, 24);
// 매 프레임에서는
ctx.drawImage(offscreen, x, y);
5. 세이브 영속화 — IndexedDB가 정답
LocalStorage는 5MB 제한과 동기 API가 약점이다. 게임 세이브에는 IndexedDB가 적합하다. 다만 매치3 모바일 브라우저 체크 글에서 다루듯, 모바일 브라우저는 저장소를 자동 정리한다.
async function openGameDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open('GameDB', 1);
req.onupgradeneeded = () => {
const db = req.result;
db.createObjectStore('saves', { keyPath: 'slot' });
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function saveGame(slot, data) {
const db = await openGameDB();
const tx = db.transaction('saves', 'readwrite');
tx.objectStore('saves').put({ slot, data, savedAt: Date.now() });
return new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
async function loadGame(slot) {
const db = await openGameDB();
const tx = db.transaction('saves', 'readonly');
const req = tx.objectStore('saves').get(slot);
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req.result?.data ?? null);
req.onerror = () => reject(req.error);
});
}
추가로 서버 백업을 둔다. 브라우저 저장소가 사라져도 마지막 진행을 복원할 수 있다.
async function saveGameWithBackup(slot, data) {
await saveGame(slot, data);
if (navigator.onLine && currentUserId) {
fetch('/api/saves', {
method: 'POST',
body: JSON.stringify({ slot, data, userId: currentUserId }),
}).catch(() => {/* 무시 — 다음 동기화에서 재시도 */});
}
}
테이블플레이 포커 웹 게임 저장 실패 글이 다루는 사용자 시나리오들은 거의 모두 서버 백업이 없을 때에 발생한다. 통합하는 게임이 제3자라면 강제할 수 없지만, 자체 게임이라면 백업을 기본으로 둔다.

6. 모바일 터치·키보드 입력
모바일 브라우저에서 게임 입력은 다음을 챙겨야 한다.
canvas {
touch-action: none; /* 스크롤 방지 */
-webkit-user-select: none; /* 텍스트 선택 방지 */
-webkit-touch-callout: none; /* 길게 누르기 메뉴 방지 */
}
touch-action: none이 없으면 캔버스 위에서 스크롤·줌이 발생해 게임이 안 된다. pointerdown·pointermove·pointerup을 통합 이벤트로 받으면 마우스·터치·펜이 같은 코드 경로로 처리된다.
키보드 게임은 모바일에서 가상 키 패드가 필요하다. CSS Grid로 가상 십자 키와 액션 버튼을 그려두고, 같은 키 이벤트로 게임 코드를 트리거한다. 가상 키 패드 영역도 touch-action: none을 적용한다.
7. 광고·세션·동의 처리
테이블플레이는 정보 사이트라 자체 광고 정책을 가진다. 일반 사이트가 외부 게임을 임베드할 때는 광고와의 경계를 명확히 한다.
- 게임 iframe 위에 오버레이 광고를 띄우지 않는다. 사용자가 게임 입력으로 광고를 클릭하는 사고가 발생한다.
- 게임이 전체 화면으로 전환되면 사이트의 헤더·광고가 보이지 않는다. 전체 화면 종료 후 정상 복귀를 테스트한다.
- 쿠키 동의 배너가 게임 위에 떠 있으면 입력이 막힌다. 게임 영역에서는 동의 배너를 상단·하단으로 고정 위치한다.
세션 만료가 게임 진행 중에 발생하면, 진행 데이터를 안전하게 저장한 뒤 로그인 다이얼로그를 띄운다. 진행을 잃은 채로 로그인 화면이 나오면 사용자는 그 자리에서 떠난다.
8. 통합 점검 체크리스트
- iframe
sandbox속성으로 권한 최소화 -
allow-top-navigation사용 안 함 - postMessage 오리진 검증
- iframe
loading="lazy"(첫 화면이 아닌 경우) - 외부 게임 도메인에 대한 CSP
frame-src명시 - Canvas dpr 처리 (모바일 캡)
- IndexedDB 세이브 + 서버 백업 병행
-
touch-action: none적용 - 전체 화면 종료 후 복귀 테스트
- 쿠키 동의 배너 위치 충돌 점검
- 광고-게임 영역 분리
마무리
브라우저 웹 게임은 사용자 체류 시간 측면에서 가성비 좋은 콘텐츠다. 다만 통합의 질이 사용자 경험을 결정한다. iframe sandbox 한 줄, touch-action: none 한 줄, IndexedDB + 서버 백업 한 패턴을 챙기지 않으면 사용자는 깨지는 페이지만 기억하고 떠난다.
테이블플레이의 즉시 플레이 영역, 포커 웹 게임 저장 실패, 매치3 모바일 브라우저 체크 세 페이지를 한 번 둘러보고, 자기 사이트에 어떤 게임을 어떤 방식으로 끌어올지 그려보면 통합 작업의 윤곽이 잡힌다.