← 글 목록으로 돌아가기

웹 브라우저에서 오락실 MAME ROM 게임 구동하기: EmulatorJS 연동 문제 해결과 완벽 디버깅 가이드

웹에뮬레이터(EmulatorJS / RetroArch)를 활용해 웹 브라우저에서 아케이드 ROM을 구동하는 전체 구조와 개발 중 발생한 5가지 핵심 오류의 디버깅 포스트모템을 정리합니다.

웹 브라우저에서 오락실 MAME ROM 게임 구동하기: EmulatorJS 연동 문제 해결과 완벽 디버깅 가이드

웹 브라우저 기술이 발전함에 따라 과거 오락실이나 콘솔 게임기에서 구동되던 고전 아케이드 게임을 별도의 프로그램 설치 없이 웹 브라우저에서 직접 실행할 수 있게 되었습니다. 이번 글에서는 WebAssembly 기반 웹 에뮬레이터인 EmulatorJS를 구축하고 MAME ROM 파일을 성공적으로 연동하기까지의 전체 과정과 실전 디버깅 기록을 공유합니다.


목차 (Table of Contents)

  1. 초보자를 위한 핵심 용어 사전
  2. 웹 에뮬레이터 구동 방식과 아키텍처
  3. 실제 발생했던 5가지 핵심 오류와 디버깅 포스트모템
  4. 게임 개발자를 위한 EmulatorJS 생태계와 스펙
  5. 웹 아케이드 게임 통합 완벽 구축 가이드
  6. 성능 및 사용자 경험 최적화 팁

