BUG · 버그 도감 #14
코드 리뷰 한 번에 11건: 같은 값을 여러 곳에서 따로 세던 자리들
스택 타워에 상점 업그레이드를 붙이면서 게임 전체를 다시 읽었더니 버그가 11건 나왔습니다. 2판부터 사라지는 와이드 부스터, 판마다 되살아나는 '첫 10층' 효과, 한 번 일찍 깨지는 연속 목표, 행운 +30%를 통째로 먹어 버린 반올림까지. 대부분은 같은 값을 여러 곳에서 따로 계산한 데서 나왔습니다.
버그 도감은 KKflow 프로젝트에서 실제로 만난 버그를 한 편에 하나씩 기록하는 시리즈입니다. 모든 내용은 해당 저장소의 커밋 기록과 코드를 근거로 합니다. 이번 편은 한 커밋에서 고친 여러 버그를 묶어 다룹니다.
프로젝트 스택 타워 · 시기 2026년 8월 29일 · 관련 기술 게임 상태 관리, 코드 리뷰, 회귀 테스트
업그레이드를 붙이다가 전체를 읽다
8월 29일 오후 4시 40분(한국 시각), 스택 타워에 PR #42가 합쳐졌습니다. 원래 목적은 상점에 영구 업그레이드 네 가지를 붙이는 것이었습니다. 코인 획득 +30%(행운), 퍼펙트 판정 +15%(정밀 조준), 콤보 성장량 +32%(성장 촉진), 시작 블록 +8%(기초 공사). 랭킹이 있는 게임이라 효과를 작게 잡고, 넷 다 최대로 올려도 1만 9천 코인이라는 상한을 두었다고 적혀 있습니다.
그런데 커밋 제목의 뒷부분이 더 깁니다. "그리고 코드 리뷰가 잡은 11건 수정." 업그레이드를 붙이면서 게임 전체를 다시 읽었고, 11건의 버그를 함께 고쳤습니다. 커밋은 이 11건의 공통점을 한 문장으로 적었습니다.
"대부분 '같은 값을 여러 곳에서 따로 계산해서' 생긴 것이다."
이 PR은 Claude Code와 함께 만들었고(커밋에 Claude가 공동 작성자로 적혀 있습니다), 11건 각각을 다시 일으켜 보는 회귀 테스트도 같이 들어갔습니다. 이 글에서는 그중 넷을 자세히, 나머지는 짧게 정리합니다.
1. 와이드 부스터가 2판부터 사라졌다
스택 타워의 스테이지 모드는 한 스테이지 안에서 여러 판을 이어서 합니다. 게임 전에 와이드 부스터를 쓰면 발판(맨 아래 블록)이 1.3배 넓어져 쌓기가 쉬워집니다. 그런데 이 효과가 첫 판에만 있었습니다.
원인은 발판 크기를 계산하는 곳이 셋이었다는 것입니다. 게임을 시작하는 startGame, 다음 판으로 넘어가는 resetStageBoard, 모양 맞추기 판을 다시 까는 shapeReset. 세 곳이 각자 크기를 계산했고, 와이드 배수는 startGame에서만 곱해지고 있었습니다. 커밋의 표현으로 "와이드와 기초 공사가 2판부터 사라졌다."
해결은 크기를 계산하는 함수 boardDims를 하나 만들고, 세 곳이 모두 그것을 부르게 한 것입니다. 와이드 부스터와 새 업그레이드인 기초 공사는 이 함수 안에서 한 번만 곱해집니다. 새 업그레이드를 붙이는 김에 옛 버그도 드러난 셈입니다. 기초 공사를 시작 블록에 곱하려고 크기 계산하는 곳을 찾아보니, 셋이 따로 있었던 것입니다.
같은 수정에서 하나가 더 따라 나왔습니다. 스테이지의 "키우기" 목표는 처음보다 발판을 몇 칸 넓혔는지를 세는데, 그 기준을 계획표에 적힌 시작 폭(plan.startW)으로 잡고 있었습니다. 와이드로 넓게 시작하면 다섯 칸 중 두세 칸을 "거저 얻었다"고 커밋은 적었습니다. 기준을 실제로 깔린 폭(growBase)으로 바꿨습니다.
2. 판마다 되살아나는 '첫 10층' 효과
슬로우 부스터(블록 속도 0.7배)와 정밀 렌즈(퍼펙트 판정 넓힘)는 "첫 10층" 동안만 효과가 있습니다. 코드는 이것을 game.level < 10으로 판단했습니다. 지금 몇 층인지가 10보다 작으면 효과가 있다는 뜻입니다.
그런데 스테이지 모드는 판이 바뀔 때마다 층수(game.level)가 0으로 돌아갑니다. 그러니 판이 바뀔 때마다 "첫 10층"이 새로 시작되어, 한 번 쓴 부스터가 판마다 되살아났습니다. 무한 모드에서는 맞던 판단이, 판이 여러 개인 모드에서는 다른 뜻이 된 것입니다.
// 고치기 전
game.boost.slow && game.level < 10
// 고친 뒤 — 판을 넘어 이어지는 '이번 게임에서 놓은 블록 수'로 센다
const earlyRun = () => game.runBlocks < 10;
game.boost.slow && earlyRun()
"지금 층수"와 "이번 게임에서 놓은 블록 수"는 무한 모드에서는 같은 값이라, 둘을 구분할 필요가 없어 보였습니다. 스테이지 모드가 생기면서 둘이 갈라졌는데, 코드는 여전히 하나로 쓰고 있었습니다.
3. 연속 목표가 한 번 일찍 깨졌다
스테이지에는 "퍼펙트 N연속" 같은 목표가 있습니다. 스테이지가 기록하는 연속 횟수를 game.combo + 1로 계산하고 있어서, 실제보다 하나 크게 셌습니다. 5연속 목표가 4연속에서 깨진 것입니다. + 1을 지웠습니다.
짝을 이루는 버그가 하나 더 있었습니다. 판이 바뀔 때 콤보를 끊지 않아서, 앞 판에서 이어 온 콤보로 다음 판의 연속 목표가 첫 블록에 바로 깨졌습니다. 두 버그 모두 "연속 몇 번"이라는 같은 숫자를 서로 다른 시점, 서로 다른 범위에서 세던 데서 나왔습니다.
4. 행운 +30%를 먹어 버린 반올림
마지막은 이번 PR에서 새로 붙인 행운 업그레이드에서 나왔습니다. 스택 타워는 블록을 하나 놓을 때마다 코인 1개를 줍니다. 행운이 최대면 1.3배, 그러니 블록 하나에 1.3개입니다. 코인은 정수이므로 반올림하는데, 커밋의 표현으로 "Math.round가 블록당 +1에서 행운 배수를 통째로 버렸다(1.3 → 1)."
| 블록 | 1.3씩 받을 몫 | 매번 반올림 | 소수 이월(고친 뒤) |
|---|---|---|---|
| 1 | 1.3 | 1 | 1 (남은 0.3) |
| 2 | 2.6 | 2 | 2 (남은 0.6) |
| 3 | 3.9 | 3 | 3 (남은 0.9) |
| 4 | 5.2 | 4 | 5 (남은 0.2) |
| 10 | 13.0 | 10 | 13 |
매번 1.3을 반올림하면 언제나 1입니다. 열 블록을 놓아도 10개, 업그레이드를 안 산 것과 똑같습니다. 고친 코드는 소수 부분을 버리지 않고 판 안에서 이월합니다.
// 블록당 +1이 대부분이라 그냥 반올림하면 행운 배수가 통째로 사라진다.
// 남는 소수를 판 안에서 이월해 실제 배수대로 쌓이게 한다.
const exact = n * (fever ? 2 : 1) * upMul('fortune') + game.coinFrac;
const gained = Math.floor(exact);
game.coinFrac = exact - gained;
이 버그는 업그레이드와 같은 PR 안에서 생기고 고쳐졌으므로, 운영 중인 게임에는 나가지 않았습니다. 다른 항목들과 달리 "출시 전에 잡은" 경우입니다.
나머지 다섯
- 시작 안내 동안 흐르던 시계 — 스테이지를 목록에서 고르면 1.9초짜리 시작 안내가 뜨는데, 그동안 제한 시간이 흐르고 있었습니다. "목록에서 고른 판만 손해였다." 안내 중에는 시계를 멈췄습니다.
- 밀린 채 남은 홈 배경 — 다리 판에서 나오면 카메라 위치(
cam.x)를 되돌리지 않아 홈 화면 배경이 옆으로 밀려 있었습니다. - 다리 칸이 겹치던 문제 — 발판 크기를 한곳으로 모으는 과정에서, 다리 판의 한 칸 길이는 늘리지 않도록 막았습니다. 칸이 놓이는 간격과 길이가 달라지면 칸이 겹치기 때문입니다.
- 가이드의 틀린 표 — 부스터 설명표에 정밀 렌즈와 와이드가 빠져 있었고, 업그레이드 총액을 "2만 코인"으로 잘못 적고 있었습니다.
- 이어 온 콤보 — 위 3번의 짝. 판이 바뀔 때 콤보를 끊습니다.
검증: 11건을 하나씩 다시 일으킨다
커밋은 두 가지 테스트 묶음을 새로 만들었습니다. 업그레이드의 구매·저장·상한·실제 효과를 보는 uptest, 그리고 11건 각각을 재현하는 회귀 테스트 fixtest입니다. 고친 버그마다 "그 버그가 다시 생기면 실패하는" 검사를 하나씩 둔 것입니다. 기존 상점 테스트는 렌즈 만료 기준이 층수에서 놓은 블록 수로 바뀌어 함께 고쳤고, 모두 29개 테스트 묶음이 통과했다고 적혀 있습니다.
교훈
- 같은 값은 한 곳에서 계산한다. 발판 크기를 세 곳에서 계산하면, 효과를 하나 더할 때마다 세 곳을 다 고쳐야 하고 언젠가 하나를 빼먹습니다. 버그 도감 #10에서 "지금 목표"를 한 함수로 모은 것과 같은 교훈입니다.
- 새 모드는 옛 가정을 깬다. "지금 층수 = 이번 게임에서 놓은 블록 수"는 무한 모드에서만 참이었습니다. 판이 여러 개인 모드를 더할 때는 "이 값이 판마다 초기화되는가"를 하나씩 확인해야 합니다.
- 작은 배수는 반올림에 먹힌다. 1에 1.3을 곱해 반올림하면 1입니다. 정수 단위로 조금씩 쌓이는 보상에 배수를 걸 때는 소수를 이월하거나 합계에서 한 번만 반올림합니다.
- 기능을 더할 때가 다시 읽을 때다. 이번 11건은 업그레이드를 붙이려고 관련 코드를 따라가다 나왔습니다. 고친 버그마다 회귀 테스트를 하나씩 남기면, 다음에 같은 자리를 건드려도 안심할 수 있습니다.
스택 타워의 다른 버그는 #1 멈춘 카운트다운, #8 안내선, #10 보이지 않는 목표에, 전체 과정은 스택 타워 제작기에 있습니다.