무엇을 만들었나

브라우저에서 바로 하는 2048을 단일 HTML 파일로 만들었다. 프롬프트를 잘게 쪼개는 대신 설계안 한 장을 통째로 넣었고, 왕복 없이 22분 만에 배포까지 끝났다.

3회차 수업의 “혼자 간단한 게임 만들어 보기” 과제다. 라이브러리 없이 index.html 한 개, 593줄. 설치할 것도, 빌드할 것도 없다. 링크만 열면 시작된다.

직접 해보기https://songyee-ai.github.io/game_2048/

코드 보기github.com/songyee-ai/game_2048

입력 동작
스와이프 · 방향키 · WASD · HJKL 이동
Z · Ctrl+Z 한 수 되돌리기
R 새 게임

진행 중인 판과 최고 점수는 브라우저에 저장돼서, 탭을 닫았다 열어도 그 판이 그대로 남아 있다.


1. 만들기 전 스케치

코드를 한 줄도 쓰기 전에 설계안을 먼저 한 장 썼다. 화면, 상태, 규칙, 모바일에서 챙길 것 네 덩어리다.

상단 바·안내문·4×4 보드로 구성된 세로 한 화면 구상도와 설계안 항목들
그림 1. 설계안에 적어 둔 화면 — 첫 줄이 '세로 한 화면에 다 들어가야 한다'였다

여기서 제일 중요한 판단은 화면이 아니라 상태를 어떻게 들 것인가였다.

보드를 숫자 16칸 배열로만 들고 있으면 이동 애니메이션을 못 만듭니다. “어느 타일이 어디서 어디로 갔는지”를 알 수 없기 때문입니다. 그래서 타일에 고유 id를 부여합니다.

이 한 문단이 이 게임의 유일한 설계 포인트였다. 실제로 만들고 나서 보니 코드의 절반이 이 결정에서 갈라져 나왔다.


2. 만드는 과정 — 프롬프트 기록

먼저 정직하게 적어 두면, 내가 넣은 프롬프트는 두 번뿐이다. 시도 1에서 설계안을 통째로 넣었고, 마지막에 배포 여부를 골랐다. 그 사이 12분은 AI가 혼자 만들고 혼자 검증한 시간이다.

설계안 투입부터 배포까지 이어지는 작업 흐름 타임라인
그림 2. 22분 동안 실제로 일어난 일 — 파란 칸이 내가 한 것

시도 1 — 설계안 넣기

코드를 쓰기 전에 설계안을 한 장 써서 통째로 넣었다. 요약하면 이렇다.

  • 화면 — 세로 한 화면. 상단 바(타이틀 · 점수 · 최고), 안내문과 새 게임 버튼, 4×4 보드. 값이 커질수록 진한 색, 자릿수가 늘면 작은 폰트
  • 상태 — 타일마다 고유 id를 준다. 숫자 16칸 배열로만 들면 “어느 타일이 어디서 어디로 갔는지”를 알 수 없어 이동 애니메이션을 만들 수 없다
  • 규칙 — 한 번의 이동에서 타일은 최대 한 번만 병합(4 4 4 48 8, 16이면 버그) · 격자가 실제로 바뀌었을 때만 새 타일 · 방향 4개를 격자 회전으로 로직 하나에 몰기 · 빈 칸이 없고 네 방향 다 막히면 게임 오버
  • 모바일 — 스와이프 임계값 30px, 보드에 touch-action: none(없으면 스와이프마다 페이지가 스크롤된다), 테스트용 방향키

그리고 맨 앞에 한 줄을 덧붙였다.

수정하거나 첨가하고 싶은 아이디어가 있으면 함께 제안해줘.

결과: 폴더 생성 → git init → 원격 저장소 연결까지 먼저 하고, index.html 한 개를 만들었다. 첫 시도는 셸에서 긴 HTML을 한 번에 밀어 넣다가 파일이 깨졌다. 다시 파일 쓰기 도구로 통째로 작성해서 해결.

