이 저장소가 대체 무엇인가.
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)과 파일명을 단서로 "이건 슈퍼 패미컴용 슈퍼마리오 월드"라고 식별하고, 그에 맞는 공식 표지와 정보를 자동으로 채운다.
"셀프호스팅 붐" + 400개 플랫폼이라는 압도적 범위 + 브라우저 즉시 플레이.
2020년대 중반 홈랩·NAS 붐과 함께 "내 데이터는 내 서버에"라는 셀프호스팅 문화가 폭발했다. 사진(Immich), 영화(Jellyfin), 문서(Paperless) 각 분야마다 "자기호스팅 표준"이 자리 잡았는데, 레트로 게임 컬렉션 분야의 그 자리를 RomM이 빠르게 차지하고 있다. Hacker News 노출과 selfh.st(셀프호스팅 큐레이션) 설문에서 상위에 오르며 커뮤니티 신뢰를 얻은 것이 트렌딩의 1차 동력이다.
| 비교 축 | 단순 파일 공유(Samba/NFS) | EmulationStation / RetroArch | RomM |
|---|---|---|---|
| 메타데이터 | 없음(파일명만) | 스크래퍼로 부분 지원 | 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의 기술 구조를 학습 목적으로 해부할 뿐, 롬 취득 방법을 다루지 않는다.
pyproject.toml · frontend/package.json · docker/Dockerfile을 직접 뜯어 확인한 구성.
| 항목 | 선택 | 설명 |
|---|---|---|
| 언어 | 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.16 | DB 스키마 버전 관리. alembic/versions에 93개 마이그레이션 파일 |
| 데이터베이스 | MariaDB (기본) / PostgreSQL | 드라이버 mariadb-connector·psycopg[c] 3.2. 예제 compose는 mariadb:latest |
| 캐시 · 작업 큐 | Redis / Valkey 8 + RQ 2.7 | RQ(Redis Queue)로 스캔 같은 무거운 작업을 백그라운드 실행. 포크한 rq-scheduler로 예약 |
| 실시간 | python-socketio 5.16 | 스캔 진행률·로그를 WebSocket으로 프론트에 실시간 푸시 |
| 인증 · 검증 | authlib·joserfc(JWT)·passlib[bcrypt] / pydantic 2.11 | OAuth2·OIDC·세션. 응답 검증·페이지네이션(fastapi-pagination) |
| 관측성 | OpenTelemetry + Sentry | 분산 트레이싱·에러 리포팅까지 갖춘 프로덕션 지향 |
| 항목 | 선택 | 설명 |
|---|---|---|
| 프레임워크 | Vue 3.4 (Composition API) | <script setup> 스타일. 컴포넌트 ~168개 |
| UI 라이브러리 | Vuetify 3.9 + Tailwind 4 | 머티리얼 디자인 컴포넌트 + 유틸리티 CSS 병용 |
| 빌드 · 언어 | Vite + TypeScript 5.7 | vue-tsc 타입 체크. Node 24 / npm 11 요구 |
| 상태 · 라우팅 | Pinia 3 (스토어 18개) + vue-router 4 | HTTP는 axios. i18n(vue-i18n)로 17개 언어 |
| 실시간 · PWA | socket.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 스테이지에서 직접 컴파일 |
| 컨테이너 | 멀티스테이지 Dockerfile | frontend-build → backend-build → rahasher → emulator → nginx → 최종 이미지 |
| 라이선스 | AGPL-3.0-only | 네트워크로 제공하는 개작본도 소스 공개 의무(카피레프트 강함) |
FastAPI = 두뇌(API·로직), MariaDB = 서류함(게임 정보 저장), Redis + RQ = 알바생(오래 걸리는 스캔을 뒤에서 처리), Vue/Vuetify = 얼굴(웹 화면), EmulatorJS = 게임기(브라우저 안 콘솔), Nginx = 안내데스크 겸 짐꾼(요청 분배 + 무거운 롬 파일 나르기). RomM은 이 조각들을 Docker 한 컨테이너로 묶었다.
high_prio_queue(급한 것)와 low_prio_queue(느긋한 것) 두 갈래로 나눠 돌린다.한 개의 Docker 컨테이너 안에서 요청·스캔·플레이가 어떻게 흐르는가.
롬 파일은 수백 MB~수 GB에 이른다. 이걸 Python(FastAPI)이 읽어서 응답에 실어 보내면 파이썬 프로세스가 파일 하나 내보내는 내내 묶여 다른 요청을 못 받는다. RomM은 X-Accel-Redirect 기법을 쓴다: FastAPI는 "이 요청 허가한다, 실제 파일은 /library/...에 있다"는 헤더만 돌려주고, 실제 파일 전송은 Nginx가 담당한다. 파이썬은 권한 검사(인증·소유권)만 하고 즉시 자유로워지므로, 대용량 다운로드가 서버 전체를 느리게 만들지 않는다.
스캔의 핵심은 backend/handler/scan_handler.py다. 사용자가 스캔을 누르면 API는 즉시 응답하고 실제 작업은 RQ 워커로 넘긴다. 스캔 종류(ScanType)는 NEW_PLATFORMS(새 플랫폼만)·QUICK(빠르게)·UPDATE·UNMATCHED(매칭 실패한 것만 재시도)·COMPLETE(전체)·HASHES(해시만) 6가지다. 상황별로 필요한 만큼만 돌려 비용·시간을 아끼는 설계다.
롬 식별의 1차 단서는 해시(파일의 지문)다. roms_handler.py가 CRC32·MD5·SHA1을 계산하고, CD 이미지(CHD)는 별도 chd_sha1_hash를 만든다. 이 해시를 Hasheous·Playmatch 같은 해시 DB에 던져 "정확히 이 게임"을 찾아낸다. 라즈베리파이처럼 약한 기기를 위해 skip_hash_calculation으로 해시 계산을 끌 수도 있다.
RomM은 무려 13개 메타데이터 공급자를 지원한다: IGDB·MobyGames·ScreenScraper·RetroAchievements·LaunchBox·Hasheous·TheGamesDB·SteamGridDB·Flashpoint·HowLongToBeat·ES-DE gamelist·Libretro 썸네일·Playmatch. 각 소스는 handler/metadata/의 핸들러 + adapters/services/의 저수준 클라이언트 2층으로 나뉜다. 어떤 소스를 먼저 믿을지는 config.yml의 scan.priority.metadata/.artwork/.region/.language로 사용자가 정한다.
adapters/services/igdb.py 등)를 두고, 그 위에 통일된 규칙으로 여러 소스를 조합하는 핸들러 계층을 얹었다. 덕분에 새 메타데이터 소스를 추가할 때 어댑터 하나만 만들면 되고, 나머지 코드는 손대지 않는다. "바깥세상의 제각각인 것들을, 안쪽에는 하나의 표준 콘센트로 보이게" 하는 패턴이다.
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 클레임에서 자동 매핑된다.
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 + 캐시/큐 + 리버스 프록시. 여기에 "외부 데이터로 보강(스크래핑)", "무거운 작업은 큐로", "대용량 파일은 프록시가 직접 전송"이라는 세 가지 공통 패턴이 얹힌다. 이 구조 하나를 제대로 이해하면 자기호스팅 앱 대부분의 설계를 읽을 수 있게 된다.
루트와 backend/·frontend/의 주요 폴더가 각각 무슨 일을 하는가.
구조의 핵심은 "요청을 받는 층(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.yml과 config.example.yml로 실제 배포·설정 이해. 이 다섯이면 뼈대가 잡힌다.
이 레포에서 실제로 배워 갈 수 있는 것들.
튜토리얼 수준의 FastAPI는 라우터 몇 개가 전부지만, RomM은 미들웨어 스택·다중 인증·페이지네이션·OpenAPI 스키마 export·비동기 작업 위임까지 실무 요소를 다 보여준다. 특히 main.py가 라우터를 개별 import해 include_router로 붙이고 lifespan으로 기동/종료를 관리하는 방식은 대형 FastAPI 앱의 표준 골격이다.
미니 API를 만들되, 인증 미들웨어 한 개 + Redis 세션 한 개를 직접 붙여 보라. RomM의 handler/auth/middleware/를 참고하면 "요청이 라우터에 닿기 전에 가로채는" 미들웨어 감각이 잡힌다.
models/의 rom.py·platform.py 등은 SQLAlchemy 2.0 스타일 모델의 좋은 예제다. 더 값진 건 alembic/versions의 93개 마이그레이션이다 — 실제 서비스가 스키마를 어떻게 점진적으로 진화시켜 왔는지 타임라인으로 읽을 수 있다. "컬럼 추가할 때 기존 데이터는 어떻게 채웠나"를 보면 실전 DB 운영이 보인다.
작은 SQLAlchemy 프로젝트에서 Alembic으로 마이그레이션 3개를 만들어 보라(테이블 생성 → 컬럼 추가 → 인덱스 추가). alembic upgrade/downgrade로 앞뒤로 오가는 걸 체험하면 마이그레이션의 두려움이 사라진다.
"오래 걸리는 일은 큐로 넘긴다"는 웹 개발의 필수 패턴이다. RomM은 스캔·정리를 RQ로 넘기고, 진행률을 Socket.IO로 실시간 push한다. API 응답 즉시 반환 → 워커가 처리 → WebSocket으로 진행 알림이라는 이 삼각 구조는 파일 업로드·리포트 생성·이메일 발송 등 어디에나 쓰인다.
RQ로 "10초 걸리는 가짜 작업"을 큐에 넣고, 워커가 처리하는 동안 진행률을 Socket.IO(또는 SSE)로 브라우저에 흘려보내는 최소 데모를 만들어 보라.
RomM의 프론트는 백엔드 OpenAPI 스키마에서 타입·API 클라이언트를 자동 생성(__generated__/)한다. 백엔드가 응답 형태를 바꾸면 프론트 타입도 재생성되어 컴파일 에러로 잡힌다 — "프론트-백 계약을 코드로 강제"하는 현대적 워크플로다. Pinia 스토어 18개로 상태를 도메인별(롬·플랫폼·유저·설정…)로 나눈 구성도 참고할 만하다.
docker/Dockerfile은 frontend-build(Node로 Vue 빌드) → backend-build(uv sync) → rahasher-build(C 컴파일) → emulator-stage(EmulatorJS 다운로드) → nginx → 최종 슬림 이미지 순으로 스테이지를 쌓는다. 빌드 도구는 최종 이미지에 안 남기고 결과물만 복사하는 멀티스테이지의 교과서다. 서로 다른 언어(Python·JS·C)를 한 이미지에 조립하는 실전 사례이기도 하다.
dist/)만 있으면 된다. 멀티스테이지를 쓰면 무거운 빌드 도구는 버리고 알맹이만 남겨, 최종 이미지가 작고 안전해진다. RomM처럼 언어가 여러 개 섞인 앱에 특히 유용하다.무엇이 있어야 돌릴 수 있는가 — 그리고 폴더는 어떻게 구성하는가.
| 구분 | 요구사항 |
|---|---|
| 실행 방식 | 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) 아래는 이렇게 생겼다:
폴더명이 표준 슬러그와 다르면(예: 게임큐브 폴더를 gc로 만들었는데 RomM은 ngc를 기대) config.yml의 system.platforms 바인딩으로 별칭을 매핑(gc: ngc, ps1: psx)한다. 즉 "내 폴더 이름 = RomM 플랫폼 ID"라는 연결을 설정으로 교정할 수 있다.
난이도별로. 위에서 아래로 갈수록 어렵다. (합법적으로 소유·백업한 롬 전제)
examples/docker-compose.example.yml과 config.example.yml을 복사해 로컬(또는 NAS)에 RomM + MariaDB를 올려라. library/{platform}/roms/ 구조를 지켜 자유 라이선스 홈브루 롬 한두 개를 넣고, 웹 UI에서 스캔이 도는 걸 확인한다.
여러 종류의 스캔(QUICK, UNMATCHED, COMPLETE)을 각각 돌려 보고, 브라우저 개발자도구 네트워크 탭에서 Socket.IO(WebSocket) 메시지로 진행률·로그가 실시간 오는 걸 확인하라. RQ 워커 로그와 대조하면 "API → 큐 → 워커 → 소켓" 흐름이 눈에 보인다.
scan.priority.metadata 순서를 바꿔(예: ScreenScraper를 IGDB보다 앞으로) 재스캔한 뒤, 표지·설명이 어떤 소스에서 왔는지 달라지는 걸 비교하라. 플랫폼 바인딩(gc: ngc)도 실험해 폴더명 교정을 익힌다.
backend/handler/scan_handler.py의 ScanType·MetadataSource 열거형과 filesystem/roms_handler.py의 해시 계산(_make_file_hash)을 따라가며, "폴더 → 플랫폼 인식 → 해시 → 매칭 → 보강" 파이프라인을 직접 시퀀스 다이어그램으로 그려 보라.
adapters/services/의 기존 클라이언트 하나를 본떠, 로컬 JSON 파일을 소스로 삼는 "가짜 메타데이터 어댑터"를 만들고 handler/metadata/에 핸들러를 붙여 보라. 어댑터 패턴이 새 소스 추가를 얼마나 국소적으로 만드는지 체감할 수 있다.
이 레포를 완전히 소화하기 위한 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 같은 다른 자기호스팅 앱의 구조도 같은 눈으로 읽을 수 있게 된다.
본문에 나온 용어를 한 곳에.
| 용어 | 뜻 |
|---|---|
| RomM | 롬을 스캔·보강·탐색·재생하는 자기호스팅 게임 라이브러리. FastAPI + Vue, AGPL-3.0. |
| ROM | 옛 게임기 매체(카트리지·CD)를 파일로 덤프한 것. 에뮬레이터로 실행. |
| 셀프호스팅 | 클라우드 대신 내 서버(NAS·홈랩)에 직접 설치·운영하는 방식. |
| 메타데이터 보강(Enrich) | 파일명·해시로 게임을 식별해 외부 DB에서 표지·설명·출시일을 자동으로 붙이는 과정. |
| 해시(CRC32/MD5/SHA1) | 파일 내용에서 만든 고유 지문. 파일명이 엉망이어도 "이 게임"을 정확히 식별하는 근거. |
| EmulatorJS | 브라우저(WebAssembly)에서 콘솔 게임을 실행하는 웹 에뮬레이터. RomM의 "바로 플레이"를 담당. |
| RQ (Redis Queue) | Redis 기반 파이썬 작업 큐. 스캔 같은 무거운 일을 워커가 뒤에서 처리. |
| X-Accel-Redirect | FastAPI는 권한만 검사하고, 실제 대용량 파일 전송은 Nginx가 맡게 하는 위임 헤더. |
| COOP / COEP | 브라우저 격리 헤더. 있어야 SharedArrayBuffer(멀티스레드 에뮬)를 쓸 수 있음. RomM은 플레이어 경로에만 부여. |
| 어댑터 패턴 | 제각각인 외부 API를 앱 내부의 공통 형태로 번역하는 계층. 새 소스 추가를 국소화. |
| Alembic 마이그레이션 | DB 스키마 변경을 버전으로 관리하는 도구. RomM엔 93개가 쌓여 있음. |
| OIDC | OpenID Connect. 외부 신원공급자(Authentik 등)로 로그인·역할 매핑을 위임하는 표준. |
| 멀티스테이지 빌드 | Docker에서 빌드 도구는 버리고 결과물만 남겨 최종 이미지를 작게 만드는 기법. |