2048 게임은 ↑ ↓ → ← 방향키를 이용해 타일들을 이동시켜 같은 숫자끼리 합하고 최종적으로 2048을 만드는 게임이다.
먼저 사용자의 키보드 입력을 받기 위해 window 객체에 keydown
이벤트 리스너를 등록해야한다.
useEffect
훅 안에서 addEventListener()
를 사용하여 컴포넌트가 마운트되었을 때 이벤트 리스너를 등록하고, 컴포넌트가 언마운트될 때 실행되는 리턴 문에서 removeEventLister()
를 사용해 컴포넌트가 언마운트될 때 이벤트 리스너를 제거한다.
이렇게 하면 컴포넌트의 라이프사이클과 이벤트 리스너를 동기화시킬 수 있다.
export default function Grid() {
const [tileList, setTileList] = useState(getInitialGrid);
useEffect(() => {
window.addEventListener("keydown", handleKeyboard); // 마운트시 실행
return () => { // 언마운트시 실행
window.removeEventListener("keydown", handleKeyboard);
};
});
function handleKeyboard(e) {
console.log(e.key); // 사용자가 입력한 키가 출력된다.
}
// ...생략
}
window.addEventListener
와 window.removeEventListener
는 이벤트 리스너를 전역적으로 추가하고 제거하는 메소드이다.
따라서 컴포넌트가 언마운트된 후 이벤트 리스너를 제거해주지 않으면 컴포넌트 언마운트된 이후에도 계속해서 이벤트가 발생할 수 있으며, 아래와 같은 문제를 발생할 수 있다.
따라서 이벤트 리스너를 추가한 경우에는 컴포넌트 라이프사이클에서 이벤트 리스너를 제거해주는 것이 중요하다.
🔌
useEffect
훅의 반환 함수를 사용해 정리(clean-up)하는 작업을 해주자.
먼저 handleKeyboard()
함수에서 이벤트 객체의 key
속성을 이용해 위, 아래, 좌, 우 방향키 각각의 경우에 함수를 실행하도록 해주었다.
function handleKeyboard(e) {
switch (e.key) {
case 'ArrowUp':
moveUp();
break;
case 'ArrowDown':
moveDown();
break;
case 'ArrowLeft':
moveLeft();
break;
case 'ArrowRight':
moveRight();
break;
default:
break;
}
}
function moveUp() {
slideTile(0, -1);
}
function moveDown() {
slideTile(0, 1);
}
function moveLeft() {
slideTile(-1, 0);
}
function moveRight() {
slideTile(1, 0);
}
move_()
함수는 slideTile()
함수에 사용자가 어느 방향으로 타일을 이동시키고 싶어하는지를 x
와 y
로 방향값을 전달한다.
자 이제 시작이다..🙂
먼저 인자로 받은 x
와 y
를 이용해서 아래 변수들을 선언해준다.
isVerticalMove
: 수직으로 이동했는지(위, 아래) / 수평으로 이동했는지(좌,우) 나타내는 boolean 값isMinus
: -1 쪽으로 이동했는지(위, 왼쪽) / 1 쪽으로 이동했는지(아래, 오른쪽)를 나타내는 boolean 값const isVerticalMove = y !== 0;
const isMinus = x < 0 || y < 0;
이제 isVerticalMove
와 isMinus
값을 통해 방향키가 위, 아래, 좌, 우 중 어떤 것인지 구별할 수 있게 되었다.
isVerticalMove | isMinus | |
---|---|---|
위 | true | true |
아래 | true | false |
좌 | false | true |
우 | false | false |
이제 sort()
메소드를 이용해 tileList
를 재정렬해보자.
tileList
는 아래와 같은 데이터다.
tileList = [
{x: 1, y: 2, value:2, id: 1},
{x: 3, y: 1, value: 2, id: 2}
]
먼저 1차 정렬 기준은 이동 방향이 수직이냐 수평이냐이다.
이동 방향이 수직이라면 x
를 기준으로 정렬하고, 수평이라면 y
를 기준으로 정렬한다.
const sortedTileList = [...tileList].sort((tile1, tile2) => {
const result = isVerticalMove ? tile1.x - tile2.x : tile1.y - tile2.y;
return result;
});
하지만, 만약 두 타일이 같은 열이나 행에 있을 경우에는 위의 정렬 공식에서 0이 반환된다.
이 경우에는 2차 정렬을 해줘야 한다.
수직 이동의 경우, 1차 정렬 때 x
값을 사용했기 때문에 2차 정렬에서는 y
값을 사용해주면 되고,
수평 이동의 경우, 1차 정렬 때 y
값을 사용했기 때문에 2차 정렬에서는 x
값을 사용해 정렬해주면 된다.
음의 방향(위/왼쪽)일 경우 오름차순으로 정렬해주고, 양의 방향(아래/오른쪽)일 경우 내림차순으로 정렬해준다.
const sortedTileList = [...tileList].sort((tile1, tile2) => {
const result = isVerticalMove ? tile1.x - tile2.x : tile1.y - tile2.y;
if (result !== 0) { // 1차 정렬때 정렬이 됐으면 그냥 반환
return result;
} else { // 정렬이 안됐으면 2차 정렬한다.
if (isVerticalMove) {
return isMinus ? tile1.y - tile2.y : tile2.y - tile1.y; // 위로 이동이라면 오름차순, 아래로 이동이라면 내림차순
} else {
return isMinus ? tile1.x - tile2.x : tile2.x - tile2.x; // 왼쪽 이동이라면 오름차순, 오른쪽 이동이라면 내림차순
}
}
});
이제 이렇게 정렬된 배열 sortedTileList
을 순회하며, i
값을 이용해 타일의 x
와 y
를 변경해줄 것이다.
먼저 초기 인덱스를 나타내는 initialPostion
이라는 변수에 isMinus
값에 따라 0 또는 3을 할당해준다.
그리고 position
변수를 초기화해준다.
이후 position
의 값을 1씩 늘려주면서 position
값을 타일의 x
나 y
좌표에 할당해줄 것이다.
let initialPosition = isMinus ? 0 : 3;
// isMinus가 true라면 ? 맨 위 혹은 맨 왼쪽으로 밀어야 하므로 0 : 맨 아래 혹은 맨 오른쪽으로 밀어야 하므로 3
let position = initialPosition;
정렬된 배열 sortedTileList
에 반복문을 순회하면서
만약 수직 이동이었다면 배열 안의 모든 타일들의 y
값을 조정해줄 것이고, 수평 이동이었다면 x
값을 조정해줄 것이다.
그리고 위/왼쪽 이동이었다면(isMinus
가 true
) position
값이 0에서 시작하여 1씩 증가시켜주고,
아래/오른쪽 이동이었다면(isMinus
가 false
) position
값이 3에서 시작하여 1씩 감소시켜줄 것이다.
for (let i = 0; i < sortedTileList.length; i++) {
if (isVerticalMove) { // 수직 이동
sortedTileList[i].y = position; // y값 조정
position = isMinus ? position + 1 : position - 1; // isMinus값에 따라 position 증감
} else { // 수평 이동
sortedTileList[i].x = position; // x값 조정
position = isMinus ? position + 1 : position - 1;
}
}
setTileList(sortedTileList);
여기에 조건을 하나 더 추가해야 한다.
타일리스트는 단순히 1차원 배열로 이루어져있기 때문에, 어디에서 행이나 열이 바뀌는지 알 수 없다.
따라서 현재 타일의 x
값과 다음 타일의 x
을 비교해서 값이 다르다면 행이 바뀐 것이므로 position
값을 다시 초기값으로 초기화시켜줘야 한다.
for (let i = 0; i < sortedTileList.length; i++) {
if (isVerticalMove) {
sortedTileList[i].y = position;
position = isMinus ? position + 1 : position - 1;
// 만약 현재 타일의 x좌표가 다음 타일의 x좌표와 다르다면 (열이 변경되었다면)
if (sortedTileList[i].x !== sortedTileList[i + 1]?.x) {
position = initialPosition; // position 값을 초기화한다.
}
} else {
sortedTileList[i].x = position;
position = isMinus ? position + 1 : position - 1;
// 만약 현재 타일의 y좌표가 다음 타일의 y좌표와 다르다면 (행이 변경되었다면)
if (sortedTileList[i].y !== sortedTileList[i + 1]?.y) {
position = initialPosition; // position 값을 초기화한다.
}
}
}
이때 sortedTileList[i + 1]
가 존재하지 않는 경우, 참조 에러가 발생해서 옵셔널 체이닝 연산자(?.
)를 추가해줬다.
이제 이렇게 값을 변경해준 sortedList
를 setTileList()
함수에 전달하면 된다.
프로젝트 진행이 잘되가는거같아요! 화이팅입니다!