게임을 위한 iframe 임베드 모범 사례
많은 플랫폼(Itch.io, Newgrounds, Cinevva, 직접 운영하는 사이트)이 게임을 iframe으로 임베드합니다. 이 튜토리얼에서는 게임이 임베드된 상태에서도 잘 작동하게 만드는 방법을 다룹니다.
1) 기본 임베드 설정
게임은 페이지 전체 제어권 없이도 작동해야 합니다.
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; }
canvas { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<script src="game.js"></script>
</body>
</html>2) iframe 환경 감지하기
js
const isEmbedded = window.self !== window.top
if (isEmbedded) {
// 임베드 환경에 맞게 동작 조정
hideExternalLinks()
adjustUIForSmallSize()
}
function hideExternalLinks() {
document.querySelectorAll('a[target="_blank"]').forEach(link => {
link.style.display = 'none'
})
}3) 필요한 iframe 권한 허용하기
부모 페이지가 iframe이 할 수 있는 일을 제어합니다. 흔히 쓰는 권한입니다.
html
<iframe
src="https://game.example.com"
allow="fullscreen; autoplay; gamepad; pointer-lock"
sandbox="allow-scripts allow-same-origin allow-pointer-lock allow-popups"
></iframe>게임은 권한이 없을 때도 자연스럽게 처리해야 합니다.
js
// 전체 화면을 쓸 수 있는지 확인
const canFullscreen = document.fullscreenEnabled || document.webkitFullscreenEnabled
if (!canFullscreen) {
document.getElementById('fullscreen-btn').style.display = 'none'
}4) iframe에서 전체 화면 전환하기
전체 화면에는 allow="fullscreen" 속성이 필요합니다.
js
async function requestFullscreen() {
const elem = document.documentElement
try {
if (elem.requestFullscreen) {
await elem.requestFullscreen()
} else if (elem.webkitRequestFullscreen) {
await elem.webkitRequestFullscreen()
}
} catch (err) {
// 전체 화면이 허용되지 않음 - 메시지 표시
showMessage('Fullscreen not available when embedded')
}
}5) 부모 페이지와 통신하기
안전한 교차 출처 통신에는 postMessage를 사용하세요.
게임 쪽:
js
// 부모에게 메시지 보내기
function notifyParent(type, data) {
if (window.parent !== window) {
window.parent.postMessage({ type, data, source: 'game' }, '*')
}
}
// 부모로부터 메시지 받기
window.addEventListener('message', (event) => {
// 필요하면 출처 검증
// if (event.origin !== 'https://trusted-host.com') return
const { type, data } = event.data
switch (type) {
case 'pause':
pauseGame()
break
case 'resume':
resumeGame()
break
case 'setVolume':
setVolume(data.volume)
break
}
})
// 게임이 준비되면 알리기
window.addEventListener('load', () => {
notifyParent('ready', { width: 800, height: 600 })
})
// 게임 이벤트가 발생하면 알리기
function onGameOver(score) {
notifyParent('gameover', { score })
}부모 페이지 쪽:
js
const iframe = document.getElementById('game-iframe')
iframe.addEventListener('load', () => {
// 게임에서 오는 메시지 수신
window.addEventListener('message', (event) => {
if (event.source !== iframe.contentWindow) return
if (event.data.source !== 'game') return
const { type, data } = event.data
if (type === 'ready') {
console.log('Game ready:', data)
}
if (type === 'gameover') {
showScoreModal(data.score)
}
})
})
// 게임에 명령 보내기
function pauseGame() {
iframe.contentWindow.postMessage({ type: 'pause' }, '*')
}6) 포커스 다루기
iframe은 포커스를 잃어 키보드 입력이 끊길 수 있습니다.
js
// 클릭하면 canvas에 자동으로 포커스
canvas.addEventListener('click', () => {
canvas.focus()
})
// canvas를 포커스 가능하게 만들기
canvas.tabIndex = 1
// 포커스 손실 처리
window.addEventListener('blur', () => {
// 눌려 있던 키 초기화
input.left = input.right = input.up = input.down = false
if (isEmbedded) {
// 필요하면 일시정지
// pauseGame()
}
})
// 부모에게 포커스 요청
function requestFocus() {
notifyParent('requestFocus', {})
}7) 임베드 크기에 반응하기
다양한 임베드 크기를 처리하세요.
js
function handleResize() {
const width = window.innerWidth
const height = window.innerHeight
// 크기에 맞춰 UI 조정
if (width < 400 || height < 300) {
enableCompactUI()
} else {
enableFullUI()
}
// 게임을 적절히 스케일링
resizeCanvas(width, height)
}
window.addEventListener('resize', handleResize)
handleResize()8) 임베드용 로딩 표시
뭔가를 즉시 보여주세요.
js
// 즉시 표시되도록 HTML에 인라인으로
const loadingHTML = `
<div id="loading" style="
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a2e;
color: #fff;
font-family: sans-serif;
">
<div>
<div class="spinner"></div>
<p>Loading...</p>
</div>
</div>
`
// 게임이 준비되면 제거
function hideLoading() {
const loading = document.getElementById('loading')
if (loading) {
loading.style.opacity = '0'
loading.style.transition = 'opacity 0.3s'
setTimeout(() => loading.remove(), 300)
}
}9) 플랫폼별 SDK
일부 플랫폼에는 자체 API가 있습니다.
Itch.io:
js
// SDK는 필요 없지만, 업적에 postMessage를 쓸 수 있음
window.parent.postMessage({ type: 'itch-achievement', data: { id: 'first-win' }}, '*')Newgrounds:
js
// Newgrounds.io SDK 포함
const ngio = new Newgrounds.io.core('APP_ID', 'AES_KEY')
ngio.callComponent('Medal.unlock', { id: 12345 })Cinevva:
js
// 게임 준비 완료 신호
window.parent.postMessage({ type: 'cinevva:ready' }, '*')
// 플레이 가능 상태 신호
window.parent.postMessage({ type: 'cinevva:playable' }, '*')10) 보안 고려사항
js
// 민감한 작업일 때 메시지 출처 검증
window.addEventListener('message', (event) => {
const trustedOrigins = [
'https://itch.io',
'https://cinevva.com',
'https://yoursite.com'
]
if (!trustedOrigins.includes(event.origin)) {
return // 신뢰할 수 없는 메시지 무시
}
// 메시지 처리...
})
// postMessage로 민감한 작업을 노출하지 말 것
// 화이트리스트에 있는 명령만 허용
const allowedCommands = ['pause', 'resume', 'setVolume', 'mute']
window.addEventListener('message', (event) => {
const { type } = event.data
if (!allowedCommands.includes(type)) return
// 명령 처리...
})테스트 체크리스트
- 로컬 테스트:
file://이 아니라 간단한 HTTP 서버를 쓰세요 - 교차 출처: 실제 iframe 임베드로 테스트하세요
- 권한: 제한된 sandbox에서 테스트하세요
- 포커스: iframe 밖을 클릭한 뒤 키보드를 테스트하세요
- 리사이즈: 여러 임베드 크기에서 테스트하세요
- 모바일: 임베드 환경에서 터치를 테스트하세요
html
<!-- Test embed page -->
<!DOCTYPE html>
<html>
<body style="background: #333; padding: 20px;">
<h1 style="color: #fff;">Embed Test</h1>
<iframe
src="http://localhost:8000"
width="800"
height="600"
allow="fullscreen; autoplay; gamepad"
></iframe>
</body>
</html>관련 글
- 빠르게 로드되는 웹 게임 출시하기
- 모바일 친화적인 웹 게임
- 창작자를 위해
- COOP/COEP와 SharedArrayBuffer — iframe 임베드에 영향을 주는 교차 출처 헤더
- itch.io에서 게임 출시하는 방법 — itch.io는 브라우저 게임을 iframe으로 기본 임베드합니다
외부 자료
- MDN: iframe element — iframe 속성에 대한 전체 레퍼런스
- MDN: Permissions Policy — iframe이 사용할 수 있는 기능 제어
- MDN: postMessage API — iframe과 부모 페이지 간 통신