TrendShift 딥다이브 · 2026-07-02 · Daily #3

RomM 딥다이브
— 흩어진 레트로 게임 롬(ROM)을 "넷플릭스처럼" 정리·재생하는 셀프호스팅 서버

RomM(ROM Manager)은 내 서버(NAS·홈랩)에 직접 설치해, 수천 개의 게임 롬 파일을 스캔·자동 태깅하고 브라우저에서 바로 플레이까지 하는 자기호스팅형 게임 라이브러리다. IGDB·ScreenScraper·MobyGames 같은 메타데이터 소스에서 표지·설명·출시일을 긁어와 폴더에 쌓인 정체불명의 롬 파일들을 예쁜 게임 카탈로그로 바꾼다. 백엔드는 FastAPI(Python 3.13), 프론트는 Vue 3 + Vuetify, 브라우저 플레이는 EmulatorJS가 담당한다. (저장소: rommapp/romm · Python + Vue · AGPL-3.0 · 400+ 플랫폼 지원 · TrendShift 오늘 Daily 3위, 모멘텀 431)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 저장소가 대체 무엇인가.

핵심 메시지

"내 게임 롬을 위한 넷플릭스 + 자동 사서(司書)" — 폴더에 던져 넣은 롬을 스캔해 표지·설명을 자동으로 붙이고, 브라우저에서 바로 실행하게 해 주는 자기호스팅 서버.

RomM의 정체는 새 에뮬레이터가 아니다. 이미 있는 에뮬레이터·메타데이터 서비스·롬 파일들을 하나로 엮어 주는 "관리 계층(management layer)"이다. 롬 파일을 library/{플랫폼}/roms/ 폴더에 넣고 스캔을 돌리면, RomM이 파일 이름과 해시(지문)를 분석해 "이게 무슨 게임인지" 알아내고, 인터넷의 게임 DB에서 표지 이미지·요약·출시연도·장르를 가져와 카탈로그를 만든다.

비유하면: 창고에 라벨 없는 게임 카트리지 수천 개가 쌓여 있는데, RomM이라는 사서가 하나하나 집어 바코드(해시)를 찍고, 도서관 DB에서 정보를 찾아 표지와 설명을 붙여 진열장에 꽂아 주는 것과 같다. 게다가 이 진열장은 웹페이지라서, 스마트폰·PC 어디서든 접속해 클릭 한 번으로 그 게임을 바로 플레이할 수 있다.

README의 자기소개는 한 문장이다: "RomM allows you to scan, enrich, browse and play your game collection with a clean and responsive interface." 여기서 핵심 동사 넷을 기억하면 프로젝트 전체가 보인다 — scan(스캔): 폴더를 훑어 롬을 찾는다, enrich(보강): 외부 DB에서 메타데이터를 붙인다, browse(탐색): 웹 UI로 구경한다, play(재생): 브라우저에서 EmulatorJS로 실행한다.

일반적인 파일 서버(예: 그냥 Samba 공유 폴더)와의 결정적 차이는 "보강(enrich)"에 있다. 롬 파일명이 Super Mario World (USA).sfc처럼 지저분하거나 rom001.bin처럼 무의미해도, RomM은 파일의 해시값(CRC32/MD5/SHA1)과 파일명을 단서로 "이건 슈퍼 패미컴용 슈퍼마리오 월드"라고 식별하고, 그에 맞는 공식 표지와 정보를 자동으로 채운다.

용어
롬(ROM) / 셀프호스팅(Self-hosting)
롬(ROM)은 원래 "Read-Only Memory(읽기 전용 메모리)"의 약자로, 옛 게임기의 카트리지·CD에 담긴 게임 데이터를 파일로 복제(덤프)한 것을 말한다. 이 파일을 에뮬레이터로 실행하면 옛 게임을 PC·폰에서 돌릴 수 있다. 셀프호스팅은 남의 클라우드 서비스에 맡기지 않고 내가 소유한 서버(집 NAS, 홈 서버, VPS)에 직접 소프트웨어를 설치해 운영하는 방식이다. 데이터가 밖으로 안 나가고, 구독료도 없다.

2왜 주목받는가

"셀프호스팅 붐" + 400개 플랫폼이라는 압도적 범위 + 브라우저 즉시 플레이.