고친 점: 이 단계에서 내가 다시 요청한 건 없다. 대신 AI가 만든 결과를 스스로 브라우저에 띄워 검증했고, 그 과정에서 나온 수정이 두 가지였다.

그 사이 — 검증과 수정

확인한 것 결과
회전·병합 로직 — 순수 함수로 떼어내 단위 테스트 17개 전부 통과
판이 망가지지 않는가 — 랜덤 90수 두고 좌표·점수 검사 오류 0
되돌리기 · 승리 · 게임 오버 — 상황을 만들어 재현 정상
모바일 터치 — 스와이프 4방향, 임계값, 스크롤 차단 정상

버그로 의심했다가 아니었던 것 하나. “왼쪽·위만 눌렀는데 아무 일도 안 일어난다” — 그 판에서 두 방향이 실제로 아무것도 바꾸지 못하는 상태였다. 격자가 바뀔 때만 새 타일이 생긴다는 규칙이 제대로 동작한 것이었다.

진짜로 고친 것 하나. 게임 오버 화면의 글씨가 뒤의 타일과 겹쳐 읽히지 않았다. 보드 전체를 덮는 반투명 레이어 위에 글자를 얹은 구조였다. 가운데 카드 패널로 바꿨다.

시도 2 — 배포 결정

“공개 레포로 push하고 GitHub Pages를 켤까요?”라는 질문을 받고 켜기를 골랐다. 빌드까지 1분.

그 뒤 실제 https:// 주소에서 판 저장·복원을 한 번 더 확인했다. 로컬 파일로 열었을 때는 브라우저 저장소에 접근할 수 없어 그 항목만 검증이 비어 있었기 때문이다.

왕복이 없었던 이유

프롬프트를 여러 번 주고받지 않은 건 운이 좋아서가 아닌 것 같다. 설계안에 “이렇게 되면 틀린 것”을 미리 적어 둔 게 컸다 — 16이 되면 버그다, 벽에 붙은 방향으로 밀었는데 타일이 늘면 억울한 게임이 된다, touch-action이 없으면 페이지가 스크롤된다.

만들 것만 적으면 결과를 볼 때 “음, 되네”밖에 할 말이 없다. 틀린 모습을 먼저 적어 두면 그게 그대로 검사 항목이 된다.


3. 완성 화면

새 게임을 시작한 직후의 2048 화면, 타일 두 개
그림 3. 새 게임 직후 — 빈 칸 중 임의의 두 곳에 타일이 생긴다
타일이 여러 개 놓인 진행 중 화면
그림 4. 진행 중 — 값이 커질수록 색이 진해지고 숫자 폰트는 작아진다
'더 이상 움직일 수 없어요' 카드가 보드 가운데 떠 있는 게임 오버 화면
그림 5. 게임 오버 — 겹쳐서 안 읽히던 문제를 고친 뒤의 화면. '되돌리기'가 같이 있어서 억울한 마지막 한 수를 무를 수 있다
어두운 배경에 같은 배치로 렌더링된 다크모드 화면
그림 6. 다크모드 — 기기 설정을 따라간다

4. 코드에서 배운 부분

4-1. 방향 네 개를 로직 하나로

설계안 규칙 4에 적은 내용인데, 실제 코드를 보고 나서야 왜 그런지 알았다.

네 방향이 회전 횟수 k로 바뀌어 '왼쪽으로 밀기' 로직 하나로 모이는 구조도
그림 7. 방향은 네 개, 로직은 하나

핵심은 이 여섯 줄이다.

var DIRS = ['left', 'down', 'right', 'up'];   // 배열 인덱스 = 격자 시계방향 회전 횟수

// 격자를 시계방향으로 k번 돌렸을 때의 좌표 (r,c)가 원본 격자에서 어디였는지 되돌린다
function back(r, c, k) {
  for (var i = 0; i < k; i++) { var nr = N - 1 - c; c = r; r = nr; }
  return [r, c];
}

