1. 개요
안녕하세요! 오늘은 React를 사용하여 클래식 타일드롭 게임을 만드는 방법을 단계별로 알아보겠습니다. 이 튜토리얼을 통해 React 기초, 상태 관리, 그리고 게임 로직 구현에 대해 배울 수 있습니다.
2. 개발 환경 설정
2.1 Node.js 설치
먼저, Node.js를 설치해야 합니다. Node.js 공식 웹사이트에서 LTS 버전을 다운로드하고 설치하세요.
2.2 새 React 프로젝트 생성
터미널을 열고 다음 명령어를 실행하여 새 React 프로젝트를 생성합니다:
# 새 React 프로젝트 생성
npx create-react-app tetris-game
# 프로젝트 폴더로 이동
cd tetris-game
2.3 Tailwind CSS 설치 및 설정
Tailwind CSS를 설치하고 설정합니다:
# Tailwind CSS 및 관련 패키지 설치
npm install -D tailwindcss postcss autoprefixer
# Tailwind 설정 파일 생성
npx tailwindcss init -p
tailwind.config.js 파일을 열고 다음과 같이 수정합니다:
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
src/index.css 파일을 열고 다음 내용을 추가합니다:
@tailwind base;
@tailwind components;
@tailwind utilities;
3. 타일드롭 게임 컴포넌트 생성
src 폴더 안에 TetrisGame.js 파일을 생성하고 다음 코드를 작성합니다:
import React, { useState, useEffect, useCallback } from 'react';
// 게임 보드의 크기 설정
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
// 테트로미노 모양과 색상 정의
const TETROMINOS = [
{ shape: [[1, 1, 1, 1]], color: 'cyan' },
{ shape: [[1, 1], [1, 1]], color: 'yellow' },
{ shape: [[1, 1, 1], [0, 1, 0]], color: 'purple' },
{ shape: [[1, 1, 1], [1, 0, 0]], color: 'orange' },
{ shape: [[1, 1, 1], [0, 0, 1]], color: 'blue' },
{ shape: [[1, 1, 0], [0, 1, 1]], color: 'red' },
{ shape: [[0, 1, 1], [1, 1, 0]], color: 'green' },
];
// 빈 게임 보드 생성 함수
const createEmptyBoard = () =>
Array.from({ length: BOARD_HEIGHT }, () => Array(BOARD_WIDTH).fill(0));
// 타일드롭 게임 컴포넌트
const TetrisGame = () => {
// 게임 상태 관리를 위한 useState 훅 사용
const [board, setBoard] = useState(createEmptyBoard());
const [currentPiece, setCurrentPiece] = useState(null);
const [gameOver, setGameOver] = useState(false);
const [score, setScore] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
// 새로운 테트로미노 생성 함수
const spawnNewPiece = useCallback(() => {
const randomPiece = TETROMINOS[Math.floor(Math.random() * TETROMINOS.length)];
const newPiece = {
shape: randomPiece.shape,
color: randomPiece.color,
x: Math.floor(BOARD_WIDTH / 2) - Math.floor(randomPiece.shape[0].length / 2),
y: 0,
};
if (checkCollision(newPiece.shape, newPiece.x, newPiece.y)) {
setGameOver(true);
setIsPlaying(false);
} else {
setCurrentPiece(newPiece);
}
}, []);
// 테트로미노 아래로 이동 함수
const moveDown = useCallback(() => {
if (!currentPiece || gameOver) return;
if (checkCollision(currentPiece.shape, currentPiece.x, currentPiece.y + 1)) {
placePiece();
return;
}
setCurrentPiece(prev => ({ ...prev, y: prev.y + 1 }));
}, [currentPiece, gameOver]);
// 테트로미노 왼쪽으로 이동 함수
const moveLeft = () => {
if (!currentPiece || gameOver) return;
if (!checkCollision(currentPiece.shape, currentPiece.x - 1, currentPiece.y)) {
setCurrentPiece(prev => ({ ...prev, x: prev.x - 1 }));
}
};
// 테트로미노 오른쪽으로 이동 함수
const moveRight = () => {
if (!currentPiece || gameOver) return;
if (!checkCollision(currentPiece.shape, currentPiece.x + 1, currentPiece.y)) {
setCurrentPiece(prev => ({ ...prev, x: prev.x + 1 }));
}
};
// 테트로미노 회전 함수
const rotate = () => {
if (!currentPiece || gameOver) return;
const rotated = currentPiece.shape[0].map((_, index) =>
currentPiece.shape.map(row => row[index]).reverse()
);
if (!checkCollision(rotated, currentPiece.x, currentPiece.y)) {
setCurrentPiece(prev => ({ ...prev, shape: rotated }));
}
};
// 충돌 검사 함수
const checkCollision = (shape, x, y) => {
for (let row = 0; row < shape.length; row++) {
for (let col = 0; col < shape[row].length; col++) {
if (shape[row][col] &&
(y + row >= BOARD_HEIGHT ||
x + col < 0 ||
x + col >= BOARD_WIDTH ||
board[y + row][x + col] !== 0)) {
return true;
}
}
}
return false;
};
// 테트로미노를 보드에 배치하는 함수
const placePiece = () => {
const newBoard = board.map(row => [...row]);
currentPiece.shape.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
newBoard[y + currentPiece.y][x + currentPiece.x] = currentPiece.color;
}
});
});
setBoard(newBoard);
clearLines(newBoard);
spawnNewPiece();
};
// 완성된 줄 제거 및 점수 계산 함수
const clearLines = (newBoard) => {
let linesCleared = 0;
for (let y = BOARD_HEIGHT - 1; y >= 0; y--) {
if (newBoard[y].every(cell => cell !== 0)) {
newBoard.splice(y, 1);
newBoard.unshift(Array(BOARD_WIDTH).fill(0));
linesCleared++;
}
}
if (linesCleared > 0) {
setScore(prev => prev + linesCleared * 100);
}
};
// 게임 루프 useEffect
useEffect(() => {
if (isPlaying && !gameOver) {
const gameLoop = setInterval(() => {
moveDown();
}, 1000);
return () => clearInterval(gameLoop);
}
}, [isPlaying, gameOver, moveDown]);
// 새 테트로미노 생성 useEffect
useEffect(() => {
if (isPlaying && !currentPiece && !gameOver) {
spawnNewPiece();
}
}, [currentPiece, gameOver, isPlaying, spawnNewPiece]);
// 키보드 이벤트 처리 useEffect
useEffect(() => {
const handleKeyPress = (e) => {
if (!isPlaying || gameOver) return;
switch (e.key) {
case 'ArrowLeft':
moveLeft();
break;
case 'ArrowRight':
moveRight();
break;
case 'ArrowDown':
moveDown();
break;
case 'ArrowUp':
rotate();
break;
default:
break;
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, [isPlaying, gameOver, moveDown]);
// 게임 시작 함수
const startGame = () => {
setBoard(createEmptyBoard());
setCurrentPiece(null);
setGameOver(false);
setScore(0);
setIsPlaying(true);
spawnNewPiece();
};
// 게임 일시정지 함수
const pauseGame = () => {
setIsPlaying(false);
};
// 게임 재개 함수
const resumeGame = () => {
if (!gameOver) {
setIsPlaying(true);
}
};
// 게임 종료 함수
const endGame = () => {
setGameOver(true);
setIsPlaying(false);
};
// 렌더링
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<h1 className="text-4xl font-bold mb-4">타일드롭</h1>
<div className="border-4 border-gray-800 bg-white">
{board.map((row, y) => (
<div key={y} className="flex">
{row.map((cell, x) => (
<div
key={`${y}-${x}`}
className="w-6 h-6 border border-gray-300"
style={{
backgroundColor:
currentPiece &&
y >= currentPiece.y &&
y < currentPiece.y + currentPiece.shape.length &&
x >= currentPiece.x &&
x < currentPiece.x + currentPiece.shape[0].length &&
currentPiece.shape[y - currentPiece.y][x - currentPiece.x]
? currentPiece.color
: cell || 'white'
}}
/>
))}
</div>
))}
</div>
<div className="mt-4 text-xl">점수: {score}</div>
<div className="mt-4 flex space-x-2">
{!isPlaying && !gameOver && (
<button
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
onClick={startGame}
>
게임 시작
</button>
)}
{isPlaying && !gameOver && (
<button
className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
onClick={pauseGame}
>
일시정지
</button>
)}
{!isPlaying && !gameOver && (
<button
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
onClick={resumeGame}
>
계속하기
</button>
)}
<button
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
onClick={endGame}
>
게임 종료
</button>
</div>
{gameOver && (
<div className="mt-4 text-2xl font-bold text-red-600">게임 오버!</div>
)}
</div>
);
};
export default TetrisGame;
4. App.js 수정
이제 만든 타일드롭 게임 컴포넌트를 앱에 추가해봅시다. src/App.js 파일을 열고 다음과 같이 수정합니다:
import React from 'react';
import TetrisGame from './TetrisGame';
function App() {
return (
<div className="App">
<TetrisGame />
</div>
);
}
export default App;
5. 게임 실행
모든 준비가 완료되었습니다! 이제 게임을 실행해봅시다. 터미널에서 다음 명령어를 실행합니다:
# 개발 서버 실행
npm start
브라우저에서 http://localhost:3000로 접속하면 타일드롭 게임을 플레이할 수 있습니다.
6. 코드 설명
이제 타일드롭 게임의 주요 부분들을 살펴보겠습니다:
- 상태 관리:
useState훅을 사용하여 게임의 여러 상태를 관리합니다. (board, currentPiece, gameOver, score, isPlaying) - 테트로미노 생성:
spawnNewPiece함수는 랜덤한 테트로미노를 생성하고, 게임 오버 조건을 확인합니다. - 테트로미노 이동:
moveDown,moveLeft,moveRight,rotate함수들이 테트로미노의 이동과 회전을 담당합니다. - 충돌 감지:
checkCollision함수는 테트로미노가 벽이나 다른 블록과 충돌하는지 확인합니다. - 라인 제거:
clearLines함수는 완성된 라인을 제거하고 점수를 업데이트합니다. - 게임 루프:
useEffect훅을 사용하여 주기적으로 테트로미노를 아래로 이동시킵니다. - 키보드 입력 처리: 또 다른
useEffect를 사용하여 키보드 이벤트를 처리합니다. - 게임 컨트롤:
startGame,pauseGame,resumeGame,endGame함수들이 게임의 상태를 제어합니다.
이 튜토리얼을 통해 React를 사용하여 클래식 타일드롭 게임을 만드는 방법을 배웠습니다. 이 과정에서 React의 주요 개념들인 컴포넌트, 상태 관리, 효과 처리 등을 실제로 적용해 보았습니다.
이 기본 버전의 타일드롭 게임을 바탕으로, 다음과 같은 추가 기능들을 구현해 볼 수 있습니다:
- 다음 테트로미노 미리보기
- 테트로미노 홀드 기능
- 레벨 시스템 (속도 증가)
- 최고 점수 기록
- 게임 디자인 개선
React와 게임 개발에 대해 더 깊이 이해하고 싶다면, 이러한 추가 기능들을 직접 구현해 보는 것을 추천합니다. 즐거운 코딩 되세요!