2020년대 중반 홈랩·NAS 붐과 함께 "내 데이터는 내 서버에"라는 셀프호스팅 문화가 폭발했다. 사진(Immich), 영화(Jellyfin), 문서(Paperless) 각 분야마다 "자기호스팅 표준"이 자리 잡았는데, 레트로 게임 컬렉션 분야의 그 자리를 RomM이 빠르게 차지하고 있다. Hacker News 노출과 selfh.st(셀프호스팅 큐레이션) 설문에서 상위에 오르며 커뮤니티 신뢰를 얻은 것이 트렌딩의 1차 동력이다.

기존 대안과 무엇이 다른가

비교 축단순 파일 공유(Samba/NFS)EmulationStation / RetroArchRomM
메타데이터없음(파일명만)스크래퍼로 부분 지원13개 소스에서 자동 보강(IGDB·ScreenScraper·MobyGames…)
접근 방식파일 탐색기기기에 설치된 프론트엔드웹 브라우저 어디서나(반응형 UI·PWA)
플레이직접 에뮬레이터 실행로컬 에뮬레이터브라우저 내 EmulatorJS로 즉시(설치 0)
세이브 관리수동기기 로컬서버에 세이브·스테이트 중앙 저장·동기화
멀티유저OS 권한보통 단일 사용자친구 공유 + 역할(admin/editor/viewer)·OIDC
플랫폼 범위무관수십~수백400+ 플랫폼 메타데이터

둘째 동력은 완성도다. RomM은 취미 스크립트 수준을 한참 넘는다. 저장소 안에 docs/BACKEND_ARCHITECTURE.md·docs/FRONTEND_ARCHITECTURE.md라는 본격 아키텍처 문서를 두고, OpenTelemetry·Sentry로 관측성까지 챙긴다. 공식 안드로이드 앱(Argosy), Playnite 플러그인, 휴대용 게임기(muOS·NextUI)용 다운로더(Grout)까지 1st-party 앱 생태계를 갖췄고, Aikido 보안 감사 배지도 달았다.

셋째, "브라우저에서 바로 플레이"라는 경험이 강력하다. EmulatorJS 덕분에 사용자는 아무것도 설치하지 않고 게임 상세 페이지에서 버튼 하나로 즉시 게임을 켠다. 여기엔 뒤에서 볼 SharedArrayBuffer / COOP·COEP 헤더라는 브라우저 보안 트릭이 숨어 있다.

냉정하게 짚기 · 법적 경계
RomM은 롬을 제공하지 않는다 — 관리 도구일 뿐

RomM은 당신이 이미 소유한 롬 파일을 정리하는 소프트웨어다. 롬 파일 자체를 배포하지 않으며, 상용 게임 롬을 인터넷에서 내려받는 것은 대부분의 국가에서 저작권 침해다. 합법적 사용은 보통 "본인이 소장한 정품 매체를 직접 덤프한 백업"에 한정된다. 이 문서는 RomM의 기술 구조를 학습 목적으로 해부할 뿐, 롬 취득 방법을 다루지 않는다.

3기술 스택 전체 지도

pyproject.toml · frontend/package.json · docker/Dockerfile을 직접 뜯어 확인한 구성.

백엔드 (Python)

항목선택설명
언어Python 3.13+requires-python = ">=3.13". 패키지 매니저는 uv(uv.lock, uv sync --frozen)
웹 프레임워크FastAPI ~0.134 + Starlette비동기 API. Uvicorn(worker) + Gunicorn으로 프로덕션 구동
ORM · 마이그레이션SQLAlchemy 2.0 + Alembic 1.16DB 스키마 버전 관리. alembic/versions93개 마이그레이션 파일
데이터베이스MariaDB (기본) / PostgreSQL드라이버 mariadb-connector·psycopg[c] 3.2. 예제 compose는 mariadb:latest
캐시 · 작업 큐Redis / Valkey 8 + RQ 2.7RQ(Redis Queue)로 스캔 같은 무거운 작업을 백그라운드 실행. 포크한 rq-scheduler로 예약
실시간python-socketio 5.16스캔 진행률·로그를 WebSocket으로 프론트에 실시간 푸시
인증 · 검증authlib·joserfc(JWT)·passlib[bcrypt] / pydantic 2.11OAuth2·OIDC·세션. 응답 검증·페이지네이션(fastapi-pagination)
관측성OpenTelemetry + Sentry분산 트레이싱·에러 리포팅까지 갖춘 프로덕션 지향

