본문 바로가기
[ 프로젝트 ]/미니 프로젝트 스터디

행맨 게임 🎮

by 디디 ( DD ) 2023. 3. 7.

 

 

 

 

 

  이번 네 번째 미니 프로젝트 스터디의 주제는 리액트로 미니 게임 만들기!

뭔가 만들어보고 싶은 것은 많은데, 아무래도 게임은 처음이라 웹상에 다양한 튜토리얼이 있는 것을 골라 만들어 보기로 했다. 그러다 발견한 것이 행맨 게임. 어릴 적 영어 학원에서 많이 했던 단어 맞히기 게임이다. 

 

 

 

  아직 리액트 개발이 익숙하지 않아 컴포넌트의 구조나 관리할 상태들에 대해 미리 생각을 좀 해보고 코드를 짜기 시작했다. 먼저, 컴포넌트 구조는 App 컴포넌트 안에 게임판이 될 Hangman 컴포넌트와 자판이 될 Alphabet 컴포넌트가 자식으로 들어가 있는 간단한 구조이다. 그리고 App 컴포넌트에 아래 세 가지 상태를 만들었다.

 

const [answer, setAnswer] = useState("");  // 맞혀야 할 단어
const [guessed, setGuessed] = useState(new Set());  // 유저가 추측한 알파벳 집합
const [remainingGuesses, setRemainingGuesses] = useState(6);  // 남은 기회

 

  Set 메서드는 처음 사용해 보는 것 같은데, 중복된 값을 제거하고 싶을 때 많이 사용한다고 한다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Set

 

Set - JavaScript | MDN

Set 객체는 자료형에 관계 없이 원시 값과 객체 참조 모두 유일한 값을 저장할 수 있습니다.

developer.mozilla.org

 

 

 

  영어 단어 데이터는 무료 API를 통해 가져오려고 했지만 대부분 API가 제공하는 단어들이 인명이나 지명도 포함되어 있어 게임용으로 쓰기엔 적절하지 않은 것 같아 직접 데이터 파일을 넣어 주었다. 부산시 교육청 자료를 보고 중학 영단어 200개를 뽑아 배열 형태로 만들었는데, 이 과정에서 chatGPT를 이용했고, 적당히 자료를 뽑아 가공하는 데 꽤 쓸만해서 시간을 단축할 수 있었다. 

 

import Words from "./wordsData";

const restartGame = () => {
    const randomIndex = Math.floor(Math.random() * Words.length);
    const randomWord = Words[randomIndex];
    setAnswer(randomWord.toUpperCase()); //랜덤으로 단어 뽑기
    console.log(randomWord);
    setGuessed(new Set());
    setRemainingGuesses(6);
}; //초기화 함수

<button className="startBtn" onClick={restartGame}>Start Game</button>

 

  그리고 게임을 (재)시작하는 버튼을 위와 같이 만들었다. Math.random() 메서드를 이용해서 배열의 단어들을 랜덤으로 뽑고, 게임 화면에 대문자가 나올 수 있도록 소문자인 배열 자료를 toUpperCase() 해주었다. 

 

 

 

  아래는 Hangman 컴포넌트 파일의 코드이다.

 

import img1 from "./img/1.jpg";
import img2 from "./img/2.jpg";
import img3 from "./img/3.jpg";
import img4 from "./img/4.jpg";
import img5 from "./img/5.jpg";
import img6 from "./img/6.jpg";
import loseImg from "./img/lose.jpg";
import winImage from "./img/win.jpg";

const Hangman = (props) => {
  const answerArray = props.answer.split("");
  const letters = answerArray.map((letter, index) => (
    <span className="letter" key={index}>
      {props.guessed.has(letter) ? letter : " _ "}
    </span>
  ));

  let image;
  const isWin = !letters.some((letter) => letter.props.children === " _ ");
  if (isWin && letters.length !== 0) {
    image = winImage;
  } else if (props.remainingGuesses === 0) {
    image = loseImg;
  } else if (props.remainingGuesses === 1) {
    image = img1;
  } else if (props.remainingGuesses === 2) {
    image = img2;
  } else if (props.remainingGuesses === 3) {
    image = img3;
  } else if (props.remainingGuesses === 4) {
    image = img4;
  } else if (props.remainingGuesses === 5) {
    image = img5;
  } else {
    image = img6;
  }

  return (
    <>
      <div className="Hangman">
        <img
          src={image}
          alt={`Hangman: ${props.remainingGuesses} guesses left`}
        />
      </div>
      <div className="Word">{letters}</div>
    </>
  );
};

