에뮬레이터 없이 브라우저에서 돌아가는 콘트라
‘위위아래아래 좌우좌우 BA’, 한 번쯤 들어보셨죠? 이 코나미 커맨드를 입력하면 목숨 30개를 받고 시작할 수 있었던 그 게임, 코나미의 콘트라(Contra) NES판이 웹 브라우저에서 바로 돌아가는 버전으로 공개됐어요. 흔히 보던 ‘웹 에뮬레이터에 롬 파일 올려놓은 것’과는 접근 방식이 완전히 달라요.
보통 옛날 게임을 브라우저에서 돌릴 때는 NES라는 기계 자체를 소프트웨어로 흉내 내는 에뮬레이터를 만들고, 그 위에 원본 게임 데이터(ROM)를 얹어요. 이 프로젝트는 반대로 게임 코드 자체를 바꿨어요. 원래 6502 어셈블리로 작성된 콘트라의 로직을 C 코드로 옮기고, 그 C 코드를 다시 WebAssembly(브라우저에서 네이티브에 가까운 속도로 돌아가는 바이너리 포맷)로 컴파일한 거예요. 비유하자면 에뮬레이션은 외국어 원서를 동시통역사를 옆에 두고 읽는 거고, 이 프로젝트는 원서를 아예 한국어로 번역해서 새로 출간한 거예요.
어떤 과정을 거치냐면
1단계: 어셈블리 해독하기. NES의 CPU는 6502 계열(리코 2A03)이고 클럭은 약 1.79MHz, 메인 램은 고작 2KB예요. 이 시절 게임은 거의 전부 어셈블리로 짰고요. 그래서 이식은 ROM을 디스어셈블(기계어를 사람이 읽을 수 있는 어셈블리로 되돌리는 작업)하고, 각 루틴이 뭘 하는지 하나하나 이름과 주석을 붙이는 데서 시작해요. 콘트라는 팬 커뮤니티에 주석이 꼼꼼히 달린 디스어셈블리가 공개돼 있을 만큼 분석이 많이 된 게임이라, 이런 이식을 시작하기에 좋은 대상이에요.
2단계: 어셈블리를 C로. 여기가 제일 까다로워요. 6502에는 A, X, Y 레지스터와 캐리(carry), 제로(zero) 같은 상태 플래그가 있어요. 원본 코드는 “방금 덧셈에서 올림이 생겼으면 저쪽으로 점프”처럼 이 플래그에 크게 기대고 있고요. 예를 들어 이런 코드가 있다고 해볼게요.
LDA player_x ; A = 플레이어 X 좌표
CLC ; 캐리 플래그 초기화
ADC #$02 ; A = A + 2
STA player_x ; 결과 저장
BCS edge ; 255를 넘겨 올림이 생기면 분기
C로 옮기면 대략 이런 모양이 돼요.
uint16_t sum = player_x + 2;
player_x = (uint8_t)sum;
if (sum > 0xFF) { / 화면 끝 처리 / }
이 정도면 쉬운 편이에요. 정말 골치 아픈 건 따로 있어요. 주소 표를 보고 간접 점프하는 점프 테이블, 스택에 쌓인 리턴 주소를 직접 고치는 트릭, 코드와 데이터가 뒤섞인 영역 같은 것들이에요. 8비트 값이 255에서 0으로 넘어가는 동작까지 똑같이 재현해야 적의 움직임이나 충돌 판정이 원본과 같아져요. 그래서 원본과 한 프레임씩 비교하면서 검증하는 과정이 꼭 필요해요.
3단계: 하드웨어는 여전히 필요해요. 게임 로직은 C로 옮겼더라도, 화면을 그리는 PPU(그래픽 칩)와 소리를 내는 APU(오디오 칩)는 게임 코드가 아니라 하드웨어예요. 원래 게임은 특정 메모리 주소에 값을 쓰면 PPU가 스프라이트와 배경을 알아서 그려주는 구조거든요. 그래서 이런 포트는 보통 게임 로직은 네이티브 C로 돌리고, 그래픽과 사운드는 하드웨어 동작을 재현한 레이어가 맡는 하이브리드 구조가 돼요.
4단계: C에서 WebAssembly로. C 코드가 확보되면 나머지는 비교적 수월해요. Emscripten 같은 툴체인으로 컴파일하면 .wasm 파일이 나오고, 화면은 canvas에, 입력은 키보드 이벤트에 연결하면 끝이에요. 더 중요한 건 일단 C로 옮겨두면 윈도우, 맥, 리눅스는 물론 다른 기기로도 이식하기 쉬워진다는 점이에요.
업계 맥락: 디컴파일과 재컴파일의 시대
요 몇 년 레트로 게임 커뮤니티에서 가장 활발한 흐름이 바로 이거예요. 슈퍼 마리오 64는 팬들이 원본 바이너리와 바이트 단위까지 똑같이 컴파일되는 C 코드를 복원해냈어요. 이걸 ‘매칭 디컴파일’이라고 부르는데, 덕분에 PC 네이티브 포트와 고해상도 버전까지 나왔죠. 젤다의 전설: 시간의 오카리나도 같은 길을 걸었고요. 최근에는 N64Recomp 같은 정적 재컴파일 도구도 나왔어요. 바이너리를 자동으로 C 코드로 바꿔주는 기법이라, 사람이 일일이 옮기지 않고도 네이티브 포트를 만들 수 있게 됐죠.
두 방식의 장단점은 분명해요. 에뮬레이터는 하나만 잘 만들면 게임 수천 개를 돌릴 수 있지만, 특정 게임을 고치거나 기능을 덧붙이긴 어려워요. 소스 포트는 게임 하나에 엄청난 노력이 들어가는 대신 와이드스크린, 고프레임, 모드 지원처럼 원본에 없던 기능을 자유롭게 붙일 수 있고요. 8비트 게임은 코드량이 작아서 사람이 직접 옮기는 방식도 아직 현실적이에요.
한국 개발자에게 주는 시사점
“이게 실무랑 무슨 상관이지?” 싶을 수 있는데, 연결고리가 꽤 많아요.
- 레거시 마이그레이션의 축소판이에요. 문서도 없는 옛 코드를 읽고 동작을 그대로 지키면서 새 언어로 옮기는 일은 코볼 시스템을 전환하거나 C++ 코드를 Rust로 옮기는 작업과 본질이 같아요. 원본과 결과물을 프레임 단위로 비교하는 방식은 그대로 회귀 테스트 전략이 되고요.
- WebAssembly 실전 감각을 얻을 수 있어요. Figma나 AutoCAD 웹 버전 같은 실제 서비스도 기존 C/C++ 자산을 이런 방식으로 브라우저에 가져왔어요.
- 컴퓨터 구조 교재로도 훌륭해요. 2KB 램과 1.79MHz CPU로 화면 스크롤에 수많은 적과 총알까지 처리한 기법을 들여다보면, 메모리와 연산을 아끼는 감각이 확 늘거든요.
마무리
한 줄 정리: 에뮬레이터로 ‘흉내’ 내는 대신 코드 자체를 C로 ‘번역’해서, 40년 가까이 된 게임을 어디서든 돌아가는 소프트웨어로 되살린 프로젝트예요.
여러분은 에뮬레이션과 소스 포트 중 어느 쪽이 게임 보존에 더 의미 있다고 보세요? 그리고 회사에 이렇게 ‘번역’이 필요한 레거시 코드가 있다면, 어떤 방식으로 검증하면서 옮기고 계신가요?
🔗 출처: Hacker News