2026-10-01 · 심심오락실(simsim-arcade) 팀 프로젝트
심심오락실은 예능식 퀴즈 게임과 추억의 플래시 스타일 게임을 웹에서 즐길 수 있게 만드는 팀 프로젝트입니다.
Cloudflare 하나로 웹(Pages), API(Workers), DB(D1)를 모두 운영하고, 팀원 4명이 웹 공통 작업과 "1인 1게임" 게임 카드를 나눠 맡고 있습니다.
오늘 제가 머지한 PR은 6개입니다.
| PR | 종류 | 내용 |
|---|---|---|
| #44 | 공통(프론트) | 윈도우 7 · 11 테마 추가 |
| #45 | 공통(프론트) | 윈도우 11 테마 시작 버튼 · 글씨 대비 개선 |
| #48 | 공통(프론트) | 2D 물리 엔진 matter-js 추가 |
| #49 | 게임 카드 9 | 의자 탑 쌓기 |
| #51 | 게임 카드 9 | 의자 탑 쌓기 난이도 쉬움 · 어려움 |
| #52 | 공통(프론트) | 아직 등록하지 않은 카드 칸 숨기기 |
사이트에는 이미 XP · 98 · 클래식 세 가지 테마가 있었습니다.
XP와 98은 "바탕화면 + 창 + 작업 표시줄 + 시작 메뉴" 구조를 같이 쓰고, 스타일만 테마별 CSS 파일로 나눠 둔 구조입니다.
처음에는 10과 11 중 하나만 넣으려고 했습니다.
그래서 7과 11을 둘 다 넣었습니다.
구현 방식
Theme 타입에 win7, win11을 추가하면 타입 검사가 빠진 곳을 전부 알려 줍니다.global.css에 테마별 색 토큰(--bg, --surface, --accent 등)과 공통 버튼 스타일을 넣었습니다.*.win7.module.css, *.win11.module.css) 20개를 추가했습니다.7 테마는 짙은 파랑 위에 빛줄기가 퍼지는 바탕화면, 뒤가 흐리게 비치는 유리 창 테두리(backdrop-filter: blur), 둥근 시작 버튼(오브), 위아래 두 톤 광택 버튼으로 만들었습니다.
11 테마는 둥근 창, 가운데 정렬 작업 표시줄, 가운데에서 떠오르는 시작 메뉴, Fluent 스타일 버튼과 카드로 만들었습니다.
CSS 그라디언트만으로 바탕화면을 그려서 이미지 파일은 하나도 추가하지 않았습니다.
써 보니 11 테마에서 불편한 점이 두 가지 있었습니다.
#5f5f5f에서 #4a4a4a로 진하게, 제목 글씨는 조금 키우고 굵게 했습니다."진짜 윈도우처럼"보다 "우리 사이트에서 쓰기 편하게"가 먼저라는 걸 다시 느꼈습니다.
이번 제 세 번째 게임은 보드게임에서 의자를 쌓아 올리는 놀이를 옆에서 보는 2D 물리 게임으로 옮긴 것입니다.
다리가 달린 울퉁불퉁한 의자가 기울고 걸치는 걸 직접 계산하기는 어렵습니다. 탑이 덜덜 떨리거나 서로 뚫고 지나가는 버그가 생기기 쉽습니다.
그래서 2D 물리 엔진 중 가장 많이 쓰이고, 여러 도형을 합친 모양(복합 물체)을 지원하는 matter-js를 골랐습니다.
다만 우리 팀 규칙상 게임 PR에서는 내 게임 폴더와 등록 줄만 고칠 수 있고, package.json이나 lockfile 같은 공통 파일은 건드릴 수 없습니다.
그래서 순서를 나눴습니다.
feature/fe-add-matter-js — 라이브러리만 추가하는 공통 PR (#48)feature/game-chair-stack — 게임 PR (#49). 처음엔 #48 위에서 작업하다가 #48이 머지된 뒤 git rebase --onto로 최신 main 위로 옮겼습니다.게임은 React.lazy로 따로 불러오기 때문에 matter-js는 게임 코드에만 들어가고(gzip 약 32KB) 메인 화면 번들 크기에는 영향이 없습니다.
의자는 옆에서 본 모습을 직사각형 부품 여러 개로 정의하고, matter-js의 Body.create({ parts })로 하나의 물체로 묶었습니다.
{
id: 'basic',
name: '기본 의자',
parts: [
{ x: 0, y: 0, w: 56, h: 7 }, // 앉는 판
{ x: -24.5, y: -28, w: 7, h: 49 }, // 등받이
{ x: -24, y: 22, w: 6, h: 37, dark: true }, // 다리
{ x: 24, y: 22, w: 6, h: 37, dark: true },
],
}
월드 좌표 1 = 1cm로 잡아서, 점수(높이)를 그대로 cm로 보여 줄 수 있게 했습니다.
그리기는 matter-js 렌더러 대신 캔버스에 직접 부품 꼭짓점을 그려서, 의자 색과 높이 눈금, 최고 높이 점선, 다음 의자 미리보기를 자유롭게 넣었습니다.
화면을 띄우기 전에 Node에서 물리만 따로 시뮬레이션해 보는 테스트를 만들었습니다.
의자를 받침대 가운데에 똑바로 떨어뜨리고, 멈출 때까지 돌린 뒤 기울기와 높이를 출력하는 방식입니다.
single basic settle=300f tilt=-160° height=-785cm FELL
single stool settle= 98f tilt= 0° height=46cm
single highback settle=300f tilt=-401° height=-2202cm FELL
기본 의자와 등받이 높은 의자가 아무것도 안 했는데 넘어져서 떨어졌습니다.
프레임별로 기울기를 찍어 보니, 착지한 뒤 각도가 0.5° → 3° → 4.7° → -1.3° → … → -35°처럼 흔들림이 점점 커지다가 넘어지고 있었습니다.
원인은 부품 모서리를 둥글게 깎는 옵션(chamfer)이었습니다.
폭 6cm짜리 가는 다리 끝을 둥글게 깎으니 다리 끝이 흔들의자처럼 굴러다녔던 것입니다.
고친 내용은 세 가지입니다.
chamfer 제거Body.setInertia) — 덜덜 떨림이 커지지 않게frictionAir: 0.02)과 반발 계수 0single basic settle=103f tilt= 0° height=93cm
single highback settle=102f tilt= 0° height=113cm
이후 7종류 모두 혼자 똑바로 섰고, 조준 없이 가운데에만 계속 떨어뜨리면 3~6개쯤에서 무너지는 정도의 난이도가 나왔습니다.
캔버스·requestAnimationFrame·ResizeObserver를 가짜로 채워서 엔진을 한 판 끝까지(게임 오버 → 다시 시작) 돌리는 스모크 테스트도 같이 돌렸습니다.
의자 모양이 매번 바뀌니 처음 하는 사람에게는 꽤 어려웠습니다. 그래서 난이도를 나눴습니다.
| 난이도 | 의자 | 점수 |
|---|---|---|
| 쉬움 | 모두 같은 기본 의자 (색만 무작위) | 최고 높이(cm) |
| 어려움 | 7가지 의자가 무작위 | 최고 높이 × 1.5 |
랭킹은 게임마다 하나뿐이라, 배율이 없으면 쉬움이 같은 랭킹에서 유리해집니다.
그래서 국기 퀴즈의 난이도 배율처럼 어려움에 ×1.5를 줬고, 결과 화면에 "어려움 · 최고 높이 109cm × 1.5"처럼 점수 계산 과정을 같이 보여 주도록 했습니다.
우리 팀은 게임 카드 번호를 순서대로 돌아가며 받습니다(혁 1·5·9, 경수 2·6·10 …).
지금까지 메인 화면은 번호 칸을 미리 만들어 두고, 게임이 없는 칸은 "🚧 준비 중"으로 보여 줬습니다.
하지만 번호는 이미 정해져 있으니 등록하면 그때 보여 주면 되는 일이었습니다.
registry.ts에 GAMES_BY_CARD를 만들었습니다.결과적으로 추가 17줄, 삭제 224줄. 기능을 줄이면서 코드도 줄어드는 작업은 언제나 기분이 좋습니다.
팀원 PR(#50)이 머지된 뒤 GitHub Actions의 CI가 "cancelled"로 표시되어 확인해 봤습니다.
ci.yml이 같은 브랜치의 이전 실행을 취소하도록(cancel-in-progress: true) 설정되어 있어서 #50의 CI가 취소된 것이었습니다.main CI는 #50의 코드까지 포함해서 성공했고, Cloudflare Pages 배포도 두 커밋 모두 성공했습니다.실패가 아니라 취소였고, 다시 돌릴 필요는 없었습니다.
다만 머지가 몰릴 때마다 헷갈릴 수 있어서, main에서는 끝까지 돌리고 PR에서만 취소하도록 바꾸는 것도 검토해 볼 만합니다.
cancel-in-progress를 PR에서만 동작하도록 조정 검토