프론트엔드 (JavaScript/TypeScript)

항목선택설명
프레임워크Vue 3.4 (Composition API)<script setup> 스타일. 컴포넌트 ~168개
UI 라이브러리Vuetify 3.9 + Tailwind 4머티리얼 디자인 컴포넌트 + 유틸리티 CSS 병용
빌드 · 언어Vite + TypeScript 5.7vue-tsc 타입 체크. Node 24 / npm 11 요구
상태 · 라우팅Pinia 3 (스토어 18개) + vue-router 4HTTP는 axios. i18n(vue-i18n)로 17개 언어
실시간 · PWAsocket.io-client + vite-plugin-pwa스캔 진행 실시간 수신. 앱처럼 설치 가능
API 클라이언트OpenAPI 자동 생성백엔드 OpenAPI 스키마 → 타입·클라이언트 자동 생성(frontend/src/__generated__/)

플레이어 · 인프라

항목선택설명
브라우저 에뮬EmulatorJS + RuffleRS웹에서 콘솔 게임(EmulatorJS)·Flash(Ruffle) 실행. Docker 빌드 때 EmulatorJS를 받아 포함
리버스 프록시Nginx정적 SPA 서빙 + API 프록시 + X-Accel-Redirect로 대용량 롬 파일 전송(Python 안 거침)
업적 해시RAHasher (C, 빌드)RetroAchievements용 해시 계산기를 Docker 스테이지에서 직접 컴파일
컨테이너멀티스테이지 Dockerfilefrontend-build → backend-build → rahasher → emulator → nginx → 최종 이미지
라이선스AGPL-3.0-only네트워크로 제공하는 개작본도 소스 공개 의무(카피레프트 강함)
한눈에

FastAPI = 두뇌(API·로직), MariaDB = 서류함(게임 정보 저장), Redis + RQ = 알바생(오래 걸리는 스캔을 뒤에서 처리), Vue/Vuetify = 얼굴(웹 화면), EmulatorJS = 게임기(브라우저 안 콘솔), Nginx = 안내데스크 겸 짐꾼(요청 분배 + 무거운 롬 파일 나르기). RomM은 이 조각들을 Docker 한 컨테이너로 묶었다.

용어
작업 큐(Task Queue) / RQ
웹 요청은 몇 초 안에 응답해야 하는데, "수천 개 롬 스캔"은 몇 분~몇십 분이 걸린다. 이런 오래 걸리는 일을 즉시 처리하지 않고 대기열(큐)에 넣어 두고, 별도 워커(일꾼) 프로세스가 뒤에서 하나씩 꺼내 처리하는 구조가 작업 큐다. RomM은 RQ(Redis Queue)를 써서 스캔·정리 작업을 high_prio_queue(급한 것)와 low_prio_queue(느긋한 것) 두 갈래로 나눠 돌린다.

4아키텍처 심화 분석

한 개의 Docker 컨테이너 안에서 요청·스캔·플레이가 어떻게 흐르는가.

전체 시스템 구조

브라우저 (Vue SPA / PWA) │ ①요청 ▼ ┌────────────────────────────────────────────────┐ │ Nginx (리버스 프록시 · 유일한 관문 :8080) │ │ · 정적 SPA 서빙(try_files → index.html) │ │ · /api → FastAPI 프록시 │ │ · 롬/이미지 → X-Accel-Redirect(파이썬 우회) │ │ · /rom/.../ejs 경로에만 COOP/COEP 헤더 부여 │ └───────┬───────────────────────┬─────────────────┘ │ ②API │ ⑤정적 파일(대용량) ▼ ▼ ┌────────────────────────┐ library/ · resources/ · assets/ │ FastAPI (Uvicorn/ │ (디스크 볼륨) │ Gunicorn) │ │ endpoints/ 라우터 │ │ handler/ 비즈니스로직 │ └───┬─────────┬──────────┘ │③저장 │④무거운 작업 enqueue ▼ ▼ ┌────────┐ ┌──────────────────────────────┐ │MariaDB │ │ Redis/Valkey ── RQ 워커 │ │(SQLAl- │ │ high_prio / low_prio 큐 │ │chemy) │ │ └ scan_handler 실행 │ └────────┘ │ └ 외부 메타데이터 API 호출 │ └──────────────────────────────┘ │ 진행률·로그 ▼ (Socket.IO WebSocket) 브라우저로 실시간 push

