자바스크립트로 같은 그림 찾기 게임 만들기
이번 포스팅에서는 “같은 그림 찾기(Memory Match)” 게임을 자바스크립트와 순수 HTML, CSS만으로 구현하는 방법을 다룹니다. 단순한 클릭 이벤트 예제를 넘어, 셔플 알고리즘, 카드 매칭 상태 관리, 화면 전환까지 확장해봅니다. 어렸을 때 해봤던 사천성 게임과 비슷한 게임을 만들어봅니다. 같은 그림 찾기의 가장 기초적이고 심플한 모양을 만들고, 그 안에 기능들을 구현합니다.
목표 및 설계
- 카드 셔플 및 랜덤 배치
- 두 장의 카드 뒤집기 및 비교 로직
- 일치 여부 판단
- 전체 매칭 완료 시 결과 메시지 출력
- 카드를 클릭했을 때 회전하는 모양
4 x 3 구조로 카드 배열을 만듭니다.
카드 앞면에는 같은 문자가 보이도록 합니다.
카드 뒷면에는 1부터 6까지 숫자가 랜덤으로 2개씩 배치되도록 합니다.
사용자가 카드를 클릭하면 회전하면서 뒷면으로 돌아가도록 합니다.
카드를 2개 뒤집었을 때 2개의 카드를 비교합니다.
2개의 카드가 일치하면 그대로 있고, 일치하지 않는다면 다시 카드 뒷면으로 돌아갑니다.
모든 카드의 짝을 맞추면 게임이 끝납니다.
1. 예시 실행 화면
2. 기본 HTML 구조
HTML은 시작 화면, 메인 화면, 끝 화면 이렇게 나누어서 작성합니다.
시작 화면은 게임 제목과 시작하기 버튼으로 구성합니다.
메인 화면에는 자바스크립트를 통해서 카드를 만들고 배열할 것이기 때문에 별 내용이 없습니다.
끝 화면에는 간단한 문구와 다시하기 버튼이 위치합니다.
3. 카드 데이터와 셔플
저는 기본적으로 심플하게 만들다보니 숫자로 하였지만, 동일한 문구나 이미지를 2장씩 준비해서 만드시면 더 퀄리티 있는 게임이 될 듯합니다.
저번 포스팅에서도 셔플 함수를 만들어서 사용하였는데, 이번에도 사용할 생각입니다.
1부터 6까지의 숫자가 2개씩 들어가 있는 배열을 만들고, 그 배열을 셔플 함수를 통해서 섞어줍니다.
배열안에서 위치를 변경하는 알고리즘은 다양하게 있지만, 셔플 함수가 가장 심플하고 사용하기 좋은 것 같습니다.
전체 배열을 반대로 돌면서 랜덤한 위치의 인덱스의 배열값을 서로 바꿔주는 방식입니다.
function setNumberList() {
for(let i = 0; i < (ROW_NUM*COL_NUM) / 2; i++) {
numberList.push((i+1).toString());
numberList.push((i+1).toString());
}
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
numberList = shuffle(numberList);
4. 카드 생성과 클릭 이벤트
앞서 설계한 내용대로 카드를 동적으로 만듭니다.
4 x 3 배열로 카드를 만들고, 위에서 랜덤으로 섞은 배열값을 차례대로 입력해줍니다.
let c = 0;
for(let i=0; i<ROW_NUM; i++) {
let itemsGroup = document.createElement("div");
itemsGroup.setAttribute("class", "items-group");
for(let j=0; j<COL_NUM; j++) {
let item = document.createElement("div");
item.setAttribute("class", "item");
let frontItem = document.createElement("div");
frontItem.setAttribute("class", "front-item");
frontItem.innerHTML = "Q";
let backItem = document.createElement("div");
backItem.setAttribute("class", "back-item");
backItem.innerHTML = numberList[c];
c++;
item.append(frontItem);
item.append(backItem);
itemsGroup.append(item);
}
mainPage.append(itemsGroup);
}
5. 카드 뒤집기
사용자가 카드를 클릭하면 카드 뒷면으로 돌아가면서 해당 숫자가 보이도록 합니다.
let itemList = document.querySelectorAll(".item");
for(let i=0; i<itemList.length; i++) {
const clickHandler = function() {
if(isDisabled)
return;
itemList[i].style.transform = "rotateY(180deg)";
clickedItemList.push(itemList[i]);
if(clickedItemList.length === 2) {
itemClickFunc(itemList);
}
};
itemList[i].clickHandle = clickHandler;
itemList[i].addEventListener('click', clickHandler);
}
removeEventListener 통해서 삭제합니다. 6. 매칭 판정
사용자가 두 장의 카드를 뒤집었을 때 2개의 카드에 적힌 숫자가 일치하는지 검사를 진행합니다.
두 장의 카드가 일치하면 맞춘 횟수를 증가시킵니다.
두 장의 카드가 일치하지 않으면 다시 카드 앞면으로 돌아갑니다.
약간의 딜레이를 주기 위해서 setTImeout를 사용하였습니다.
앞서 말했던 부분도 추가하였습니다.
isDisabled = true;
let halfItemListLength = itemList.length / 2;
for(let i=0; i<itemList.length; i++) {
itemList[i].removeEventListener('click', itemList[i].clickHandle);
}
setTimeout(function() {
if(clickedItemList[0].querySelector(".back-item").innerHTML === clickedItemList[1].querySelector(".back-item").innerHTML) {
// 두 개의 번호가 같으면
clickedCount++;
} else {
// 두 개의 번호가 다르면
for(let j=0; j<clickedItemList.length; j++) {
clickedItemList[j].style.transform = "rotateY(0deg)";
}
}
for(let i=0; i<itemList.length; i++) {
itemList[i].addEventListener('click', itemList[i].clickHandle);
}
clickedItemList = [];
isDisabled = false;
}, 1000);
7. 게임 종료 감지
모든 카드가 열리면 게임 클리어 메시지를 표시합니다.
if(clickedCount === halfItemListLength) {
// 카두 두쌍에 하나로해서 전체 카드의 절반과 같으면 끝
setTimeout(function() {
// 결과 페이지로
chapter = "result";
quizNumber.innerHTML = "결과 확인";
resultTitle.innerHTML = "축하합니다.";
mainPage.classList.toggle("hidden");
resultPage.classList.toggle("hidden");
}, 1000);
}
MutationObserver나 querySelectorAll을 활용하면 DOM 상태 기반으로 간단히 종료를 감지할 수 있습니다.8. 개발 중 실수 & 디버깅 노트
이 프로젝트를 하면서 자주 생기는 문제가 바로 두 카드의 상태 관리입니다. 한 카드만 열린 상태에서 다른 카드를 클릭했을 때 이벤트 중복이 발생하지 않게 `lock` 변수를 꼭 만들고 코드를 작성해야합니다. 또, 카드 DOM을 매번 새로 생성할 때 기존 이벤트가 중복으로 쌓이지 않도록 `innerHTML`로 초기화 후 다시 생성하는 게 안전합니다.
9. 완성 후 확장 아이디어
- 레벨 모드 추가 (카드 수 증가)
- 점수 시스템 추가
- 로컬 점수 저장 (localStorage)
- 사용자 이름과 랭킹 시스템
- 이미지를 다양하게 추가하여 확장
마무리 정리
이 프로젝트는 작은 규모지만, JS 상태 관리의 기본이 모두 들어 있습니다. DOM 조작 → 이벤트 → 조건 분기 → 타이밍 함수의 순환 구조를 익히기에 딱 좋습니다. 앞으로 React나 Vue로 확장하기 전에 이런 구조를 몸에 익혀두면 프레임워크 로직을 이해하는 속도가 훨씬 빨라집니다.