“위로 밀기”를 따로 구현하지 않는다. 격자를 세 번 돌려서 읽는 순서만 바꾸면 그건 이미 “왼쪽으로 밀기”다. 밀기 로직은 한 벌만 존재하고, 고칠 일이 생겨도 한 군데만 고치면 된다.

4-2. 합쳐진 타일은 바로 지우지 않는다

설계안에는 “사라지는 쪽 id를 제거”라고 적었는데, 그대로 하면 두 타일이 겹쳐지는 장면이 아예 안 보인다. 이미 사라진 뒤니까.

병합 직전, 이동 중, 정리 후 세 단계로 나눈 타일의 수명 다이어그램
그림 8. 사라지는 타일도 130ms 동안은 살아 있어야 한다
plan.merges.forEach(function (m) {
  var p = plan.pos.get(m.survivor.id);
  m.victim.row = p[0]; m.victim.col = p[1];   // 사라지는 타일을 살아남는 쪽 칸으로 보내고
  m.victim.dying = true;                       // 표시만 해 둔다 (아직 안 지운다)
});

// 새 타일은 이동이 거의 끝난 시점에 등장시킨다 (동시에 뜨면 어디서 왔는지 읽기 어렵다)
var born = spawn(alive, Math.round(MOVE * 0.8));

애니메이션이 끝나는 시점(130ms + 20)에 dying 타일을 실제로 지운다. “언제 지우는가”가 화면의 자연스러움을 만든다는 게 이 코드에서 배운 부분이다.

4-3. 설계안과 다르게 구현된 곳 세 군데

내가 쓴 설계안이 전부 그대로 간 건 아니다. 바뀐 데가 셋 있고, 셋 다 이유가 분명했다.

설계안 실제 구현
사라지는 쪽 id를 제거 dying으로 남겼다가 애니메이션 후 제거 즉시 지우면 합쳐지는 장면이 안 보인다
이동할 때 위치 트랜지션 Web Animations API 클래스를 토글하는 방식은 연속으로 병합될 때 같은 클래스가 유지돼 다시 재생되지 않는다
보드는 화면 폭의 90% min(92vw, 440px, 66dvh) 가로 폭만 기준으로 삼으면 화면이 낮을 때(가로 모드) 보드가 화면을 넘는다

5. 막혔던 점과 해결

막힌 지점 원인 해결 방법
긴 HTML을 셸로 넣다가 파일이 깨짐 따옴표·특수문자가 섞인 긴 텍스트는 셸이 먼저 해석해 버린다 파일 쓰기 도구로 통째로 작성
로컬 미리보기 서버를 못 띄움 python이 실제로는 Microsoft Store 안내만 뜨는 껍데기였다 file://로 직접 열어 확인, 저장 기능만 배포 후로 미룸
“왼쪽·위만 눌렀는데 반응이 없다” 버그가 아니라 두 방향이 실제로 아무것도 못 바꾸는 판이었다 규칙이 정상 동작한 것으로 확인
게임 오버 글씨가 타일과 겹쳐 안 읽힘 보드를 덮는 반투명 레이어 위에 바로 글자를 얹었다 가운데 카드 패널로 분리 (그림 5)

6. 다음에 개선할 것

  • 드래그 프리뷰 — 손가락을 미는 동안 타일이 20~30%쯤 따라 움직이고, 떼면 완주. 하나만 고른다면 이것이라고 제안받았다
  • 데일리 챌린지 — 날짜를 씨앗으로 난수를 고정해서 하루 한 판, 모두가 같은 판으로 점수 비교
  • 보드 크기 선택 (3×3 / 5×5) — 칸 수가 이미 상수라서 로직은 그대로고 UI만 붙이면 된다
  • 되돌리기를 한 수가 아니라 여러 수로
  • 코드를 내 손으로 직접 고쳐 보기 — 이번엔 설계만 하고 구현은 한 줄도 쓰지 않았다

관련 용어

PRD · Git · GitHub · CLI · MVP