NextJSReact게임개발반응형디자인CloudflarePages

1366×768 구형 맥북에서도 돌아가는 어린이 웹게임 만들기 — KidsPlay 확장기

2026년 7월 25일10 분 읽기

브라우저 안에서는 잘 보였는데, 실제 맥북에서는 달랐다

KidsPlay는 아이들이 마우스로 누르고, 듣고, 색칠하면서 놀 수 있도록 만든 웹게임 사이트다.

처음에는 최신 브라우저와 큰 화면을 기준으로 기능을 하나씩 붙였다. 색칠놀이, 퍼즐, 동물 소리, 숫자놀이처럼 짧게 즐길 수 있는 게임부터 시작했고, 이후 미로, 직소퍼즐, 더하기 카드, 별팡팡, 모양 달리기 같은 게임도 추가했다.

개발 화면에서는 꽤 그럴듯했다. 하지만 실제 사용 환경인 1366×768 구형 맥북에서 실행하자 문제가 한꺼번에 보였다.

  • 게임이 한 화면에 들어오지 않아 아래로 스크롤해야 했다
  • 일부 그림과 아이콘이 깨지거나 빈 사각형으로 보였다
  • 동물 소리 버튼을 눌러도 아무 소리가 나지 않을 때가 있었다
  • 색칠 그림을 확대하면 밑그림만 커지고 이미 칠한 내용은 그대로 남았다
  • 가로 목록의 마지막 그림이 절반만 보였다
  • 게임은 동작하지만 문제 수가 적어 금방 끝났다

이때부터 목표를 바꿨다.

단순히 게임을 더 추가하는 것이 아니라, 구형 노트북에서도 한 화면으로 플레이할 수 있고 여러 단계로 오래 즐길 수 있는 구조로 다시 정리하기로 했다.

실제 기기에서 확인한 문제를 먼저 분류했다

문제를 화면, 입력, 소리, 콘텐츠 네 가지로 나눴다.

구분실제 증상개선 방향
화면1366×768에서 게임판과 버튼이 잘림화면 높이 기준의 압축 레이아웃
입력확대 후 색칠 위치가 어긋남캔버스 좌표와 표시 배율 분리
소리오래된 Chrome에서 음성이 재생되지 않음사용자 클릭 피드백과 TTS 호출 안정화
콘텐츠같은 문제가 반복되고 금방 끝남난이도별 데이터와 단계 진행 추가

특히 반응형 디자인을 화면 너비만으로 판단한 것이 가장 큰 실수였다.

1366px이라는 너비는 데스크톱으로 분류되지만 실제 높이는 768px뿐이다. 브라우저 상단 영역과 사이트 헤더를 빼면 게임이 쓸 수 있는 높이는 더 작다.

따라서 작은 화면 대응을 max-width가 아닌 max-height 중심으로 다시 설계했다.

1366×768을 위한 높이 기준 레이아웃

게임 화면은 공통 헤더, 게임 제목, 실제 게임판으로 구성되어 있다. 각 게임이 서로 다른 높이를 가지면 페이지마다 스크롤 동작도 달라진다.

그래서 공통 게임 영역의 높이를 뷰포트에서 헤더 높이를 뺀 값으로 고정했다.

@media (max-height: 820px) and (min-width: 900px) {
  .game-surface {
    height: calc(100vh - 146px);
    min-height: 0;
    overflow: hidden;
  }

  .game-toolbar {
    min-height: 72px;
  }
}

이 미디어 쿼리는 태블릿용 축소 디자인이 아니다. 너비는 충분하지만 높이가 부족한 오래된 노트북을 위한 별도의 레이아웃이다.

각 게임에서도 같은 기준을 사용했다.

  • 제목과 설명 사이의 여백 축소
  • 카드와 버튼 높이 축소
  • 게임판 내부의 불필요한 빈 공간 제거
  • 결과 버튼을 화면 아래가 아니라 게임판 안쪽에 배치
  • 한 화면에 들어와야 하는 게임은 내부 스크롤도 제거

버튼은 작게 만들되 아이가 누르기 어려울 정도로 줄이지 않는 것이 중요했다. 텍스트 크기보다 여백을 먼저 줄이고, 주요 선택 버튼의 클릭 영역은 유지했다.

깨지는 아이콘은 로컬 그림으로 교체했다

최신 운영체제에서는 이모지가 풍부하게 보이지만, 오래된 macOS와 Chrome에서는 같은 문자가 빈 사각형으로 표시될 수 있다.

퍼즐, 공룡, 그림 맞추기처럼 그림 자체가 문제의 일부인 게임에서는 이모지 지원 여부가 곧 게임 가능 여부가 된다.