설계 패턴 1 — Nginx가 대용량 롬을 직접 나른다 (X-Accel-Redirect)

롬 파일은 수백 MB~수 GB에 이른다. 이걸 Python(FastAPI)이 읽어서 응답에 실어 보내면 파이썬 프로세스가 파일 하나 내보내는 내내 묶여 다른 요청을 못 받는다. RomM은 X-Accel-Redirect 기법을 쓴다: FastAPI는 "이 요청 허가한다, 실제 파일은 /library/...에 있다"는 헤더만 돌려주고, 실제 파일 전송은 Nginx가 담당한다. 파이썬은 권한 검사(인증·소유권)만 하고 즉시 자유로워지므로, 대용량 다운로드가 서버 전체를 느리게 만들지 않는다.

설계 패턴 2 — 스캔은 6가지 모드로, 워커가 뒤에서

스캔의 핵심은 backend/handler/scan_handler.py다. 사용자가 스캔을 누르면 API는 즉시 응답하고 실제 작업은 RQ 워커로 넘긴다. 스캔 종류(ScanType)는 NEW_PLATFORMS(새 플랫폼만)·QUICK(빠르게)·UPDATE·UNMATCHED(매칭 실패한 것만 재시도)·COMPLETE(전체)·HASHES(해시만) 6가지다. 상황별로 필요한 만큼만 돌려 비용·시간을 아끼는 설계다.

폴더 스캔 → 플랫폼 인식 → 롬 파일 해시 계산 → 식별 → 메타데이터 보강 │ │ │ │ │ library/{fs_slug} fs_slug이 CRC32/MD5/SHA1 해시로 우선순위 순서대로 /roms/의 폴더명 안 맞으면 +CHD는 chd_sha1 Hasheous/ IGDB→ScreenScraper 로 플랫폼 판단 config.yml (일부 플랫폼은 Playmatch →MobyGames…에서 바인딩으로 NON_HASHABLE로 로 매칭 표지·설명 fetch 교정(gc→ngc) 해시 생략)

롬 식별의 1차 단서는 해시(파일의 지문)다. roms_handler.py가 CRC32·MD5·SHA1을 계산하고, CD 이미지(CHD)는 별도 chd_sha1_hash를 만든다. 이 해시를 Hasheous·Playmatch 같은 해시 DB에 던져 "정확히 이 게임"을 찾아낸다. 라즈베리파이처럼 약한 기기를 위해 skip_hash_calculation으로 해시 계산을 끌 수도 있다.

설계 패턴 3 — 메타데이터 13개 소스와 우선순위

RomM은 무려 13개 메타데이터 공급자를 지원한다: IGDB·MobyGames·ScreenScraper·RetroAchievements·LaunchBox·Hasheous·TheGamesDB·SteamGridDB·Flashpoint·HowLongToBeat·ES-DE gamelist·Libretro 썸네일·Playmatch. 각 소스는 handler/metadata/의 핸들러 + adapters/services/의 저수준 클라이언트 2층으로 나뉜다. 어떤 소스를 먼저 믿을지는 config.ymlscan.priority.metadata/.artwork/.region/.language로 사용자가 정한다.

용어
어댑터 패턴 (Adapter Pattern)
IGDB·MobyGames 등은 저마다 API 형식이 다르다. RomM은 각 외부 서비스마다 "우리 앱이 이해하는 공통 형태로 번역해 주는" 어댑터(adapters/services/igdb.py 등)를 두고, 그 위에 통일된 규칙으로 여러 소스를 조합하는 핸들러 계층을 얹었다. 덕분에 새 메타데이터 소스를 추가할 때 어댑터 하나만 만들면 되고, 나머지 코드는 손대지 않는다. "바깥세상의 제각각인 것들을, 안쪽에는 하나의 표준 콘센트로 보이게" 하는 패턴이다.

설계 패턴 4 — 5겹 미들웨어와 다중 인증

backend/main.py는 FastAPI 앱에 CORS → CSRF → 인증 → Redis 세션 → 컨텍스트 변수 5겹 미들웨어를 쌓는다. 인증은 하나가 아니라 OAuth2 + HTTP Basic + OIDC + 세션을 모두 지원(HybridAuthBackend)한다. 비밀번호는 bcrypt로 해싱하고, 외부 클라이언트(안드로이드 앱 등)용 토큰은 rmm_ + 32바이트 hex를 발급하되 DB에는 원문이 아니라 SHA-256 해시로 저장해, DB가 유출돼도 토큰을 복원할 수 없게 한다. 역할은 admin/editor/viewer 3단계이며 OIDC 클레임에서 자동 매핑된다.

