개요

유명한 2048 게임을 클론하여 저만의 방식으로 만들어보았습니다.

GitHub에 올라온 해당 게임의 리포지토리 등 2048에 관련된 소스코드를 참고하지 않고 스스로 알고리즘을 구현했으며, 로그 기능과 같이 원작에는 없는 일부 기능을 추가하였습니다. 별도의 프레임워크 없이 Vanilla JS로 구현했으며, 순수 자바스크립로 직접 DOM을 다뤄보고자 초기 HTML의 구조는 싱글 페이지 애플리케이션(SPA)의 구조를 모방하였습니다.

주요 기능 및 스크린샷

메인 화면 (데스크탑)

데스크탑 화면

  • 상단에는 현재 점수최고 점수현재 턴이 표시되며, 하단에는 로그 창 토글 버튼, 리플레이 버튼, 다크 모드 토글 버튼이 있습니다.
    • 최고 점수다크 모드 상태는 localStorage에 저장합니다.
  • 키보드 방향키로 블록을 이동할 수 있습니다.
  • 화면 슬라이드로도 블록을 이동할 수 있습니다. 데스크탑과 모바일 모두 지원됩니다.
    • 데스크탑에서 마우스로 슬라이드하는 경우 MouseEvent를 사용했으며, 모바일 환경에서는 마우스 이벤트가 동작하지 않기 때문에 터치 입력을 받는 TouchEvent를 사용했습니다.

메인 화면 (모바일)

데스크탑 화면모바일 화면
▲ 안드로이드 (갤럭시 S8+)▲ iOS (아이폰 14 프로)
  • 미디어 쿼리로 반응형 디자인을 적용했으며, 모바일 환경에서도 플레이할 수 있습니다.

다크 모드

다크 모드

  • prefers-color-scheme 미디어 쿼리로 시스템 색상 여부를 판단하여 초기 실행 시의 색상 모드를 결정하도록 구현했습니다.
  • 하단의 다크 모드 토글 버튼을 눌러서 색상 모드를 전환할 수 있습니다.

로그 기능

로그 화면

▲ 로그 화면

로그 툴팁

▲ 로그 툴팁

  • 지금까지 이동한 블록의 정보(방향, 획득한 점수, 블록의 위치)를 로그 형식으로 볼 수 있습니다.
  • 각 아이템의 가장 우측에 있는 아이콘에 마우스를 올리면 해당 턴에서의 블록 위치를 확인할 수 있습니다.

구조 및 알고리즘

전체 구조 및 흐름

전체 구조는 다음과 같습니다.

앱 구조

게임의 전체적인 흐름은 다음과 같습니다.

게임의 흐름

블록(타일) 이동 알고리즘

키보드 혹은 화면 슬라이드에 따라 블록(타일)을 이동시킬 때, 이동 방향에 따라 현재 블록 정보가 담긴 2차원 배열(이하 '행렬')을 변형시킨 후, 각 행에 대해 알고리즘을 적용하고 다시 원래 모습으로 변형시켜서 사용합니다.

  • 아래 -> 위로 이동: 기존 행렬을 왼쪽으로 90도 회전한 형태, 즉 행렬의 행과 열을 스왑 후 행의 순서를 뒤집은 상태 아래에서 위로
  • 위 -> 아래로 이동: 기존 행렬을 오른쪽으로 90도 회전한 형태, 즉 행렬의 행과 열을 스왑한 상태 위에서 아래로
  • 오른쪽 -> 왼쪽으로 이동: 기존 행렬을 좌우 반전한 형태, 즉 행렬의 열 순서를 뒤집은 상태 오른쪽에서 왼쪽으로
  • 왼쪽 -> 오른쪽으로 이동: 기존 행렬 그대로의 상태 왼쪽에서 오른쪽으로

블록(타일) 병합 알고리즘

이동 방향에 맞게 행렬을 변형시켰으면, 각 행에 대해 블록을 이동하거나 병합 혹은 삭제하는 알고리즘을 적용합니다. 가장 오른쪽(마지막) 인덱스부터 '기준'을 잡고 기준 인덱스를 줄여가며 그 왼쪽에 있는 모든 값('검사 대상')을 거꾸로 순회합니다. 다음과 같이 세 가지의 경우로 나누었습니다.

  1. 검사 대상이 0이면, 다음 검사 대상으로 첫 번째 경우
  2. 검사 대상이 기준과 같은 값이면, 그 위치의 값을 0으로 설정 후, 기준의 값은 2배로 설정 & 현재 기준을 끝내고 다음 기준으로 두 번째 경우
  3. 검사 대상이 그 외의 값이면 현재 기준을 끝내고 다음 기준으로 세 번째 경우
    • 단, 기준의 값이 0이면 기준과 검사 대상의 값을 스왑한 후 다음 검사 대상으로 세 번째 경우 추가 1 세 번째 경우 추가 2

회고

간단한 게임이니만큼 빠른 시간 내에 구현할 수 있다고 나름 자신했지만, 생각보다 쉬운 일이 아니었습니다. 밑바닥부터 하나하나 만들어가면서 게임 알고리즘에 대해 탐구하고, 이를 어떻게 화면에 출력할 것인지, 그리고 어떻게 하면 효율적으로 코드를 작성할 수 있는지 고민해야 했습니다. Vue와 같은 FE 프레임워크에 익숙해진 상태로 이를 구현하려다 보니 평소보다 많은 고민을 해야 했지만, 그만큼 배운 점도 많았다고 생각합니다. 특히 맨 처음 하나의 DOM부터 시작해서 다른 DOM을 붙여가며 화면에 렌더링하는 과정을 직접 구현해보면서 SPA가 어떻게 동작하는지 이해할 수 있었고, 키보드나 터치 이벤트 핸들러를 연결했을 때, this가 올바르게 바인딩되지 않았을 경우에 어떻게 해결해야 하는지 알게 되었습니다.

다만 아쉬운 점도 적지 않았습니다. 우선 기능 구현에 집중하느라 성능을 신경 쓰지 않았다는 점이 아쉬웠습니다. 블록 알고리즘이나 로그 기능의 최적화를 고려해야 했지만 그러지 않았습니다. 그리고 전반적으로 코드가 지저분하다는 점입니다. 배포 후 지금까지 작성한 코드를 살펴봤을 때, 중복되는 코드가 곳곳에 있었고, 기능적으로 분리할 수 있음에도 불구하고 하나의 파일에 로직을 몰아넣어서 유지보수하기 어려울 것 같다는 생각이 들었습니다. 또 하나 아쉬운 점은, 바닐라 JS로 SPA의 동작 원리를 모방했다고 했지만, 정작 SPA에서 실제로 많이 사용되는 ‘라우팅(Routing)’ 시스템은 구현하지 않았다는 점입니다. 물론 게임 화면 외에는 굳이 페이지를 나눌 필요가 없지만, 이왕 SPA를 모방하는 참에 라우팅까지 구현했다면 지금처럼 후회가 남지 않았을 거라고 생각합니다. 시간적 여유가 된다면 이 세 가지 이슈를 해결하고 싶습니다.

좋았던 점도, 아쉬웠던 점도 많았지만, 지금까지 당연하게 쓰고 있었던 기술의 원리에 대해 고민해보고, 제가 잊고 있거나 모르고 있던 다양한 지식을 습득할 수 있었던 좋은 기회였습니다. 물론 여기에서 그치지 않고 프로젝트의 부족한 점을 보완하거나, 평소에 궁금했던 FE 지식에 대해 지속적으로 탐구할 계획입니다.

링크