그래서 핵심 게임 이미지는 브라우저 폰트에 기대지 않고 프로젝트 안의 이미지 자산을 사용하도록 정리했다. 사이트를 설치형 웹앱으로 사용할 때도 외부 이미지 요청에 덜 의존하도록 했다.

이 변경은 화려한 디자인 개선은 아니지만, 오래된 기기에서 “보이는가”를 결정하는 기본 조건이었다.

색칠놀이와 그림 그리기를 하나로 합쳤다

기존에는 색칠놀이와 그림 그리기가 별도 게임이었다. 두 게임 모두 밑그림을 고르고 색과 붓을 선택해 칠하는 방식이어서 기능이 겹쳤다.

기능을 따로 유지하니 한쪽에 추가한 붓 크기와 색상이 다른 쪽에는 없고, 화면 잘림도 두 군데에서 각각 고쳐야 했다.

결국 그림 그리기의 샘플을 색칠놀이로 옮기고 하나의 게임으로 합쳤다.

현재 색칠놀이는 다음 기능을 한 화면에서 제공한다.

  • 나비, 부엉이, 자동차, 로켓, 코끼리 등 14개 밑그림
  • 12가지 색상
  • 3단계 붓 크기
  • 지우개
  • 그림 확대와 축소
  • 자동 색칠
  • 다시 칠하기
  • 완성 그림 저장

그림 선택 목록은 가로 스크롤 방식으로 바꿨다. 목록의 양쪽에 충분한 안쪽 여백을 두어 마지막 고양이 그림도 잘리지 않게 했다.

중앙 그림판은 HTML Canvas와 투명 밑그림을 겹쳐 사용한다. 여기서 중요한 점은 확대할 때 두 레이어를 같은 컨테이너 안에서 함께 확대하는 것이다.

이전에는 밑그림 이미지의 크기만 바뀌어 이미 그린 선과 위치가 어긋났다. 지금은 논리 캔버스 크기를 고정하고 화면에 표시되는 배율만 변경한다.

포인터 좌표도 표시 크기가 아니라 논리 캔버스 크기로 환산한다.

const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * (canvas.width / rect.width);
const y = (event.clientY - rect.top) * (canvas.height / rect.height);

이렇게 하면 80%, 100%, 120%처럼 표시 배율이 달라져도 붓이 실제 포인터 아래에 그려진다.

저장할 때는 사용자가 그린 캔버스와 밑그림을 다시 합성한다. 화면에서 보이는 모습과 저장한 결과가 최대한 같도록 두 레이어를 한 이미지로 만든다.

게임을 추가하는 것보다 오래 플레이할 구조가 필요했다

게임 수만 늘리면 첫 화면은 풍성해진다. 하지만 각 게임이 한두 문제로 끝나면 아이가 다시 같은 메뉴를 선택해야 한다.

그래서 이번 확장에서는 “종류 수”와 “난이도 단계”를 함께 늘렸다.

게임구성
미로 찾기서로 다른 미로 10단계
동물 직소퍼즐동물 12종과 단계별 조각 수
더하기 카드한 번에 10문제
짝꿍 찾기초급 8장, 중급 16장, 고급 32장
숫자 선 잇기5개 완성 그림
숨은 모양 찾기10문제
알파벳초급 6자, 중급 13자, 고급 26자
숫자놀이초급 10개, 중급 30개, 고급 50개
동물 소리초급 5종, 중급 10종, 고급 15종
탈것·생활 소리초급 5종, 중급 10종, 고급 15종
공룡 나라초급 3종, 중급 6종, 고급 10종

난이도를 바꾸면 현재 문제 번호와 정답 상태도 함께 초기화한다. 화면만 바뀌고 이전 단계의 상태가 남아 있으면 정답 판정이 틀어질 수 있기 때문이다.

문제 데이터와 화면 코드를 분리해 두니 새 동물이나 알파벳을 추가할 때 버튼을 일일이 새로 만드는 대신 배열에 항목을 넣는 방식으로 확장할 수 있었다.

미로는 한 판이 아니라 10단계로 만들었다

초기 미로는 한 종류뿐이었다. 토끼가 아래 통로로 이동할 때 게임판의 높이가 바뀌는 문제도 있었다.

미로 외곽의 크기를 고정하고, 토끼 위치만 게임판 안에서 이동하도록 바꿨다. 캐릭터가 어느 줄에 있더라도 제목과 미로판의 위치는 그대로 유지된다.

플레이 방식은 단순하다.

  1. 미로 안의 이동할 위치를 누른다
  2. 벽과 충돌하지 않으면 토끼가 그 방향으로 이동한다
  3. 치즈에 도착하면 다음 미로가 열린다
  4. 단계가 올라갈수록 길이 더 복잡해진다

구형 브라우저에서도 동작하도록 복잡한 물리 엔진 대신 고정된 미로 데이터와 단순한 위치 판정을 사용했다.

