사과게임 만드는 법 — v0부터 배포까지
사과게임 만드는 법을 v0부터 배포까지 버전별로 기록한다. 시작은 “게임 아이디어 10개 뽑아줘”였다. 조건은 간단하지만 머리는 써야 하고, 남녀노소 다 즐길 수 있고, 아이 두뇌발달·어르신 치매예방에 도움이 되고, 설명 없이도 직관적으로 알 수 있을 것. 여기서 v13까지 몇 시간 만에 불어났다. 구구단 드롭에 이은 세 번째 실험이고, 결과물은 사과게임 10 만들기에서 바로 해볼 수 있다.
v0: 후보 10개 중 사과게임을 고르다
드래그한 사각형 안 숫자 합이 10이면 사라지는, 한때 전국민이 했던 그 게임을 골랐다. 이유는 두 가지. 재미와 리플레이성이 이미 검증된 포맷이라는 것, 그리고 규칙을 설명할 필요가 아예 없다는 것 — 드래그해보고 사라지는지 안 사라지는지만 보면 5초 안에 다 안다.
v1: 원조 그대로 — 지우면 그 자리가 계속 비었다
첫 버전은 원조 방식 그대로였다. 한 번 지운 자리는 빈칸으로 남고, 더 이상 10을 만들 수 없으면 게임이 끝났다. 2분 타이머, 난이도 3단계, 5초 지나면 자동으로 뜨는 힌트(무한 반복 애니메이션), 효과음, localStorage 최고 기록까지 이 시점에 다 있었다.
v2: 리필과 시간 보상을 넣다
판에 사과가 잔뜩 남아 있어도 조합만 고갈되면 끝나버리니 “너무 쉽게 끝난다”는 반응이 나왔다. 지운 자리에 바로 새 숫자를 채우는 식으로 바꾸고, 지울 때마다 시간을 조금 보상했다(칸당 0.3초). 판이 막히면 자동으로 조용히 한 번 섞고 이어가는 안전장치도 이때 넣었다 — 아직 횟수 제한은 없었다.
v3: 판 크기를 1/4로
가로·세로를 각각 절반으로 줄여달라는 요청으로 쉬움 4×5(20칸)·보통 5×7(35칸)·어려움 6×8(48칸)이 됐다. 원래 8×10·10×13·12×16이었던 걸 정확히 4분의 1 면적으로 맞췄다.
v4: 정말 막히면 게임을 끝내다
판이 작아지니 “정말 막히는” 경우가 실제로 늘 수 있다는 지적이 나왔다. 그래서 한 번 섞어도 여전히 10을 만들 조합이 없으면(숫자 구성 자체가 불가능한 경우) 그 자리에서 게임을 종료하도록 만들었다.
삽질 1: 화면 숫자와 실제 값이 어긋났다
리필을 넣으면서 “논리 상태는 즉시 새 값으로, 화면 연출만 260ms 늦게”로 짰었다. 사라지는 애니메이션이 끝나기 전에 다음 판을 계산해도 안전하게 하려던 의도였는데, 이게 정확히 반대 문제를 만들었다. 팝 애니메이션이 도는 260ms 동안 화면엔 옛날 숫자가 그대로 보이는데, 합계 계산은 이미 새 숫자를 쓰고 있었다. 드래그가 빠른 사람일수록 이 순간을 자주 밟았다. “표시된 숫자랑 골랐을 때 실제 숫자가 다른 경우가 있어”라는 제보로 잡혔다.
v5: 화면과 값을 같은 순간에 바꾸다
고친 방법은 단순했다. 화면 표시와 논리 값을 같은 순간에 바꾸는 것. 애니메이션 시작 시점엔 둘 다 옛 값 그대로 두고, 끝나는 260ms 뒤에 텍스트와 값을 동시에 새 숫자로 바꿨다. 그 뒤로 판 전체를 15라운드, 525회 검사했는데 단 한 번도 어긋나지 않았다.
v6: 셔플과 힌트를 “한 번만”으로
이때쯤 “힌트가 나오는 조건이 뭐고 무한 셔플이 가능한지” 물어봐서 동작을 설명했는데, 그 답이 마음에 안 들었는지 규칙 자체가 바뀌었다. 셔플은 게임 한 판에 딱 1번만, 힌트도 5초 지나면 자동으로 뜨던 걸 없애고 버튼을 눌러야만 한 번(2.8초 애니메이션 한 사이클) 보여주게 했다. 판에 조합이 없으면 그냥 넘어가지 않고 알림창을 띄우기로 했는데, 이때는 아직 alert()을 썼다.
v7: alert() 대신 확인창
“확 바뀌어. 알림창을 띄우고, 확인 버튼을 눌러야 셔플되게” — alert()은 브라우저에 따라 확인 없이 그냥 넘어갈 수 있어 못 믿을 물건이었다. 게임 결과창과 같은 껍데기(모달)를 재활용해서, 확인 버튼을 눌러야만 셔플이든 게임 종료든 실제로 실행되게 만들었다.
v8: 시간이 다 돼도 남은 셔플로 한 번 더
“셔플 기회가 아직 남아있으면 마지막에 바로 안 끝나고 한 번 더 기회를 줘야 하지 않을까”에 “둘 다”라는 답이 왔다. 판이 막혔을 때뿐 아니라, 타이머가 0이 되는 순간에도 셔플 기회가 남아있으면 종료 대신 알림창을 띄우고 보너스 시간(+15초)을 제안하게 만들었다.
v9: 큰 숫자가 옆자리를 먹통으로 만든다
판을 1/4로 줄인 뒤로 “9가 나오는 순간 옆에 죽는 숫자들이 너무 많아진다”는 지적이 나왔다. 9는 짝지을 수 있는 숫자가 1뿐이라(9+1=10, 그 이상은 다 초과) 생기는 문제였다. 숫자 등장 확률에 가중치를 줘서(1은 9배, 9는 1배) 낮은 숫자가 더 자주 나오게 했다. 90,000회 샘플링해보니 1은 20%, 9는 2.1%로 설계한 그대로 나왔다.
v10: 시작 커버 + 극적인 타이머
“판을 다 가려놓고 시작 버튼을 눌러야 공개되게, 타이머는 크고 중앙에, 20초부턴 경고, 10초부턴 더 극적으로.” 판을 덮는 커버와 “시작” 버튼을 넣고, 타이머를 점수·목표 줄에서 분리해 화면 중앙에 크게 뒀다. 20초 이하는 주황색 펄스, 10초 이하는 빨간색 심장박동 펄스 + 매초 째깍 소리를 넣었다.
v11: 보상을 더 후하게
“빠르게 해도 결국 시간 타이머가 오버되네”라는 말에 칸당 시간 보상을 0.3초에서 0.6초로 두 배 올렸다. 빠른 플레이를 시뮬레이션해보니(20라운드, 45칸, 6초 소요) 보너스가 27초 붙어서 순수하게 +20.9초가 남는 걸 확인했다.
삽질 2: 알림창 뒤에서 타이머가 몰래 돌았다
“알림창이 뜨면 기존 타이머는 멈춰야 하지 않을까”라는 말에 pauseTimer()/resumeTimer()를 만들어 알림 뜨면 멈추고 확인 누르면 멈췄던 만큼 보정해서 이어가게 했다. 그런데 여기서 진짜 버그가 나왔다. 알림창이 연쇄로 뜨는 경우(판이 막힘 → 셔플해도 안 풀림 → 종료 알림) 첫 번째 알림의 확인 콜백이 무조건 타이머를 재개시켜버려서, 두 번째 알림이 화면에 떠 있는 동안에도 타이머가 뒤에서 조용히 돌고 있었다.
v12: 재개는 마지막 알림에게
확인 직후에 다른 알림이 새로 떴는지 검사해서, 떠 있으면 재개를 그 알림에 넘기는 식으로 고쳤다. 실제 클릭으로 두 단계 알림을 다 눌러보고 나서야 확실히 잡혔다는 걸 확인했다. 이 사이에 “내 최고 기록을 계속 저장할 수 있냐”는 질문도 나왔는데, 뭘 얼마나 저장할지 정하지 못한 채 다음 주제로 넘어가서 이 요청은 아직 미해결이다.
v13: 사이트에 배포하다
“이거 sonnyplot.com에 올려줘.” 단독 HTML을 구구단 드롭과 같은 구조의 Astro 컴포넌트로 옮겼다. 사과·연출 요소를 JS가 만들어서 스코프 스타일이 안 붙는 문제는 .a10 접두사 전역 스타일로, onclick 전역 의존은 즉시실행 함수 + addEventListener로 해결했다. 영어판도 같이 써서 /apple-game·/kr/apple-game 두 언어로 올렸다.
라이트 모드에서 사라진 숫자. 원래 게임은 어두운 배경을 통째로 깔고 그 위에 거의 흰색(#fff6ef) 글씨를 썼다. 그런데 사이트에 붙이면 점수·타이머는 게임판 바깥, 즉 사이트 배경 위에 놓인다. 사이트가 라이트 모드로 뜨면 흰 배경에 흰 글씨나 다름없어서 타이머 숫자가 통째로 안 보였다. 로컬 dev 서버로 라이트 모드를 직접 켜보고서야 잡았다. 게임판 안쪽(사과·모달)은 원래 팔레트를 유지하고, 게임판 바깥(점수·타이머·진행바)만 사이트의 라이트/다크 적응형 색상 변수로 바꿨다.
빌드는 됐는데 배포가 안 됐다. 커밋하고 push했는데 10분이 지나도 sonnyplot.com에 반영이 안 됐다. 로컬에서 클린 설치(rm -rf node_modules && npm ci)로 다시 빌드해봐도 문제없이 통과했고, JSON 문법·파일명 대소문자·에셋 용량도 다 확인했는데 이상이 없었다. Cloudflare 대시보드를 열어보고서야 원인이 보였다 — npm run build는 초록불이었는데, 그다음 배포 단계(npx wrangler deploy)에서 빨간불이 뜨고 있었다. 로컬에서는 wrangler 배포 자체를 재현할 수 없어서(로그인이 안 돼 있었다) 실제 원인까지는 못 봤지만, 재시도하니 정상적으로 붙었다.
결과
사과게임 10 만들기에서 바로 해볼 수 있다. 시작 전엔 판이 가려져 있고, “시작”을 누르면 공개된다. 타이머는 화면 중앙에 크게 있고, 20초부턴 주황색으로 경고하다가 10초부턴 심장박동처럼 커지면서 매초 째깍 소리가 난다. 지울 때마다 시간도 조금씩 벌리고, 판이 막히거나 시간이 다 되면 딱 한 번 섞을 기회가 있다.
소감
버전을 다 늘어놓고 보니 진짜 버그 두 건(v1의 화면/값 시차, v12의 알림창 재개)이 전부 화면에 보이는 것과 실제로 계산되는 것 사이의 시차에서 나왔다. 애니메이션이나 알림창처럼 “잠깐 기다리는” 구간을 넣을 때마다, 그 사이에 사용자가 뭘 보고 있고 시스템은 실제로 뭘 하고 있는지를 따로 그려봐야 한다는 걸 다시 배웠다. 그리고 요청 하나하나는 작았는데 다 합치니 v13까지 갔다 — 작은 피드백을 계속 받는 개발이 원래 이런 식으로 불어난다는 것도.