설계 패턴 5 — 브라우저에서 멀티스레드 에뮬을 돌리는 헤더 트릭

EmulatorJS의 일부 코어는 SharedArrayBuffer(여러 스레드가 공유하는 메모리)를 써야 빠르다. 그런데 브라우저는 보안(Spectre 공격 방지)을 이유로, 특정 격리 헤더가 없으면 SharedArrayBuffer를 막는다. RomM의 Nginx는 이 헤더(Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp)를 플레이어 경로에만 선택적으로 붙인다(~^/rom/.*/ejs$, /console/rom/[0-9]+/play). 전체 사이트에 붙이면 외부 이미지·스크립트가 깨질 수 있어, 필요한 페이지에만 격리를 켜는 정교한 절충이다.

여기서 배울 것
"관리 계층" 앱의 정석 아키텍처

RomM은 사진 서버(Immich)·미디어 서버(Jellyfin)와 판박이 구조다. 웹 UI(SPA) + API 서버 + 관계형 DB + 캐시/큐 + 리버스 프록시. 여기에 "외부 데이터로 보강(스크래핑)", "무거운 작업은 큐로", "대용량 파일은 프록시가 직접 전송"이라는 세 가지 공통 패턴이 얹힌다. 이 구조 하나를 제대로 이해하면 자기호스팅 앱 대부분의 설계를 읽을 수 있게 된다.

5디렉토리 구조 해부

루트와 backend/·frontend/의 주요 폴더가 각각 무슨 일을 하는가.

romm/ ├── backend/ ← Python / FastAPI │ ├── main.py FastAPI 앱 조립(라우터·미들웨어·소켓 등록) │ ├── startup.py 기동 시 초기화 │ ├── watcher.py 파일시스템 변화 감시(watchfiles) │ ├── alembic/versions/ DB 마이그레이션 93개 │ ├── endpoints/ ← API 라우터(요청 받는 곳) │ │ ├── roms/ files·manual·notes·patch·screenshot·upload… │ │ ├── platform.py collections.py search.py saves.py states.py │ │ ├── firmware.py stats.py sync.py netplay.py user.py │ │ ├── sockets/ WebSocket 핸들러(scan·sync·logs·activity) │ │ └── responses/ 엔티티별 Pydantic 응답 스키마 │ ├── handler/ ← 비즈니스 로직(진짜 일하는 곳) │ │ ├── scan_handler.py 스캔 오케스트레이션(6개 ScanType) │ │ ├── auth/ hybrid_auth·permissions_map·middleware │ │ ├── database/ 엔티티별 CRUD(roms·platforms·users…) │ │ ├── filesystem/ 디스크 실제 접근(roms·firmware·assets) │ │ └── metadata/ 13개 메타데이터 공급자 핸들러 │ ├── adapters/services/ 외부 API 저수준 클라이언트(igdb·moby…) │ ├── models/ SQLAlchemy 모델(rom·platform·user·assets) │ └── tasks/ 예약/수동 백그라운드 작업 ├── frontend/ ← Vue 3 / Vuetify │ └── src/ │ ├── components/ ~168개(Scan·Settings·Gallery·Details…) │ ├── views/ 화면(Player/EmulatorJS·Player/RuffleRS…) │ ├── services/api/ 17개 API 모듈 + cache/ │ ├── stores/ Pinia 스토어 18개 │ └── __generated__/ OpenAPI 자동 생성 타입·모델 ├── docker/Dockerfile 멀티스테이지 빌드 ├── docs/ BACKEND/FRONTEND_ARCHITECTURE.md └── examples/ docker-compose.example.yml · config.example.yml

구조의 핵심은 "요청을 받는 층(endpoints)"과 "일을 하는 층(handler)"의 분리다. endpoints/는 HTTP 요청을 받아 검증하고 넘기기만 하고, 실제 로직(스캔·DB·파일·메타데이터)은 전부 handler/에 있다. 그 안에서도 database/(DB CRUD)와 filesystem/(디스크 접근)이 또 갈라져, "데이터가 어디에 있든" 접근 코드가 한 곳에 모인다.