퍼즐은 조각 수와 그림 종류를 함께 늘렸다

기존 퍼즐은 네 동물을 맞추면 끝나는 짧은 게임이었다. 직소퍼즐도 여우 한 종류만 반복되어 완성 목표를 예상하기 어려웠다.

퍼즐은 다섯 단계로 나누고 단계가 올라갈수록 조각 수가 늘어나게 했다. 동물 직소퍼즐에는 완성 그림 미리보기도 함께 배치했다.

아이 입장에서는 조각 모양만 보는 것보다 “어떤 동물을 완성하는 중인지” 확인할 수 있는 편이 훨씬 이해하기 쉽다.

드래그가 익숙하지 않은 아이를 위해 조각을 목표 영역 가까이에 놓으면 정확한 자리로 붙는 스냅 판정도 유지했다.

더하기 카드는 정답 위치도 섞어야 했다

더하기 카드의 초안은 숫자와 점을 함께 보여 주는 방식이었다. 하지만 정답이 항상 두 번째 선택지에 나타나는 문제가 있었다.

계산보다 버튼 위치를 외우는 게임이 된 셈이다.

정답과 오답 후보를 만든 뒤 매 문제마다 순서를 섞고, 같은 정답 패턴이 반복되지 않도록 10문제를 구성했다.

숫자를 아직 빠르게 읽지 못하는 아이도 점의 개수를 세어 답을 고를 수 있다. 난이도가 올라가면 더하는 수의 범위를 넓히는 방식으로 확장할 수 있게 했다.

별팡팡은 섞인 보드와 연결 판정이 핵심이었다

별팡팡의 첫 버전은 같은 색 정사각형이 규칙적으로 나열되어 있었다. 누르는 순간 대부분의 블록이 사라져 퍼즐이라기보다 큰 버튼에 가까웠다.

보드를 무작위 색상으로 섞고 다음 규칙을 적용했다.

  • 위, 아래, 왼쪽, 오른쪽으로 붙은 같은 색만 한 그룹으로 계산
  • 두 개 이상 연결된 그룹만 제거
  • 한 번에 많이 제거할수록 더 높은 점수 획득
  • 제거 후 위쪽 블록이 아래로 내려옴

대각선은 연결로 보지 않는다. 같은 색이 화면에 있어도 실제로 맞닿아 있지 않으면 함께 없어지지 않는다.

모양 달리기는 숫자가 아니라 장면이 움직여야 했다

처음 모양 달리기를 만들었을 때는 거리 숫자만 10m, 20m처럼 바뀌었다. 규칙은 작동했지만 캐릭터가 달린다는 느낌은 거의 없었다.

그래서 캐릭터의 상하 움직임과 배경 레이어 애니메이션을 추가했다.

  • 병아리가 위아래로 뛰는 동작
  • 바닥에 닿을 때 작게 생기는 먼지
  • 서로 다른 속도로 지나가는 구름과 나무
  • 뒤에서 앞으로 흘러오는 거리 표지판
  • 정답 모양이 살짝 떠오르는 동작

아이는 제시된 모양과 같은 버튼을 5초 안에 눌러야 한다. 맞히면 계속 달리고, 틀리거나 시간이 끝나면 현재 거리와 최고 기록을 보여 준다.

애니메이션은 JavaScript로 매 프레임 위치를 계산하지 않고 CSS keyframes를 사용했다. 구형 기기에서 불필요한 연산을 줄이면서도 장면이 계속 움직이는 느낌을 만들기 위해서다.

숫자 선 잇기는 보기 좋은 배치보다 클릭 가능한 배치가 우선이었다

숫자 선 잇기에서는 점 두 개가 너무 가까우면 숫자가 겹치고, 작은 화면에서는 원하는 점을 누르기 어려웠다.

완성 그림의 모양을 유지하면서도 인접한 점 사이에 최소 간격을 두도록 좌표를 다시 배치했다. 작게 표시되는 점도 실제 클릭 영역은 더 넓게 잡았다.

화면에는 원형 점과 숫자가 보이지만, 입력 판정은 주변의 넓은 투명 영역에서 받는 방식이다.

정해진 순서대로 모든 점을 연결하면 번호 표시는 사라지고 완성된 그림이 나타난다.

오래된 브라우저에서 음성을 안정적으로 재생하기

동물 친구, 탈것, 알파벳, 모양 찾기에서는 소리가 게임 규칙의 일부다. 소리가 나지 않으면 정답을 고를 근거가 사라진다.

Web Speech API는 브라우저마다 음성 목록을 준비하는 시점이 다르고, cancel() 직후 같은 실행 흐름에서 speak()를 호출하면 오래된 Chrome이나 Safari가 새 음성을 버리는 경우가 있었다.

