미니게임 포털
운영 중게임 1종 = 모듈 1개 구조로 28종까지 늘린 한국어 웹게임 포털
- 기간
- 2026-08 ~ 현재
- 커밋
- 255
- 게임
- 28종
- E2E 스펙
- 49개
- D1 마이그레이션
- 17건
기술 스택
- Cloudflare Workers
- Next.js
- OpenNext
- Playwright
- React
- Sentry
- Tailwind CSS
- Three.js
- TypeScript
실제 화면 운영 중인 서비스를 그대로 캡처했습니다
2026-10-06 기준

개요
무엇이고 누구를 위한 것인가설치·가입 없이 브라우저에서 바로 즐기는 한국어 미니게임 포털이다. 2048·스도쿠·지뢰찾기·소코반 같은 고전 퍼즐부터 3D 슈팅 레인지·스네이크 3D까지 28종을 PC와 모바일에서 플레이할 수 있다.
혼자 하는 게임에서 그치지 않고, 매일 바뀌는 데일리 퍼즐·랭킹·업적·친구 대전·토너먼트로 다시 찾아올 이유를 만드는 단계까지 와 있다.
문제
무엇이 문제였나시장에 빈 자리가 있었다. "네이버 작은게임즈" 같은 국내 웹게임 포털이 있을 거라 생각했지만 DNS·웹 아카이브·공식 블로그를 전부 확인한 결과 존재하지 않았다. 슈퍼앱들의 게임 허브는 폐쇄형·큐레이션형이라, 누구나 바로 들어가 즐길 수 있는 한국어 웹 포털은 사실상 공백이었다.
그 공백을 메우려면 카탈로그를 빠르게 쌓아야 하는데 두 가지가 발목을 잡는다. 게임마다 화면·입력·저장을 새로 만들면 속도가 안 나고, 반대로 카탈로그가 커지면 목록 페이지 번들에 모든 게임 코드가 딸려 들어와 첫 화면이 무거워진다.
그리고 캐주얼 게임은 한 번 하고 떠난다. 카탈로그만 늘려서는 재방문이 생기지 않는다.
접근
어떻게 풀었나- 게임 1종 = 모듈 1개 = 브랜치 1개 = PR 1개. 모든 게임은
create(container, ctx) → { destroy, pause, resume }표준 인터페이스를 구현하는 순수 TypeScript + Canvas(또는 DOM) 모듈이다. 포털은 게임 내부를 모르고 레지스트리만 읽는다. - 메타와 본체를 갈랐다. 목록 페이지는 가벼운
meta.ts만 정적으로 참조하고, 게임 본체는 동적import로 코드 스플리팅한다. 게임이 28종이 돼도 첫 화면 번들은 그대로다. - 재방문 장치를 단계적으로 쌓았다. 1단계는 localStorage 최고 점수, 2단계는 데일리 퍼즐과 서버 랭킹(D1), 이후 업적·친구 대전·주간 도전·토너먼트 순으로 — 검증된 단계 위에 다음 단계를 올렸다.
주요 기능
사용자가 실제로 쓰는 것- 게임 28종 — 퍼즐(스도쿠·소코반·지뢰찾기·2048·리버시), 아케이드(스네이크·블록 쌓기·과일 떨어뜨리기), 두뇌(반응속도·타자), 3D(슈팅 레인지·미로·소코반·스네이크·블록 쌓기 3D), 동계 스포츠(스피드스케이팅·봅슬레이·스키점프)
- 오늘의 단어 — 매일 바뀌는 한글 워들. 스트릭 기록
- 랭킹 — 게임별 일간·전체 리더보드
- 업적·칭호·프로필 — 플레이 스타일 통계
- 친구 초대 1:1 온라인 대전 — 링크로 초대, 2초 폴링, 초읽기, 서버 검증
- 주간 도전 — 주차마다 바뀌는 과제, 고정 시드로 모두 같은 문제, 최소 수 순위
- 비동기 8강 토너먼트 — 링크로 모아 시작, 라운드 3일 자동 진행
- 공략 페이지 — 게임별 가이드
- 소셜 로그인 — 카카오·구글
아키텍처
어떻게 구성돼 있나 web/src/games/
registry.ts ── meta.ts × 28 (정적, 가벼움)
└─ import("./<id>") × 28 (동적, 게임 진입 시에만)
<id>/ create(container, ctx) → { destroy, pause?, resume? }
│
Next.js 포털 (OpenNext) ── Cloudflare Workers ── game.azclab.com
├─ localStorage 최고점·설정·스트릭
├─ D1 (17 migrations) 랭킹·업적·초대·대전·주간 도전·토너먼트·공지
└─ R2 대형 에셋·OG 이미지표준 인터페이스 덕분에 게임만 떼어내 HTML 한 장으로 감싸면 CrazyGames·Poki 같은 글로벌 포털 제출물이 된다(둘 다 iframe + 단일 엔트리를 요구한다). 데일리 퍼즐은 날짜에서 시드를 만드는 결정론 방식이라 서버에 정답을 저장하지 않아도 모두가 같은 문제를 푼다.
기술적 의사결정
무엇을 고르고 무엇을 버렸나- 성능 예산을 숫자로 못 박고, 근거 있게 바꿨다. 게임당 초기 로드 5MB 미만, 플레이 시작까지 5초 이내. 처음엔 1MB였는데 근거 없이 정한 값이었고 상세한 그래픽을 막는 병목이었다. 업계 기준(Poki 8MB, CrazyGames 50MB)을 확인하고 5MB로 올렸다.
- 원본 IP를 저장하지 않고 남용을 막았다. 랭킹 등록은 같은 출처에서 분당 5건으로 제한하는데, 출처 식별에 IP의 SHA-256 앞 8바이트만 쓴다. 남용 방지에 필요한 만큼만 안다.
- 점수 검증을 게임마다 현실 범위로 좁혔다. 기존 검증은 "0~1000만" 하나뿐이라 0초 클리어·1000만 점이 그대로 통과했다. 게임 메타에
min/max를 두고 API에서 거른다. 범위 안의 위조는 기계로 못 거르므로 어드민 삭제 도구를 함께 만들었다. - 공지는 커밋마다 내지 않는다. 사용자 체감 변경을 모았다가 단계·주 단위로 한 번에 공개한다. 배포 스크립트는 미발행 변경이 쌓였다고 알려 주기만 하고 배포를 막지 않는다.
- 운영 런북을 따로 뒀다. 배포 되돌리기, D1을 사고 이전으로 되돌리기(time travel), 배포 직후 새 페이지 404 대응을 문서로 남겨, 문제가 생긴 순간에 판단하지 않고 절차를 따르게 했다.
결과
무엇이 달라졌나28종을 운영 중이며, 사이트 메타데이터("미니게임 28종")가 리포의 실제 등록 수와 일치한다 — 게임을 추가하면 표기가 따라온다.
- Playwright E2E 스펙 49개로 데스크톱 3종·모바일 3종 브라우저를 돈다.
- 단일 플레이어 포털에서 데일리 → 랭킹 → 업적 → 1:1 대전 → 주간 도전 → 토너먼트까지 재방문 장치를 단계적으로 올렸다.
- 랭킹 무결성 점검에서 실제 버그를 잡았다. 지뢰찾기는 패배 시 0점을 제출하고 있었는데, 클리어 시간이 짧을수록 높은 순위라 패배가 곧 리더보드 1위였다. 패배 시엔 점수를 내지 않도록 고쳤다.
배운 점
다시 한다면"하루 하나"인 데일리 단어 게임에 정답 단어가 76개뿐이었다. 76일 주기로 답이 반복돼, 발견했을 때는 이미 네 번째 사이클이었다 — 연속 기록을 쌓는 게임인데 정답이 재탕되고 있었다. 단어장을 502개로 늘리고, 사이클마다 서로소 간격으로 순서를 섞어 다 쓰고 다시 돌아도 같은 순서가 나오지 않게 했다. 기능이 "동작한다"와 "의도대로 동작한다"는 다르다 — 데일리·랭킹처럼 시간이 지나야 드러나는 로직은 시간 축으로 검사해야 한다.