읽는 순서 추천
이 저장소를 공부한다면

docs/BACKEND_ARCHITECTURE.md로 큰 그림 잡기 → ② backend/main.py로 앱이 어떻게 조립되는지(라우터·미들웨어) 보기 → ③ backend/handler/scan_handler.py로 프로젝트의 심장인 스캔 흐름 읽기 → ④ backend/handler/filesystem/roms_handler.py로 해시 계산 로직 → ⑤ examples/docker-compose.example.ymlconfig.example.yml로 실제 배포·설정 이해. 이 다섯이면 뼈대가 잡힌다.

6학습 포인트 (기술별)

이 레포에서 실제로 배워 갈 수 있는 것들.

① FastAPI로 "진짜 프로덕션급" API 짜기

튜토리얼 수준의 FastAPI는 라우터 몇 개가 전부지만, RomM은 미들웨어 스택·다중 인증·페이지네이션·OpenAPI 스키마 export·비동기 작업 위임까지 실무 요소를 다 보여준다. 특히 main.py가 라우터를 개별 importinclude_router로 붙이고 lifespan으로 기동/종료를 관리하는 방식은 대형 FastAPI 앱의 표준 골격이다.

실습 아이디어

미니 API를 만들되, 인증 미들웨어 한 개 + Redis 세션 한 개를 직접 붙여 보라. RomM의 handler/auth/middleware/를 참고하면 "요청이 라우터에 닿기 전에 가로채는" 미들웨어 감각이 잡힌다.

② SQLAlchemy 2.0 + Alembic 마이그레이션

models/rom.py·platform.py 등은 SQLAlchemy 2.0 스타일 모델의 좋은 예제다. 더 값진 건 alembic/versions93개 마이그레이션이다 — 실제 서비스가 스키마를 어떻게 점진적으로 진화시켜 왔는지 타임라인으로 읽을 수 있다. "컬럼 추가할 때 기존 데이터는 어떻게 채웠나"를 보면 실전 DB 운영이 보인다.

실습 아이디어

작은 SQLAlchemy 프로젝트에서 Alembic으로 마이그레이션 3개를 만들어 보라(테이블 생성 → 컬럼 추가 → 인덱스 추가). alembic upgrade/downgrade로 앞뒤로 오가는 걸 체험하면 마이그레이션의 두려움이 사라진다.

③ Redis + RQ로 백그라운드 작업 큐

"오래 걸리는 일은 큐로 넘긴다"는 웹 개발의 필수 패턴이다. RomM은 스캔·정리를 RQ로 넘기고, 진행률을 Socket.IO로 실시간 push한다. API 응답 즉시 반환 → 워커가 처리 → WebSocket으로 진행 알림이라는 이 삼각 구조는 파일 업로드·리포트 생성·이메일 발송 등 어디에나 쓰인다.

실습 아이디어

RQ로 "10초 걸리는 가짜 작업"을 큐에 넣고, 워커가 처리하는 동안 진행률을 Socket.IO(또는 SSE)로 브라우저에 흘려보내는 최소 데모를 만들어 보라.

④ Vue 3 + OpenAPI 자동 생성 클라이언트

RomM의 프론트는 백엔드 OpenAPI 스키마에서 타입·API 클라이언트를 자동 생성(__generated__/)한다. 백엔드가 응답 형태를 바꾸면 프론트 타입도 재생성되어 컴파일 에러로 잡힌다 — "프론트-백 계약을 코드로 강제"하는 현대적 워크플로다. Pinia 스토어 18개로 상태를 도메인별(롬·플랫폼·유저·설정…)로 나눈 구성도 참고할 만하다.

⑤ Docker 멀티스테이지 빌드

docker/Dockerfilefrontend-build(Node로 Vue 빌드) → backend-build(uv sync) → rahasher-build(C 컴파일) → emulator-stage(EmulatorJS 다운로드) → nginx → 최종 슬림 이미지 순으로 스테이지를 쌓는다. 빌드 도구는 최종 이미지에 안 남기고 결과물만 복사하는 멀티스테이지의 교과서다. 서로 다른 언어(Python·JS·C)를 한 이미지에 조립하는 실전 사례이기도 하다.

