← 목록으로

터지기 전에 잡았다 - 자바스크립트 게임 속도 버그와 델타 타임

2026.07.10 6분 읽기
터지기 전에 잡았다 - 자바스크립트 게임 속도 버그와 델타 타임

이번 버그는 순서가 반대였다

지금까지 내 에러노트는 전부 사후 기록이었어. 퐁의 세 번의 재판도 그랬고, 버그가 먼저 터지고, 내가 발견하고, AI가 고치는 순서였지. 그런데 이번 건은 달라. 버그가 터지기도 전에 잡았거든. 그것도 코드라곤 한 줄도 못 읽는 내가 말이야.

정확히는, 내 질문이 잡았어.

"그럼 아직 안 터진 에러도 미리 알 수 있을까?"

발단은 벽돌깨기 제작기에 적었던 그 장면이야. AI 녀석이 시키지도 않았는데 퐁 때 겪은 무한 랠리 버그를 기억해뒀다가 미리 예방해놨다고 했잖아. 그게 아주 신통했어. 지난 실수를 기억해서 미리 고칠 수 있다면, 아직 저지르지 않은 실수도 미리 알 수 있는 거 아닌가? 그래서 물었지.

"너 퐁에서 배운 에러를 이번엔 스스로 고쳤잖아. 그럼 이 게임에서 앞으로 생길 수 있는 다른 에러도 미리 예상할 수 있어?"

돌아온 답이 이 표였어.

순번 예상 에러 무슨 일이 벌어지나
1 고주사율 모니터에서 게임이 빨라짐 144Hz 게이밍 모니터에서는 게임 전체가 2.4배속으로 돌아감
2 공이 벽돌을 뚫고 지나감 공이 아주 빨라지면 한 프레임 사이에 벽돌을 건너뛰어 충돌 판정을 놓침
3 패들 옆면에서 공이 순간이동 옆면 충돌을 윗면 충돌로 착각해 공 위치가 엉뚱한 곳으로 튐
4 키가 눌린 채로 굳음 게임 밖을 클릭한 채 키를 떼면 "뗐다"는 신호를 놓쳐 패들이 혼자 움직임
5 벽돌이 깨질 때 옆 벽돌 판정이 꼬임 배열을 앞에서부터 도는 중에 벽돌을 빼면 순서가 밀려 하나를 건너뜀

미래에 생길지도 모르는 일들을 이렇게 정리해주다니, 기특하다 싶어 감탄하려는 찰나에 녀석이 덧붙였어.

"근데 1번은 예상이 아니야. 지금 이 게임에 실제로 있는 문제야."

예상 문제집을 풀랬더니 기출 문제가 섞여 나온 거지.

게이밍 모니터에서는 2.4배속이 된다

설명을 듣고 나니 전혀 생각도 못했던 내용이라 재미있었어. 역시 모르면 몸이 고생하는 법이지. 우리 게임은 브라우저한테 "화면 그릴 때마다 나를 불러줘"라고 부탁하는 방식으로 움직인대. 그런데 나는 그게 1초에 60번, 그러니까 60Hz로 작동하는지도 몰랐거든. 아니, 정확히는 AI 녀석이 그렇게 짜놨었지. "한 번 불릴 때마다 공을 8칸 옮기자"라고, 1초에 60번 불린다는 가정하에 말이야.

그런데 브라우저가 부르는 횟수는 정해진 게 아니라 모니터가 화면을 그리는 횟수를 따라간다는 거야. 요즘 게이밍 모니터는 1초에 144번씩(또는 그 이상)도 돌잖아. 그러면 우리 게임도 1초에 144번 불리고, 한 번에 8칸씩 옮기니까, 곱하기를 해보면 정확히 2.4배속. 공만 빨라지는 게 아니라 3초 카운트다운도 1초 남짓 만에 끝나버려. 퐁도 똑같은 방식이라 똑같이 2.4배속이고.

그런데 내 모니터는 평범한 60Hz라서 내가 아무리 열심히 테스트해도 이 버그는 영영 안 보인다는 것. 나한테는 멀쩡한 게임이 게이밍 모니터 쓰는 손님한테는 정신없이 빨리 감기 된 게임인 거지. 손님이 "이 오락실 기계 왜 이래요?"라고 해도 주인은 영문을 모르는 상황이 될 뻔했어.