그래서 이전 음성을 취소한 뒤 짧게 기다리고, 가장 최근 요청만 재생하도록 요청 번호를 확인했다.

const requestId = ++speechRequestId;
window.speechSynthesis.cancel();

window.setTimeout(() => {
  if (requestId !== speechRequestId) return;

  window.speechSynthesis.resume();
  window.speechSynthesis.speak(utterance);
}, 45);

사용자가 소리 버튼을 눌렀다는 사실을 바로 알 수 있도록 Web Audio로 짧은 클릭 피드백도 함께 재생한다.

한국어와 영어 문제에 맞는 음성을 우선 선택하고, 사용 가능한 음성이 없으면 브라우저 기본 음성을 사용한다.

동물 소리와 탈것·생활 소리는 각각 초급, 중급, 고급으로 나눴다. 난이도가 올라가면 보기와 소리 종류가 5개, 10개, 15개로 늘어난다.

부모 화면은 PIN 대신 곱셈 문제로 보호했다

아이용 사이트에서 고정 PIN은 부모가 잊기 쉽고, 화면에 입력하는 모습을 아이가 볼 수도 있다.

부모 화면 버튼을 1초 동안 누르면 간단한 곱셈 문제가 나타나도록 바꿨다. 정답을 입력해야 놀이 시간과 연령 같은 설정 화면으로 들어갈 수 있다.

잘못 열었을 때 빠져나갈 수 있도록 곱셈 모달 오른쪽 위에는 닫기 버튼도 추가했다. 보호 장치이면서도 부모가 답을 입력하도록 강제하지 않는 기본적인 사용성 개선이었다.

PWA 캐시와 배포도 함께 확인했다

KidsPlay는 Next.js, React, TypeScript, Tailwind CSS로 만들었고 정적 빌드 결과를 Cloudflare Pages로 배포한다.

게임 이미지와 코드가 바뀌었는데 서비스 워커가 이전 파일을 계속 사용하면 수정한 화면이 실제 기기에는 보이지 않을 수 있다.

이번 변경에서는 서비스 워커 캐시 버전을 올리고, 빌드와 테스트를 마친 뒤 GitHub의 main 브랜치에 반영했다.

확인한 항목은 다음과 같다.

npm run lint
npm test
npm run build:pages
git diff --check

린트, 7개 테스트, 프로덕션 빌드, Cloudflare Pages용 빌드를 모두 통과한 뒤 배포했다.

GitHub 저장소는 공개로 전환하고 README에는 한국어와 영어 설명, 게임 목록, 1366×768 대응 내용, 실행 방법을 함께 정리했다.

이번 개선에서 달라진 점

이전개선 후
작은 높이에서 세로 스크롤 필요1366×768용 한 화면 레이아웃
그림 그리기와 색칠놀이 기능 중복14개 그림의 통합 색칠놀이
확대하면 밑그림과 색칠 위치 불일치공통 배율과 논리 좌표 사용
일부 음성이 재생되지 않음요청 순서 제어와 재생 지연 적용
한두 문제로 게임 종료난이도와 단계별 문제 수 확장
거리 숫자만 바뀌는 달리기캐릭터와 배경 애니메이션 추가
고정 PIN 부모 화면1초 길게 누르기와 곱셈 문제

현재 KidsPlay에는 17가지 놀이가 들어 있다. 무조건 많은 기능을 넣기보다, 같은 게임 안에서 조금씩 어려워지는 흐름을 만드는 데 더 집중했다.

실제 기기가 가장 정확한 테스트 환경이었다

이번 작업에서 가장 크게 배운 점은 개발자 도구의 화면 크기만으로는 부족하다는 것이다.

같은 1366×768이라도 운영체제와 브라우저가 차지하는 영역이 다르고, 오래된 브라우저는 이모지, 음성, 캔버스 입력을 최신 브라우저와 다르게 처리한다.

또한 어린이용 UI는 단순히 버튼을 크게 만드는 것으로 끝나지 않았다.

  • 무엇을 눌러야 하는지 소리와 그림으로 함께 알려 주기
  • 틀려도 다시 시도할 수 있게 하기
  • 단계가 올라가도 규칙은 유지하기
  • 작은 화면에서도 중요한 선택지를 한눈에 보이게 하기
  • 마우스 입력이 정확하지 않아도 넉넉하게 판정하기

기능을 추가할수록 중복 컴포넌트를 줄이고, 문제 데이터를 화면 로직에서 분리하는 일이 더 중요해졌다.

앞으로 새 그림과 새 소리를 추가할 때도 게임 화면을 다시 만드는 대신 데이터와 난이도 구성을 확장하는 방향으로 이어갈 계획이다.

PM

backtodev

40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.