용어
멀티스테이지 빌드 (Multi-stage Build)
Docker 이미지를 만들 때, 여러 개의 임시 "빌드 단계"를 거친 뒤 결과물만 최종 이미지에 복사하는 기법이다. 예를 들어 Vue를 빌드하려면 Node·npm이 필요하지만, 빌드가 끝나면 결과인 정적 파일(dist/)만 있으면 된다. 멀티스테이지를 쓰면 무거운 빌드 도구는 버리고 알맹이만 남겨, 최종 이미지가 작고 안전해진다. RomM처럼 언어가 여러 개 섞인 앱에 특히 유용하다.

7하드웨어 / 시스템 요구사항

무엇이 있어야 돌릴 수 있는가 — 그리고 폴더는 어떻게 구성하는가.

구분요구사항
실행 방식Docker + Docker Compose (권장). 이미지 rommapp/romm:latest
필수 서비스RomM 컨테이너 + DB(MariaDB 또는 PostgreSQL). 내부에 Redis/Valkey·Nginx는 포함
포트예제 compose 기준 80:8080 (호스트 80 → 컨테이너 8080)
CPU/RAM저장소에 하드 최소치 명시는 없음. NAS·라즈베리파이급에서도 동작(약한 기기는 skip_hash_calculation으로 부하 완화). 대규모 라이브러리 스캔·브라우저 에뮬은 여유 있는 CPU가 유리
디스크롬 원본 용량 + 표지·스크린샷 리소스(플랫폼·라이브러리 크기에 비례)
메타데이터 키IGDB·MobyGames·ScreenScraper·SteamGridDB 등은 각자 API 키/계정 필요(선택이지만 보강 품질에 직결)
브라우저EmulatorJS 멀티스레드 코어는 COOP/COEP 격리가 필요(RomM Nginx가 처리). 최신 브라우저 권장

폴더 레이아웃 규약 (매우 중요)

RomM이 롬을 인식하려면 정해진 폴더 구조를 지켜야 한다. 기본 베이스 경로(ROMM_BASE_PATH, 기본 /romm) 아래는 이렇게 생겼다:

/romm/ ├── library/ │ └── {platform_slug}/ ← 플랫폼 폴더명이 곧 플랫폼 식별자(fs_slug) │ ├── roms/ ← 롬 파일(폴더명은 config로 변경 가능) │ └── bios/ ← 펌웨어/BIOS ├── resources/ │ └── roms/{rom_id}/ ← 자동 수집한 표지(cover_s/l.webp)·스크린샷 ├── assets/ │ └── users/{user_id}/{rom_id}/ ← 유저별 세이브·스테이트·스크린샷 └── config/config.yml ← 스캔 우선순위·플랫폼 바인딩 설정

폴더명이 표준 슬러그와 다르면(예: 게임큐브 폴더를 gc로 만들었는데 RomM은 ngc를 기대) config.ymlsystem.platforms 바인딩으로 별칭을 매핑(gc: ngc, ps1: psx)한다. 즉 "내 폴더 이름 = RomM 플랫폼 ID"라는 연결을 설정으로 교정할 수 있다.

8직접 해볼 수 있는 실습 과제

난이도별로. 위에서 아래로 갈수록 어렵다. (합법적으로 소유·백업한 롬 전제)

난이도 ★☆☆ — 첫 설치

Docker Compose로 RomM 띄우고 폴더 규약 지키기

examples/docker-compose.example.ymlconfig.example.yml을 복사해 로컬(또는 NAS)에 RomM + MariaDB를 올려라. library/{platform}/roms/ 구조를 지켜 자유 라이선스 홈브루 롬 한두 개를 넣고, 웹 UI에서 스캔이 도는 걸 확인한다.

난이도 ★★☆ — 스캔 원리 관찰

QUICK vs COMPLETE 스캔 차이 체감 + 로그 실시간 보기

여러 종류의 스캔(QUICK, UNMATCHED, COMPLETE)을 각각 돌려 보고, 브라우저 개발자도구 네트워크 탭에서 Socket.IO(WebSocket) 메시지로 진행률·로그가 실시간 오는 걸 확인하라. RQ 워커 로그와 대조하면 "API → 큐 → 워커 → 소켓" 흐름이 눈에 보인다.

난이도 ★★☆ — 메타데이터 우선순위

config.yml로 소스 우선순위 바꿔 보기

scan.priority.metadata 순서를 바꿔(예: ScreenScraper를 IGDB보다 앞으로) 재스캔한 뒤, 표지·설명이 어떤 소스에서 왔는지 달라지는 걸 비교하라. 플랫폼 바인딩(gc: ngc)도 실험해 폴더명 교정을 익힌다.

