아이패드에서만 게임이 1초에 한 번 움직였다
구름 점프를 만들고 데스크톱 크롬에서 충분히 시험했다. 그런데 아이패드로 열어 보니 노란 병아리가 1초에 한 번쯤만 움직였다. 같은 코드가 크롬에서는 매끈한 60fps 였다. 이 문제를 고치는 데 세 단계가 걸렸고, 두 번째 단계가 잘못된 방향이었다.
게임 화면은 HTML 의 <canvas> 요소에 2D 그리기 API 로 그린다. 크롬과 사파리는 같은 코드를 실행하지만 그리는 내부 구조가 다르다. 아이패드와 아이폰의 브라우저는 사파리가 쓰는 렌더링 엔진(WebKit)을 쓰기 때문에, 크롬에서 빠르다고 해서 아이패드에서도 빠르다는 보장은 없다.
그래서 실제 아이패드가 없을 때는 맥에 설치하는 개발 도구 Xcode 에 들어 있는 iOS 시뮬레이터로 아이패드를 흉내 낸다. 시뮬레이터 안의 사파리는 실제 기기와 같은 엔진이지만, 그리기는 맥의 그래픽 칩으로 하기 때문에 실제 기기와 속도가 다를 수 있다. 이 글의 핵심이 바로 이 차이다.
시뮬레이터의 사파리에서 구름 점프는 11~14fps 가 나왔다. 게임 로직에는 문제가 없었고 프레임 하나를 그리는 데 시간이 오래 걸렸다. 실제 아이패드에서도 병아리가 1초에 한 번쯤만 움직여서 플레이할 수 없는 수준이었다.
크롬에서만 시험했다면 몰랐을 문제라서, 먼저 ?debug=fps 주소 값으로 판 위에 초당 프레임 수를 찍게 만들었다. 숫자로 보이니 어디가 느린지 비교할 수 있었다.
배경은 판 바깥 가장자리까지 이어져 보여야 한다. 이를 위해 우리 게임 공통 도구(view.stretch())가 매 프레임 이미 그린 캔버스의 가장자리 줄을 읽어 바깥으로 늘여 그렸다. 안에서는 캔버스를 자기 자신에 drawImage 로 네 번 복사했다. 설명을 위해 줄인 예시는 이렇다.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d'); // 2D drawing context
// reads the canvas that was already drawn and draws it onto itself
ctx.drawImage(canvas, 0, 0, width, 1, 0, 0, width, edge);
크롬에서는 이 복사가 거의 비용이 들지 않았지만 사파리에서는 매우 느렸다.
일단 구름 점프에서 이 늘이기 호출을 빼고, 하늘을 화면에 보이는 범위 전체에 직접 칠하게 바꿨다. 그림은 그대로였다. 구름 점프는 이것으로 해결되었다.
그런데 같은 공통 도구를 쓰는 게임이 여럿 있었다. 양궁, 버블, 칼 던지기, 승부차기, 홈런, 계단, 나선 탑, 통나무 패기가 모두 같은 문제를 갖고 있었다. 게임마다 하나씩 고치는 대신 공통 도구를 고치기로 했다.
복사하는 양이 문제라고 생각했다. 가장자리 한 줄만 작은 캔버스에 떠서 그것을 늘이는 방식으로 공통 도구를 바꿨다. 같은 캔버스를 자기 자신에 복사하는 일은 없어졌다. 시뮬레이터에서 다시 재 보니 결과가 좋았다. 앞의 여덟 개 게임 중 일곱 개가 60fps, 통나무 패기가 52fps 였다. 구름 점프의 11~14fps 와 비교하면 완전히 해결된 것처럼 보였다.
이 변경을 배포했다.
실제 아이패드에서 다시 11fps
배포한 뒤 실제 아이패드에서 대시 런을 열어 보니 여전히 11fps 였다. 이 기기의 캔버스는 2732×1784 픽셀이었다. 시뮬레이터에서는 아무 문제가 없던 방식이 실제 기기에서는 똑같이 느렸다.
원인이 복사하는 양이 아니라 그린 캔버스를 다시 읽는다는 사실 자체라는 것을 이때 알았다. 작은 캔버스에 뜨는 방식도 결국 이미 그린 캔버스에서 픽셀을 읽어 오는 일이었다. 임시 스위치를 만들어 늘이기를 끄거나 읽는 방식을 바꿔 비교해 보니, 늘이기를 끄면 같은 기기에서 60fps 가 나왔다. 시뮬레이터는 맥의 그래픽 칩으로 그리기 때문에 이 읽기 비용이 숨어 있었다.
공통 늘이기 도구를 없애고, 게임마다 배경을 화면에 보이는 범위 끝까지 처음부터 직접 칠하게 했다. 구름이나 적도 보이는 범위까지 그린다. 그린 캔버스를 다시 읽는 코드가 아예 남지 않았다.
// simplified example: paint the sky over the whole visible area, never read the canvas back
ctx.fillStyle = sky;
ctx.fillRect(view.left, view.top, view.right - view.left, view.bottom - view.top);
열 개 게임을 이 방식으로 바꿨다. 실제 아이패드에서 대시 런이 11fps 에서 120fps 로 올라갔다. 통나무 패기, 양궁, 버블도 같은 방식으로 바꾼 뒤 120fps 가 나왔다.
그린 캔버스를 다시 읽는 코드는 사파리에서 매우 느리다. 시뮬레이터에서 빠르게 나와도 믿지 말고 실제 기기로 확인해야 한다. 이번에는 시뮬레이터 숫자만 믿고 배포했다가 같은 문제를 한 번 더 겪었다.
다음 글에서는 이 fps 표시를 직접 만드는 방법을 설명한다. 게임 코드를 거의 건드리지 않고, 아이패드 시뮬레이터에서 숫자를 읽는 데까지 간다.
만든 게임은 아래에서 직접 해 볼 수 있다.
참고한 문서
- MDN, CanvasRenderingContext2D.drawImage: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
- MDN, Optimizing canvas: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
- Apple, 시뮬레이터에서 앱 실행하기: https://developer.apple.com/documentation/xcode/running-your-app-in-simulator-or-on-a-device
이 글과 관련된 게임
다른 글
- 배포했는데 한 시간 동안 예전 페이지가 보였다 2026년 10월 25일
- 구름이 닿지 않는 판은 왜 나오지 않을까 2026년 10월 25일
- 밟으면 사라지는 구름은 길에 놓지 않았다 2026년 10월 25일
- 바탕을 지웠는데 분홍 띠가 남았다 2026년 10월 25일
- 하루에 세 번 바꾼 배포 제외 규칙 2026년 10월 25일