초보자를 위한 핵심 용어 사전

  • WebAssembly (Wasm): C, C++ 등의 고성능 언어로 작성된 프로그램을 웹 브라우저에서 바이너리 속도로 빠르게 실행할 수 있게 해주는 웹 표준 기술입니다.
  • MAME (Multiple Arcade Machine Emulator): 1980~90년대 오락실 하드웨어를 소프트웨어로 구현해 고전 아케이드 게임을 재생하는 에뮬레이터 엔진입니다.
  • EmulatorJS: RetroArch 에뮬레이터 코어를 WebAssembly로 컴파일하여 웹페이지에 쉽게 삽입할 수 있도록 만든 프론트엔드 자바스크립트 라이브러리입니다.
  • ROM File: 오락실 기판의 메모리 칩에 저장되어 있던 게임 데이터와 그래픽, 사운드 바이너리를 묶어놓은 압축 파일(.zip)입니다.
  • Blob URL: 브라우저 메모리에 다운로드된 바이너리 데이터를 일시적인 URL 형태(blob:http://...)로 변환하여 참조할 수 있게 만든 가상 주소입니다.
  • SharedArrayBuffer: 여러 스레드 간 메모리를 공유하여 빠른 연산을 돕는 API로, 최신 브라우저에서는 보안 헤더(COOP/COEP) 설정이 없으면 차단됩니다.

웹 에뮬레이터 구동 방식과 아키텍처

EmulatorJS WebAssembly Architecture

웹 브라우저에서 오락실 게임을 구동하는 파이프라인은 다음과 같은 단계로 구성됩니다.

  1. ROM 바이너리 페치 (Fetch): 서버에 저장된 고전 오락실 ROM 압축 데이터(예: hyperspt.zip)를 HTTP 요청을 통해 브라우저 메모리로 다운로드합니다.
  2. Emscripten 가상 파일 시스템 (VFS): 다운로드한 ROM 압축 파일을 WebAssembly 가상 파일 시스템 내부 경로(예: /hyperspt.zip)로 마운트합니다.
  3. Wasm 에뮬레이터 코어 실행: C/C++로 구현된 MAME 2003(mame2003) 코어가 ROM 내부 그래픽, 프레임버퍼, 8비트/16비트 CPU 명령어를 해석합니다.
  4. Canvas 렌더링 & WebAudio 사운드 출력: 매 프레임별 화면 출력을 HTML5 Canvas 2D/WebGL로 렌더링하고, 합성된 오디오 신호를 WebAudio API를 통해 스피커로 출력합니다.

실제 발생했던 5가지 핵심 오류와 디버깅 포스트모템

이번 프로젝트 개발 과정에서 발생했던 5가지 주요 실패 상황과 이를 해결한 디버깅 포스트모템 기록입니다.

1. 존재하지 않는 코어 이름 지정으로 인한 HTTP 404 에러

  • 현상: 에뮬레이터가 로딩을 시작하다가 화면이 멈추고 구동되지 않는 현상 발생.
  • 원인 분석: EJS_core = 'mame'으로 지정하였으나, EmulatorJS CDN에는 mame.js 코어 파일이 존재하지 않아 HTTP 404 Not Found가 리턴됨.
  • 해결 방법: MAME 0.78 레퍼런스 기준 정식 코어 명칭인 EJS_core = 'mame2003'으로 변경하여 코어 바이너리를 정상 다운로드하도록 수정함.

2. 자동 실행 시 브라우저 오디오 컨텍스트 차단 현상

  • 현상: 페이지 진입 시 로딩 상태에서 오락실 화면이 멈춰서 구동되지 않음.
  • 원인 분석: 최신 브라우저(Chrome, Safari)는 사용자 터치나 클릭 이벤트(User Gesture) 없이 스크립트가 오디오 엔진(AudioContext)을 자동 초기화할 때 락(Suspended)을 걸어 스레드가 무한 대기 상태에 빠짐.
  • 해결 방법: 페이지 진입 즉시 자동 구동하던 방식을 제거하고, 직관적인 🎮 게임 시작하기 버튼을 배치하여 사용자가 버튼을 클릭하는 순간 유저 제스처 환경에서 에뮬레이터를 구동하도록 전환함.

3. SharedArrayBuffer 보안 헤더 부재로 인한 Failed to start game 오류

  • 현상: 모바일 및 일부 정적 호스팅 환경에서 Failed to start game 에러 팝업 발생.
  • 원인 분석: GitHub Pages나 정적 웹 호스팅은 Cross-Origin-Embedder-Policy: require-corp 보안 헤더를 기본 제공하지 않음. EmulatorJS가 멀티스레드로 구동하려다 SharedArrayBuffer 예외가 발생함.
  • 해결 방법: window.EJS_threads = false; 옵션을 명시적으로 지정하여 단일 스레드 모드로 고정함.

4. Blob URL 확장자 유실로 인한 MAME 설정 메인 메뉴 진입 문제

  • 현상: ROM 파일이 정상 다운로드되었음에도 게임이 안 나오고 RetroArch MAME 설정 화면(Main Menu / Load Content)으로 이동함.
  • 원인 분석: 메모리 로딩 후 Blob URL(blob:http://.../uuid)로 전달할 때 파일 확장자(.zip)가 유실되어 MAME 코어가 ROM 파일 드라이버를 자동 감지하지 못함.
  • 해결 방법: Blob URL 끝에 #hyperspt.zip 해시를 명시하거나, 직렬 HTTP ROM URL 스트링(validRomUrl)을 직접 EJS_gameUrl에 전달하도록 구조 변경.

5. MAME 드라이버 이름 불일치로 인한 구동 실패

  • 현상: 게임 이름에 서식 문자열(Hyper Sports)을 사용 시 MAME 코어 로딩 실패.
  • 원인 분석: MAME 2003 코어는 EJS_gameName 값을 기준으로 오락실 ROM 세트 드라이버명을 매칭함.
  • 해결 방법: 소문자 아케이드 정식 드라이버 명칭인 EJS_gameName = 'hyperspt'로 지정.

게임 개발자를 위한 EmulatorJS 생태계와 스펙

웹 포털이나 블로그에 고전 게임 에뮬레이터를 통합하려는 웹 게임 개발자라면 공식 프로젝트 사이트인 EmulatorJS (https://emulatorjs.org/)를 필히 참작해야 합니다.

1. 40여 가지 이상의 에뮬레이트 플랫폼 지원

EmulatorJS는 MAME 2003 오락실 기판 외에도 NES, SNES, GameBoy, GameBoy Advance, Nintendo 64, PlayStation 1, Sega Genesis, 3DO 등 주요 콘솔 플랫폼 코어를 대거 지원합니다. 개발자는 EJS_core 옵션만 바꿈으로써 어떤 콘솔 ROM이든 웹에서 바로 구동할 수 있습니다.

2. 공식 컨트롤 매핑 규격 (Control Mapping Spec)

EmulatorJS 개발자 공식 문서에 따르면 키보드와 연결된 게임패드를 조화롭게 연동하기 위해 window.EJS_defaultControls 객체 구조를 준수해야 합니다.

// EmulatorJS 공식 개발자 컨트롤 매핑 규격 예시
window.EJS_defaultControls = {
  0: { // Player 1
    0: { value: 'z', value2: 'BUTTON_1' }, // 버튼 1 (B / Left Run)
    1: { value: 'x', value2: 'BUTTON_2' }, // 버튼 2 (Y / Jump)
    8: { value: 'c', value2: 'BUTTON_3' }, // 버튼 3 (A / Right Run)
    2: { value: '5', value2: 'SELECT' },   // 동전 (Coin)
    3: { value: '1', value2: 'START' }    // 시작 (P1 Start)
  }
};

각 버튼 객체에는 키보드 입력을 위한 value속성뿐만 아니라, 물리 게임패드 매핑을 위한 value2 속성(예: BUTTON_1, SELECT, START)을 함께 작성해야 스크립트 실행 중 인라인 자바스크립트 예외가 발생하지 않습니다.

3. 개발자 도구 (Code Generator & Netplay)

  • Code Generator: EmulatorJS Code Generator 도구를 활용하면 UI상에서 원하는 옵션을 선택하여 Embed 코드를 자동 생성할 수 있습니다.
  • Netplay 웹소켓 멀티플레이: WebSockets 기술 기반의 Netplay 서버를 연동하면 웹 브라우저 사용자들끼리 원격 2인용 멀티플레이를 구현할 수 있습니다.

웹 아케이드 게임 통합 완벽 구축 가이드

실제 사용된 Astro 페이지 예시 코드 구조입니다.

<div class="hyper-screen" tabindex="0">
  <div id="hyper-game"></div>
  <div class="hyper-placeholder" id="hyper-placeholder">
    <button id="hyper-center-start" type="button">🎮 게임 시작하기</button>
  </div>
</div>

<!-- 온스크린 아케이드 액션 터치 바 -->
<div class="hyper-arcade-bar">
  <button class="arcade-btn coin-btn" data-key="5" data-code="Digit5" data-keycode="53">🪙 동전 (5)</button>
  <button class="arcade-btn start-btn" data-key="1" data-code="Digit1" data-keycode="49">▶️ 시작 (1)</button>
  <button class="arcade-btn action-btn" data-key="z" data-code="KeyZ" data-keycode="90">◀️ 버튼 1: Z (왼쪽)</button>
  <button class="arcade-btn action-btn" data-key="x" data-code="KeyX" data-keycode="88">🦘 버튼 2: X (점프)</button>
  <button class="arcade-btn action-btn" data-key="c" data-code="KeyC" data-keycode="67">▶️ 버튼 3: C (오른쪽)</button>
</div>

<script is:inline>
  const DATA_URL = 'https://cdn.emulatorjs.org/stable/data/';
  const centerBtn = document.getElementById('hyper-center-start');

  async function launchGame() {
    // 1. 공식 컨트롤 매핑 규격 적용 (value & value2 포함)
    window.EJS_defaultControls = {
      0: {
        0: { value: 'z', value2: 'BUTTON_1' },
        1: { value: 'x', value2: 'BUTTON_2' },
        8: { value: 'c', value2: 'BUTTON_3' },
        2: { value: '5', value2: 'SELECT' },
        3: { value: '1', value2: 'START' }
      }
    };

    // 2. 에뮬레이터 환경 변수 설정
    window.EJS_player = '#hyper-game';
    window.EJS_core = 'mame2003';
    window.EJS_gameUrl = '/hyperspt.zip';
    window.EJS_gameName = 'hyperspt';
    window.EJS_pathtodata = DATA_URL;
    window.EJS_threads = false;
    window.EJS_volume = 1.0;
    window.EJS_startOnLoaded = true;

    // 3. 로더 스크립트 동적 주입
    const loader = document.createElement('script');
    loader.src = `${DATA_URL}loader.js`;
    document.body.appendChild(loader);
  }

  centerBtn.addEventListener('click', launchGame);
</script>

성능 및 사용자 경험 최적화 팁

  1. 다중 후보 ROM 탐색 (Fallback Fetch): 상대 경로, 절대 경로, GitHub Raw 원격 백업 경로를 묶어 순차 탐색하도록 구현하면 서브디렉터리 배포 환경에서도 100% 자산을 불러올 수 있습니다.
  2. 이중 CDN 백업 (jsDelivr Fallback): cdn.emulatorjs.org 실패 시 cdn.jsdelivr.net으로 자동 전환되도록 구현하면 광고 차단기나 네트워크 제한 환경에서도 안정적으로 로더를 받아옵니다.
  3. 온스크린 터치 패널 연동: 모바일 및 터치 디바이스 이용자를 위해 합성 KeyboardEvent를 발생시키는 버튼 바를 배치하면 플랫폼에 구애받지 않고 오락실 게임을 즐길 수 있습니다.

이번 디버깅 기록과 공식 EmulatorJS 가이드를 바탕으로 웹 브라우저 기반 고전 게임 구축 시 발생할 수 있는 시행착오를 미리 예방해 보세요.

NT

NewType Studio Editorial

기술과 디자인의 경계를 허무는 세련된 디지털 가치를 만듭니다.