난이도 ★★★ — 코드 뜯기

scan_handler.py 흐름을 도식화

backend/handler/scan_handler.pyScanType·MetadataSource 열거형과 filesystem/roms_handler.py의 해시 계산(_make_file_hash)을 따라가며, "폴더 → 플랫폼 인식 → 해시 → 매칭 → 보강" 파이프라인을 직접 시퀀스 다이어그램으로 그려 보라.

난이도 ★★★ — 확장

새 메타데이터 어댑터(모의) 추가

adapters/services/의 기존 클라이언트 하나를 본떠, 로컬 JSON 파일을 소스로 삼는 "가짜 메타데이터 어댑터"를 만들고 handler/metadata/에 핸들러를 붙여 보라. 어댑터 패턴이 새 소스 추가를 얼마나 국소적으로 만드는지 체감할 수 있다.

9관련 기술 심화 학습 로드맵

이 레포를 완전히 소화하기 위한 4주 코스.

주차주제할 일
1주차Docker & 셀프호스팅 기초Docker/Compose로 다중 컨테이너(앱+DB+Redis) 띄우기, 볼륨·네트워크·환경변수, Nginx 리버스 프록시 기본. RomM compose를 한 줄씩 해석
2주차FastAPI + SQLAlchemy + Alembic비동기 라우터·의존성 주입·미들웨어, SQLAlchemy 2.0 모델, Alembic 마이그레이션. RomM main.py·models/ 정독
3주차Redis · RQ · WebSocket작업 큐로 백그라운드 처리, Socket.IO로 실시간 진행 알림. RomM scan_handler·sockets/ 흐름 재현
4주차Vue 3 + OpenAPI 클라이언트 + 배포Composition API·Pinia·Vuetify, OpenAPI로 타입 자동 생성, 멀티스테이지 Docker로 프론트+백 한 이미지 빌드

이 순서의 논리는 "그릇(컨테이너) → 두뇌(API·DB) → 일꾼(큐·실시간) → 얼굴(프론트)·포장(배포)"이다. RomM이 정확히 이 층으로 쌓여 있으므로, 4주 뒤에는 Immich·Jellyfin 같은 다른 자기호스팅 앱의 구조도 같은 눈으로 읽을 수 있게 된다.

10핵심 키워드 사전

본문에 나온 용어를 한 곳에.

용어
RomM롬을 스캔·보강·탐색·재생하는 자기호스팅 게임 라이브러리. FastAPI + Vue, AGPL-3.0.
ROM옛 게임기 매체(카트리지·CD)를 파일로 덤프한 것. 에뮬레이터로 실행.
셀프호스팅클라우드 대신 내 서버(NAS·홈랩)에 직접 설치·운영하는 방식.
메타데이터 보강(Enrich)파일명·해시로 게임을 식별해 외부 DB에서 표지·설명·출시일을 자동으로 붙이는 과정.
해시(CRC32/MD5/SHA1)파일 내용에서 만든 고유 지문. 파일명이 엉망이어도 "이 게임"을 정확히 식별하는 근거.
EmulatorJS브라우저(WebAssembly)에서 콘솔 게임을 실행하는 웹 에뮬레이터. RomM의 "바로 플레이"를 담당.
RQ (Redis Queue)Redis 기반 파이썬 작업 큐. 스캔 같은 무거운 일을 워커가 뒤에서 처리.
X-Accel-RedirectFastAPI는 권한만 검사하고, 실제 대용량 파일 전송은 Nginx가 맡게 하는 위임 헤더.
COOP / COEP브라우저 격리 헤더. 있어야 SharedArrayBuffer(멀티스레드 에뮬)를 쓸 수 있음. RomM은 플레이어 경로에만 부여.
어댑터 패턴제각각인 외부 API를 앱 내부의 공통 형태로 번역하는 계층. 새 소스 추가를 국소화.
Alembic 마이그레이션DB 스키마 변경을 버전으로 관리하는 도구. RomM엔 93개가 쌓여 있음.
OIDCOpenID Connect. 외부 신원공급자(Authentik 등)로 로그인·역할 매핑을 위임하는 표준.
멀티스테이지 빌드Docker에서 빌드 도구는 버리고 결과물만 남겨 최종 이미지를 작게 만드는 기법.

11참고 링크