처방전은 델타 타임: 몇 번 불렸는지 말고, 시간이 얼마나 흘렀는지

고치는 방법에는 이름이 붙어 있대. 델타 타임(delta time). 요지는 "한 번 불릴 때마다 8칸"이 아니라 "실제로 흐른 시간만큼" 움직이게 바꾸는 거야.

델타 타임 다이어그램 — 60Hz와 144Hz 모니터에서 같은 코드가 다른 속도로 도는 이유와 처방

실제로 들어간 코드는 이 두 줄이 핵심이래.

// 지난번 불린 뒤로 흐른 시간이 "60fps 기준 몇 프레임분"인지 계산
const dt = Math.min((now - lastTime) / (1000 / 60), 3);

// 모든 이동에 dt를 곱한다 — 자주 불리면 그만큼 조금씩만 움직인다
ball.x += ball.vx * dt;

144Hz 모니터에서는 두 배 넘게 자주 불리는 대신 한 번에 절반도 안 되게 움직여서, 결국 1초 동안 움직인 거리는 어느 모니터에서든 같아지는 원리야. 전혀 몰랐고 어쩌면 앞으로도 몰랐을지 모를 오류를 잡아낸거지. 이렇게 질문을 어떻게 하느냐에 따라 결과물이 상당히 달라지는거야.

검증하는데 3초가 60초로 재졌다

고쳤으면 확인해야지. AI 녀석이 두 가지 방식으로 게임을 돌렸어. 60Hz 흉내와 144Hz 흉내. 게임 속 시간으로 거의 같은 시점에 클리어됐고, 점수도 양쪽 다 벽돌 50개어치로 정확히 일치했어. 퐁도 양쪽으로 세 판씩 돌려서 전부 정상 종료. 여기까지는 깔끔했지.

그런데 마지막에 해프닝이 하나 있었어. 실제 브라우저에서 3초 카운트다운을 재봤더니 60초가 걸린 거야. 아니, 배속 버그를 고쳤더니 이번엔 슬로모션이냐?

녀석이 이번엔 화면이 몇 번 불리는지 직접 세어보더니 판정을 내렸어.

"코드는 정상이야. 자동화된 테스트 탭이 뒷전으로 밀려 있어서 브라우저가 절전한다고 1초에 한 번만 불러주고 있어. 계산해보면 정확히 60초가 나와야 하는 상황이고, 실제로 60초가 나왔으니 오히려 코드가 정상이라는 증거야."

버그처럼 보이는 게 다 버그는 아니더라고. 퐁 게임때도 비슷한 일을 겪었는데, 측정하는 방법이 이상하면 멀쩡한 코드도 환자처럼 보인다는 걸 두 번째로 배웠어. 사실 아직도 전부 이해가 되는 건 아니야. 어렴풋이 개념만 알고 넘어가는거지.

오늘의 에러노트 요약

증상 정체 처방
게이밍 모니터에서 게임이 2.4배속 브라우저가 게임을 부르는 횟수는 모니터 주사율을 따라가는데, 코드는 1초에 60번을 가정함 델타 타임 — 불린 횟수가 아니라 실제로 흐른 시간만큼 움직이게 수정
3초 카운트다운이 60초 버그 아님 — 뒷전으로 밀린 테스트 탭을 브라우저가 절전시켜 1초에 한 번만 부름 시간 대신 "몇 번 불렸는지"를 직접 세어 코드 정상을 확인

터진 버그는 고치면 되는데, 안 터진 버그는 물어봐야 나오더라고. "앞으로 생길 수 있는 에러를 미리 예상해줘" — 이 질문 하나는 다음 게임에서도 완성 도장 찍기 전에 꼭 던져볼 생각이야. 이번에 새로 배운 개념들은 벽돌깨기 개념 도감에 표로 정리해뒀어.

← 이전 글 게임이 무한루프에 빠졌다 - 자바스크립트 무한루프 버그 해결기