안 된다고 말하는 런타임 시뮬레이션: 브라우저를 WeChat처럼 작동하도록 샌드박싱한 방법
글: Oleg Sidorkin, Cinevva CTO 겸 공동 창업자

최근 Game Creator에 WeChat 미니게임 모드를 출시했습니다. AI가 생성한 게임을 WeChat 런타임으로 이식해도 살아남는 웹 플랫폼의 하위 기능 집합 안에 유지하는 빌드 프로필입니다. 이 프로필은 생성 규칙 모음으로 구성됩니다. DOM UI 금지, Pointer Lock 금지, mp3 전용 오디오, 동적 코드 금지. 모델은 모델이 으레 규칙을 따르는 방식대로 이를 따릅니다. 즉, 대체로 따릅니다.
브라우저에서는 모든 위반이 보이지 않지만 내보낸 뒤에는 치명적이라면, “대체로”로는 부족합니다. HTML 체력 표시줄이 있는 게임은 미리보기에서는 완벽하게 실행되지만 WeChat에서는 빈 화면만 표시됩니다. WeChat 미니게임에는 DOM이 전혀 없기 때문입니다. 그래서 이러한 규칙을 제안에서 물리 법칙으로 바꿔 주는 장치를 만들었습니다. WeChat에서 할 수 없는 작업을 브라우저 자체도 거부하게 만드는 엄격 모드 샌드박스입니다. 이 글에서는 그 작동 방식을 설명합니다.
핵심 결정: 존재가 아니라 부재를 시뮬레이션한다
가장 뻔한 접근법은 WeChat을 에뮬레이션하는 것입니다. wx.createCanvas, wx.onTouchStart, wx.setStorageSync를 구현하고 에뮬레이션된 wx.* 인터페이스를 대상으로 게임을 실행하는 방식입니다. 하지만 저희는 정반대 방향을 택했습니다. 그 이유는 내보내기 파이프라인의 구조에 있습니다.
WeChat 모드의 게임도 여전히 브라우저 API를 기준으로 작성됩니다. 패키징 시점에 어댑터(커뮤니티 표준인 weapp-adapter 방식)가 해당 브라우저 API를 wx.*에 매핑합니다. 즉, 게임은 wx.*를 직접 호출하지 않으므로 wx 에뮬레이터는 실제로 존재하지 않는 코드 경로를 테스트하게 됩니다. 실제로 이식을 망가뜨리는 원인은 브라우저에 wx.*가 없다는 사실이 아닙니다. 생성 과정에서 WeChat에 대응 기능이 없는 브라우저 API가 조용히 사용된다는 사실입니다. document.createElement('div'), requestPointerLock(), IndexedDB, Chrome에서는 문제없이 디코딩되지만 WeChat iOS에서는 절대 재생되지 않는 .ogg 파일 같은 것들입니다.
그래서 샌드박스는 부재를 시뮬레이션합니다. WeChat에 없는 모든 기능을 미리보기에서 제거하거나, 사용 시 오류를 발생시키거나, 위반으로 표시합니다. 그 결과 게임은 첫 프레임부터 두 플랫폼의 교집합 안에서 개발됩니다.
샌드박스의 위치: 이미 존재하던 서비스 워커
에디터 미리보기에는 이 작업을 거의 공짜로 구현할 수 있게 해 준 독특한 제공 경로가 있습니다. 에디터의 게임은 네트워크에서 제공되지 않습니다. 서비스 워커가 /game/* 요청을 가로채 IndexedDB에서 파일을 제공합니다. 덕분에 미리보기 iframe은 서버를 왕복하지 않고도 즉시 새로고침됩니다. 이 워커는 이미 모든 게임 페이지에 두 개의 가상 파일을 삽입하고 있었습니다. 콘솔 출력과 충돌을 에디터로 전달하는 오류 캡처 스크립트, 그리고 실시간 장면 검사기입니다.
샌드박스는 세 번째 가상 파일인 _wechat-strict.js입니다. 페이지 <head>에서 캡처 스크립트 바로 뒤, 게임 코드가 실행되기 전에 삽입됩니다. 실행 순서는 두 가지 이유로 절대적으로 중요합니다. 샌드박스의 console.error 호출이 이미 후킹되어 에디터로 전달되려면 캡처 스크립트 뒤에 실행되어야 하고, 게임 코드의 첫 줄이 실행될 때 차단 장치가 준비되어 있으려면 게임 모듈보다 앞에 실행되어야 합니다.
활성화에는 한 줄짜리 기법을 사용합니다. 크리에이터의 WeChat 토글 상태는 localStorage에 저장되며, 미리보기 iframe은 에디터와 동일 출처이므로 하네스가 플래그를 직접 읽기만 하면 됩니다.
try {
if (localStorage.getItem('cinevva-gc-profile') !== 'wechat-minigame') return;
} catch (e) { return; }WeChat용이 아닌 모든 게임에서는 스크립트가 문자열을 한 번 비교한 뒤 즉시 반환합니다. 빌드 플래그도, 서버 왕복도, 서비스 워커와 동기화할 상태도 없습니다.
차단 목록과 2단계 분류 체계
모든 위반에 같은 대응이 필요한 것은 아니므로 하네스는 두 단계를 구분합니다. WeChat에 존재하지 않는 API는 오류를 던집니다. 내보낸 뒤에도 그렇게 작동하며, 생성 중 발생하는 충돌이 심사 단계에서 발생할 충돌을 정직하게 미리 보여 주기 때문입니다. 존재하지만 나중에 문제를 일으키는 기능은 동작을 바꾸지 않고 눈에 잘 띄는 콘솔 오류로 표시합니다. 이를 차단하면 반복 작업 중인 사람이 게임의 실제 상태를 확인하지 못하게 되기 때문입니다.
오류 발생 단계에는 다음이 포함됩니다. createElement와 createElementNS를 통한 모든 HTML UI 요소 생성(Three.js는 NS 변형을 통해 캔버스를 생성하므로 두 경로 모두 같은 게이트가 필요합니다), eval, new Function, requestPointerLock, 서비스 워커 등록입니다. 각 오류 메시지에는 해결 방법이 포함됩니다.
throw violate("document.createElement('" + tag + "') — WeChat 미니게임에는 DOM이 없습니다. " +
"UI를 캔버스에 그리세요(오프스크린 2D 캔버스 -> 화면 공간 쿼드의 THREE.CanvasTexture). " +
"탭 적중 판정도 직접 구현해야 합니다.");표시 단계에는 다음이 포함됩니다. indexedDB 읽기(WeChat과 똑같이 undefined를 반환하고 localStorage를 사용하라는 오류도 표시), WeChat iOS에서 디코딩할 수 없는 형식의 오디오(Audio 생성자, 미디어 요소 프로토타입의 src 설정자, 가져오는 URL이라는 세 지점에서 검사), 게임 자체 출처나 저희 에셋 CDN이 아닌 출처로 보내는 네트워크 요청(WeChat에서는 화이트리스트에 등록되고 ICP 신고가 완료된 도메인이 필요), 그리고 Tone.js가 조금이라도 등장하는 경우입니다.
페이지가 안정화된 직후 실행되는 로드 후 검사도 있습니다. <body>를 순회하며 동적 생성이 아니라 게임 자체 마크업을 통해 들어온 HTML 요소를 모두 보고합니다. 정적 HUD는 createElement 게이트를 빠져나갈 수 있으므로 게이트만으로는 충분하지 않습니다.
모든 보고서는 메시지를 기준으로 중복을 제거하며 세션당 50개로 제한됩니다. 프레임마다 div를 하나씩 만드는 반전된 루프 버그가 있어도 신호를 묻어 버리는 오류 폭주가 아니라 오류 하나만 생성됩니다.
가장 많은 교훈을 준 네 가지 예외
샌드박스를 만드는 일은 대부분 무엇을 차단하지 않을지 결정하는 과정이며, 각각의 예외는 개발 중 저희 도구가 망가진 경험에서 나왔습니다.
저희 디버거는 eval로 실행됩니다. AI가 실행 중인 게임을 조사할 때 사용하는 크리에이터의 execute_js 도구는 캡처 스크립트의 메시지 핸들러를 통해 코드를 평가하며, 이 핸들러는 eval을 호출합니다. 무턱대고 eval을 차단했다면 AI의 눈 자체를 가렸을 것입니다. 해결책은 다음과 같습니다. 차단하기 전에 하네스가 실제 함수를 열거 불가능한 속성에 보관하고, 캡처 스크립트의 핸들러가 이를 대체 수단으로 사용합니다.
Object.defineProperty(window, '__cinevvaRealEval', { value: window.eval, enumerable: false });
window.eval = function () { throw violate('WeChat 미니게임에서는 eval()이 금지됩니다.'); };
// 캡처 스크립트, 메시지 처리 시점:
returnValue = (window.__cinevvaRealEval || eval)(e.data.code);게임 코드가 eval을 사용하려 하면 여전히 중단됩니다. 디버거는 중단되지 않습니다.
레코더도 요소를 생성합니다. 저희 릴 레코더는 iframe 자체의 document.createElement로 만든 <script>로 게임 iframe에 삽입되며, 완성된 동영상은 합성 <a> 클릭을 통해 다운로드합니다. 이러한 태그를 차단하면 정확히 WeChat 모드에서만 녹화가 망가졌을 것입니다. 따라서 script는 허용하고 a는 오류를 던지지 않고 경고만 표시합니다. 실제 링크를 포함해 출시하려는 게임은 여전히 위반으로 표시되며, 도구도 계속 작동합니다.
키보드 이벤트는 유지합니다. AI는 키 입력을 합성하고 게임 상태가 어떻게 변했는지 읽는 도구를 사용해 조작을 검증합니다. 휴대폰을 시뮬레이션하기 위해 키보드 입력을 억제했다면 카메라 방향 반전 같은 버그를 잡는 조작 검증 루프가 망가졌을 것입니다. 터치 우선 원칙은 샌드박스가 아니라 생성 규칙과 프로필의 조작 체계를 통해 적용합니다.
WebGL2는 유지하며, 그 이유는 아래에서 별도 섹션으로 설명할 가치가 있습니다. 초기 차단 목록은 프로필에서 요구하는 “WebGL1 호환” 렌더링을 강제하기 위해 getContext('webgl2')를 차단했습니다. 하지만 Three.js는 r163에서 WebGL1 지원을 완전히 제거했고, 저희는 r181을 고정해 사용합니다. WebGL2를 차단했다면 이 모드의 모든 3D 게임이 빈 화면으로 바뀌었을 것입니다. 프로필의 렌더링 규칙은 오래된 플랫폼 인식에 기반한 모순된 규칙이었습니다. 샌드박스 감사가 샌드박스 자체의 명세를 감사한 셈이며, 이 문제를 파고들자 중요한 결론에 도달했습니다.
WebGL2 문제에 대한 제대로 된 답
컨텍스트 생성을 그대로 두자 당연한 후속 질문이 생겼습니다. 엔진이 WebGL2를 요구한다면 WebGL1만 지원하는 WeChat 기기에서 게임이 망가지지 않을까요? 이를 추적한 결과 저희가 파악한 플랫폼의 최소 렌더링 환경을 가장 명확하게 정리할 수 있었습니다. 출처와 함께 설명하겠습니다.
Android에서는 최신 기반 라이브러리를 사용하는 모든 WeChat 미니게임 런타임이 WebGL2를 제공하며, 기반 GLES3 하드웨어도 사실상 어디서나 사용할 수 있습니다. 진짜 문제는 iOS입니다. WeChat 자체 엔지니어링 문서인 WebGL2 지원과 고성능+ 모드에 따르면, iPhone에서 WebGL2는 고성능+ 런타임에서만 제대로 사용할 수 있습니다. 이 런타임에는 최신 WeChat 클라이언트(8.0.45 이상, iOS 14에서는 더 높은 버전)가 필요하며, 실질적으로 iOS 15.5 이상이 요구됩니다. Tencent의 설명에 따르면 일반 고성능 모드에서는 WebGL2가 “더 많은 문제를 안고” 실행되며, 일반 iOS 런타임에는 아예 존재하지 않습니다.
정말 위험한 부분은 실패 방식입니다. 지원되지 않는 환경에서 getContext('webgl2')가 null 대신 참으로 평가되지만 망가진 컨텍스트를 반환할 수 있습니다. 개발자들이 Tencent에 직접 수정을 요청한 동작입니다. 반환값을 신뢰하는 게임은 해당 환경에서 깔끔하게 실패하지 않습니다. 깨진 화면을 렌더링하거나 아무 알림 없이 검은 화면만 표시합니다.
그래서 저희는 세 단계로 대응합니다. 샌드박스는 WebGL2 컨텍스트 생성을 그대로 둡니다. 이를 차단하면 자체 엔진과 충돌하기 때문입니다. 이제 생성 프로필은 모든 게임에 부팅 게이트를 요구합니다. 렌더러 생성을 try/catch로 감싼 뒤 기능 검사(typeof gl.createVertexArray === 'function', 거짓말하는 컨텍스트에는 없는 WebGL2 전용 기능)를 실행하고, 실패하면 빈 화면 대신 캔버스에 친절한 “WeChat을 업데이트해 주세요” 메시지를 그립니다. 이는 Unity에서 변환된 미니게임이 사용하는 것과 같은 패턴이며, 실제 서비스에서 검은 화면 대신 업그레이드 안내가 표시되는 이유이기도 합니다. 그리고 내보내기 기능이 출시되면 game.json에서 고성능+ 모드를 고정해 지원되는 경로가 기본값이 되도록 할 예정입니다.
WebGL2를 최소 사양으로 설정할 때 잃는 사용자는 어느 정도일까요? 대략 iOS 15.5 미만이거나 8.0.45보다 오래된 WeChat 클라이언트를 사용하는 사용자로, 2026년 기준 한 자릿수 초반의 비율이지만 구형 기기에 집중되어 있습니다. 장르에 따라 이 점이 더 중요할 수 있습니다. 실제 배포 데이터에서 이 꼬리 구간을 지원할 가치가 있다고 판단되면, 저렴한 탈출구도 남겨 두었습니다. WeChat 프로필을 WebGL1을 지원하는 마지막 릴리스인 three r162에 고정하는 것입니다. 프로필 게임은 안정적인 핵심 API만 사용하므로 다운그레이드는 import map 한 줄만 바꾸면 됩니다. 데이터가 요구하기 전까지는 의도적으로 사용하지 않을 선택지입니다.
모델과의 피드백 루프 완성
이 부분이 AI 우선 제품에서 샌드박스를 만들 가치가 있게 해 줍니다. 빌드가 끝날 때마다 크리에이터의 에이전트는 게임 콘솔을 읽고 부팅 완료를 기다리는 도구를 호출합니다. 캡처 스크립트는 console.error를 해당 스트림으로 전달합니다. 따라서 [wechat-strict] 위반은 사람이 스크롤하며 지나칠 수도 있는 경고가 아닙니다. 모델이 빌드 완료를 선언하기 전에 이미 확인하는 바로 그 채널로, 같은 턴에, 메시지 안에 해결 방법까지 담겨 전달됩니다.
생성 프로필은 한 가지 지침으로 마지막 빈틈을 메웁니다. 모든 [wechat-strict] 메시지를 빌드를 중단시키는 버그로 취급하고, 원인을 수정하며, 하네스를 감지하거나 우회하려 시도하지 말라는 것입니다. 샌드박스를 꺼야만 실행되는 게임은 내보낸 뒤 실패하는 게임이며, 모델에도 이를 명확하게 알려 줍니다.
결과적으로 샌드박스는 모호한 규정 준수 문제(“모델이 14개 규칙을 모두 따랐는가?”)를 시스템이 이미 잘 처리하는 디버깅 루프(“콘솔에 오류가 표시되니 수정하라”)로 바꿉니다.
브라우저가 시뮬레이션할 수 없는 것
솔직히 말해야 할 부분입니다. 이 샌드박스는 API 표면 위반을 잡아내며, 저희 추산으로는 이식 실패 원인의 대다수를 차지합니다. 하지만 실제 휴대폰에서의 성능, JIT 없이 JavaScript를 실행하는 iOS, WeChat의 WebGL 구현 특성, 파일 확장자만으로는 알 수 없는 코덱 동작 차이는 잡아낼 수 없습니다. 이런 문제에는 실제 런타임이 필요합니다.
따라서 샌드박스는 3단계 중 첫 번째 방어선입니다. 두 번째 방어선은 내보내기 기능이 WeChat DevTools 프로젝트를 생성하게 된 뒤 공식 시뮬레이터를 DevTools CLI와 miniprogram-automator를 통해 헤드리스로 구동하는 것입니다. 내보낸 패키지를 부팅하고, 첫 프레임이 렌더링되는지 검증하고, 탭을 스크립트로 실행합니다. 세 번째 방어선은 공식 기기 경로입니다. 미리보기 QR 코드와 실제 휴대폰 원격 디버깅을 사용하는 단계로, 다른 어떤 환경에서도 드러나지 않는 진실이 나타나는 곳입니다. 각 방어선은 이전 단계보다 느리지만 더 정확하며, 각 단계의 역할은 다음 단계로 넘어가는 일을 드물게 만드는 것입니다.
이 모드를 사용해 보고 싶다면 Game Creator의 “WeChat 미니게임 모드” 토글을 켜면 됩니다. 이와 관련된 시장 및 규제 환경은 WeChat의 5억 플레이어에게 다가가기 위한 현장 가이드를 참고하세요.