export default Hangman;

 

위 코드에서 answerArray를 map으로 돌릴 때, 처음엔 키를 {letter}로 했었는데, 재시작 시 기존 상태들이 제대로 초기화되지 않는 문제가 발생했다. 에러 코드를 읽어보니 고유한 값을 키로 설정해 달라고 해서 index 인자를 이용해 키를 작성해 주었고, 문제가 해결되었다.

 

 

 

그리고 각 조건문을 통해 나타나는 이미지는 아래와 같다. 

 

 

 

  다음으로 Alphabet 컴포넌트.

 

const Alphabet = (props) => {
    const handleClick = (event) => {
        const guessedLetter = event.target.value;
        props.updateGuessedLetters(guessedLetter);
    }
    
    const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const alphabetButtons = alphabet.split('').map((letter) => (
        <button className='alphabetBtn' key={letter} value={letter} onClick={handleClick}>{letter}</button>
    ))
    
    return (
        <div className="alphabet">
            {alphabetButtons}
        </div>
    )
}
    
export default Alphabet;

 

 

const updateGuessedLetters = (guessedLetter) => {
    const updatedGuessed = new Set(guessed);
    updatedGuessed.add(guessedLetter);
    setGuessed(updatedGuessed);
    if (!answer.includes(guessedLetter)) {
      setRemainingGuesses(remainingGuesses - 1);
    }
  };

 

updateGuessedLetters 함수는 부모 컴포넌트인 App.js 에서 props로 가져왔는데, 지금 보니 굳이 그러지 않고 Alphabet.js파일에 직접 함수를 작성했어도 됐을 것 같다.

 

 

 

  페이지 배포는 그동안 해왔던 것처럼 깃허브 페이지를 이용했다. 그런데 리액트 프로젝트를 빌드한 후 페이지를 만들고 삼십분이 지나도 빈 화면만 보였다. 구글링을 통해 경로 문제라는 것을 알 수 있었고 (빌드 폴더 안 index.html 파일에 적힌 경로들이 "./static/js/main" 이런 식이어야 하는데 "/static/js/main" 이렇게 .이 없으면 경로를 찾을 수 없게 된다.), package.json 파일에 "homepage": "." 를 추가해서 경로를 수정해 주었다.

 

 

그런데 이렇게 해도 다른 건 다 잘 불려오는데, 이미지 파일이 불려오질 않았다. 역시 경로 문제인 것 같아 수정을 해보려고 하다가 배포된 페이지 주소 뒤에 직접 경로를 덧붙이는 방식으로 문제를 해결했다.

(https://(깃허브 아이디).github.io/(레포지토리 이름)/index.html 이런 식으로 index.html을 붙여줌.) 

→ 다음 날 다시 확인해 보니 문제가 없었다. 깃허브 페이지 적용에 시간이 더 필요했었던 것 같다.  

 

 

 

 

 

행맨 게임 보러가기 >> https://dd-stack.github.io/mini-game-hangman/

 

 

시연 화면

 

 

● 아쉬운 점

 

  게임이다 보니 사운드도 좀 들어가고 난이도 조절을 할 수 있게 하거나 유저의 게임 기록을 저장해두는 등의 부가 기능이 있으면 더 좋을 것 같은데, 기본 기능 구현에도 어려움이 많아 이러한 추가 기능 구현을 시도해 보지 못한 게 좀 아쉬웠다.

 

 

 

● 느낀 점

 

  확실히 직접 리액트로 뭘 만들어 보니 그냥 이론만 공부하는 거랑 다른 것 같다. 이번 미니 프로젝트는 하면서 계속 막혀서 튜토리얼도 이것저것 보고 구글링도 많이 하며 다양한 자료를 찾아봤다. 부족한 부분을 그때그때 공부해서 만들어 가는 느낌이었다. 결과적으로 많은 공부가 되었고, 어느 정도 의도한 대로 게임이 완성되어 꽤 기분이 좋았다. 앞으로도 부지런히 공부해서 내가 만들고 싶은 창작 게임도 만들어낼 수 있는 실력을 쌓아야겠다.

 

 

 

 

 

댓글