본문 바로가기

코딩/ReactJS

리액트 토이 프로젝트 - 기억력 게임 만들기(feat: 42경산 기억력 테스트)

반응형

42서울과 42경산의 기억력 테스트를 간단하게 맛볼 수 있도록 하기 위해서

순서에 맞춰 카드를 클릭하면 승리하는 게임을 리액트로 만들어보았습니다.

 

물론 에꼴42에서 주관하는 기억력 테스트와 다른점이 많습니다.

에꼴42에서는 여러개의 카드 중 몇개의 카드만 순서대로 반짝이고,

반짝였던 카드를 순서대로 클릭하면 승리합니다.

 

하지만 제가 만든 기억력 테스트는 화면에 표시된 카드 모두에 순서가 부여되고,

부여된 순서에 맞춰 카드를 클릭하면 승리합니다.


저는 이 토이 프로젝트에서 리액트를 사용하였고,

스타일 컴포넌트로 스타일을 주었습니다.

마지막으로 useEffect와 useState 훅을 이용해 코드를 작성하였습니다.

 

시작하기에 앞서 리액트 프로젝트를 생성하고,

스타일 컴포넌트를 설치하겠습니다.

npx create-react-app 프로젝트명

우선 리액트 프로젝트를 생성합니다.

프로젝트명은 취향에 맞게 설정하면 되는데,

저는 cardgame이라는 이름의 프로젝트를 생성했습니다.

npm install styled-components

 

프로젝트를 무사히 생성했다면,

위 명령어를 입력하여 스타일 컴포넌트를 설치합니다.

참고로 리액트에서 라이브러리를 설치할 때에는 해당 프로젝트 디렉토리에서 명령어를 입력해야 합니다.

이게 무슨말이냐면 리액트 프로젝트를 생성하면 지정된 프로젝트명의 디렉토리가 생성되고,

그 디렉토리 안에 리액트와 관련된 파일들이 위치하게 됩니다.

 

예를 들어 저는 cardgame이라는 프로젝트명을 사용했으므로 /cardgame 디렉토리에 리액트와 관련된 파일들이 존재합니다.

그러니 터미널에서 현재 디렉토리가 /cardgame일때 라이브러리를 설치해야 한다는 뜻입니다.


 

이제 본격적으로 코드를 작성해보겠습니다.

 

import React, { useState, useEffect } from "react";
import styled from "styled-components";

const ClickedCards = styled.div`
  background: grey;
  width: 150px;
  height: 200px;
  border-radius: 20px;
  margin: 10px;
  text-align: center;
  font-size: 100px;
  line-height: 200px;
`;
const Grid = styled.div`
  margin: auto;
  width: ${(props) => props.width};
  display: grid;
  grid-template-columns: ${(props) => props.columns};
`;

function Card() {
  const [width, setWidth] = useState("340px");
  const [columns, setColumns] = useState("1fr 1fr");
  const [level, setLevel] = useState(1);
  const [cardsNum, setCardsNum] = useState(4);
  const [howMany, setHowMany] = useState(0);
  const [correct, setCorrect] = useState(
    Array.from({ length: cardsNum }, (v, i) => i + 1)
  );
  const [order, setOrder] = useState([]);
  const [timer, setTimer] = useState(1);
  const [time, setTime] = useState(1);
  const [intervals, setIntervals] = useState();

  const clickHandler = (a, b) => {
    if (time === 0) setOrder([...order, a]);
  };
  useEffect(() => {
    if (level < 6) {
      setTime(6 - level);
    } else {
      setTime(11 - level);
    }
  }, [intervals]);
  useEffect(() => {
    const id = setInterval(() => {
      setTime((time) => time - 1);
    }, 1000);
    if (time === 0) {
      setTimer(0);
      clearInterval(id);
    }
    return () => clearInterval(id);
  }, [time]);
  useEffect(() => {
    if (order.length === cardsNum) {
      if (order.toString() === correct.toString()) {
        if (level > 4) {
          setCardsNum(9);
          setWidth("510px");
          setColumns("1fr 1fr 1fr");
          setTimer(1);
        }
        if (level === 10) {
          console.log("finish");
        }
        console.log("done");
        setHowMany(howMany.sort(() => Math.random() - 0.5));
        setLevel(level + 1);
        setTimer(1);
        setOrder([]);
        setIntervals(Math.random());
      } else {
        console.log("fail");
        setTimer(1);
        setOrder([]);
        setIntervals(Math.random());
      }
    }
  }, [order]);
  useEffect(() => {
    setHowMany(
      Array.from({ length: cardsNum }, (v, i) => i + 1).sort(
        () => Math.random() - 0.5
      )
    );
    setCorrect(Array.from({ length: cardsNum }, (v, i) => i + 1));
  }, [cardsNum]);
  return (
    <>
      {level < 11 ? (
        <div style={{ textAlign: "center" }}>
          <div>level : {level}</div>
          <div>timer : {time}</div>
          <Grid width={width} columns={columns}>
            {howMany
              ? howMany.map((a, b) => {
                  return (
                    <ClickedCards
                      key={b}
                      onClick={() => {
                        clickHandler(a, b);
                      }}>
                      {timer === 0 ? null : a}
                    </ClickedCards>
                  );
                })
              : null}
          </Grid>
        </div>
      ) : (
        <div>Finished</div>
      )}
    </>
  );
}

export default Card;

위 코드는 전체코드이며,

코드의 각 부분을 하나씩 뜯어보겠습니다.


const ClickedCards = styled.div`
  background: grey;
  width: 150px;
  height: 200px;
  border-radius: 20px;
  margin: 10px;
  text-align: center;
  font-size: 100px;
  line-height: 200px;
`;
const Grid = styled.div`
  margin: auto;
  width: ${(props) => props.width};
  display: grid;
  grid-template-columns: ${(props) => props.columns};
`;

스타일 컴포넌트로 카드가 배치될 영역과 카드의 디자인을 구성해주었습니다.

카드의 중앙에 순서가 표시되어야 하므로 line-height와 text-align을 이용해 숫자를 중앙 정렬해주었습니다.

 

이 게임에서는 최초 4장의 카드의 순서를 맞혀야 하고,

이후 9장의 카드의 순서를 맞혀야 합니다.

이때 카드의 배치가 일정해야 하므로 grid를 사용해주었습니다.

grid 영역의 중앙배치는 margin : auto를 이용해주었으며,

display : grid를 이용해 그리드 배치를 설정해주었습니다.

카드가 4장일때와 9장일때 각각 다른 값을 가져야 하는 width와 grid-template-columns에는 props를 이용해 상황에 따라 다른 값을 가질 수 있도록 설정해주었습니다.

  const [width, setWidth] = useState("340px");
  const [columns, setColumns] = useState("1fr 1fr");

최초의 width값과 grid-template-columns 값은 useState를 이용하여 지정해주었고,

if (level > 4) {
  setCardsNum(9);
  setWidth("510px");
  setColumns("1fr 1fr 1fr");
  setTimer(1);

level이 4를 초과했을때부터 width와 columns의 값이 바뀌도록 하였습니다.


  const [width, setWidth] = useState("340px");
  const [columns, setColumns] = useState("1fr 1fr");
  const [level, setLevel] = useState(1);
  const [cardsNum, setCardsNum] = useState(4);
  const [howMany, setHowMany] = useState(0);
  const [correct, setCorrect] = useState(
    Array.from({ length: cardsNum }, (v, i) => i + 1)
  );
  const [order, setOrder] = useState([]);
  const [timer, setTimer] = useState(1);
  const [time, setTime] = useState(1);
  const [intervals, setIntervals] = useState();

다음은 제가 설정한 useState 값들입니다.


width와 columns는 앞서 설명한 바와 같이 스타일을 위한 값을 지정해주었습니다.

setLevel(level + 1);

level은 게임의 레벨을 의미하며 게임에서 승리할 때마다 1씩 증가하도록 설정해주었습니다.


cardsNum은 카드의 갯수를 의미합니다.

if (level > 4) {
  setCardsNum(9);
}

level이 4를 초과하면 카드 갯수가 9개로 변경되도록 설정해주었습니다.


howMany는 카드의 순서를 저장합니다.

  useEffect(() => {
    setHowMany(
      Array.from({ length: cardsNum }, (v, i) => i + 1).sort(
        () => Math.random() - 0.5
      )
    );
  }, [cardsNum]);

위 코드에서 useEffect를 이용해 cardsNum의 값이 변경될 때 howMany에 저장된 값이 변경되도록 설정하였습니다.

이때 cardsNum에 담긴 수만큼의 요소가 담긴 배열이 howMany에 생성됩니다.

예를 들어 cardsNum이 4라면 howMany에는 [1, 2, 3, 4]가 저장됩니다.

cardsNum이 9라면 howMany에는 [1, 2, 3, 4, 5, 6, 7, 8, 9]가 저장됩니다.

setHowMany(howMany.sort(() => Math.random() - 0.5));

이후 sort함수를 이용해 howMany에 저장된 배열의 요소를 섞어줍니다.


correct에는 정답 저장됩니다.

useEffect(() => {
    setHowMany(
      Array.from({ length: cardsNum }, (v, i) => i + 1).sort(
        () => Math.random() - 0.5
      )
    );
    setCorrect(Array.from({ length: cardsNum }, (v, i) => i + 1));
  }, [cardsNum]);

cardsNum의 값이 변경될 때 correct 배열의 요소 갯수가 지정되며,

따로 셔플을 하지 않기 때문에 [1, 2, 3, 4] 혹은 [1, 2, 3, 4, 5, 6, 7, 8, 9]가 저장됩니다.

내가 선택한 순서와 correct의 배열을 비교하여 일치하면 승리하도록 로직을 작성하였습니다.


order에는 내가 클릭한 순서가 저장됩니다.

  const clickHandler = (a, b) => {
    if (time === 0) setOrder([...order, a]);
  };

위 코드에서 order 배열에 내가 클릭한 값이 담긴 매개변수인 a의 데이터를 추가해줍니다.

if (order.toString() === correct.toString())

이후 order 배열의 값과 correct 배열의 값이 일치하는지 if문으로 확인해줍니다.


timer는 카드의 순서를 보여줄지 말지 결정하는 요소입니다.

<ClickedCards
  key={b}
  onClick={() => {
    clickHandler(a, b);
  }}>
  {timer === 0 ? null : a}
</ClickedCards>

위 코드에서 3항연산자를 이용해 timer의 값이 0이라면 null을, 0이 아니라면 순서가 저장된 변수인 a를 반환하도록 해주었습니다.

timer가 0으로 바뀌면 위와 같이 카드의 순서가 사라집니다.


time은 남은 시간을 표시합니다.

useEffect(() => {
    const id = setInterval(() => {
      setTime((time) => time - 1);
    }, 1000);
    if (time === 0) {
      setTimer(0);
      clearInterval(id);
    }
    return () => clearInterval(id);
  }, [time]);

useEffect와 setInterval을 이용해 time의 값을 1초에 1씩 줄어들도록 설정해주었습니다.

줄어든 time의 값이 0이 되면 위에서 작성한 timer 코드에 의해 카드의 순서가 사라집니다.


intervals에는 페이지 리렌더링을 위한 랜덤한 숫자가 저장됩니다.

setIntervals(Math.random());

한 게임이 끝날때마다 난수를 생성하여 intervals에 저장된 값을 변경해주었습니다.

useEffect(() => {
    if (level < 6) {
      setTime(6 - level);
    } else {
      setTime(11 - level);
    }
  }, [intervals]);

intervals에 저장된 값이 변경되면 남은 시간이 초기화됩니다.

이때 레벨에 따른 난이도 증가를 위해 남은시간에 레벨만큼의 시간을 뺀 상태로 게임을 시작하게 하였습니다.

레벨 1에서는 6-1=5초의 암기시간이 주어집니다.

레벨 5에서는 6-5=1초의 암기시간이 주어집니다.

이후 level 6이상이 되면 초기값이 11이므로,

레벨 6에서는 11-6=5초의 암기시간이 주어집니다.


  useEffect(() => {
    if (order.length === cardsNum) {
      if (order.toString() === correct.toString()) {
        if (level > 4) {
          setCardsNum(9);
          setWidth("510px");
          setColumns("1fr 1fr 1fr");
          setTimer(1);
        }
        if (level === 10) {
          console.log("finish");
        }
        console.log("done");
        setHowMany(howMany.sort(() => Math.random() - 0.5));
        setLevel(level + 1);
        setTimer(1);
        setOrder([]);
        setIntervals(Math.random());
      } else {
        console.log("fail");
        setTimer(1);
        setOrder([]);
        setIntervals(Math.random());
      }
    }
  }, [order]);

순서에 맞게 카드를 클릭했는지 확인해주고,

결과에 따라 게임을 재설정해주는 핵심 로직입니다.

 

우선 order의 값이 변할 때 마다 코드를 실행하도록 하였기 때문에

내가 순서를 모두 채워넣은 즉시 채점이 이루어집니다.

 

채점은 order 배열의 크기와 cardsNum의 크기가 동일할 때 시작되며,

order 배열의 값과 correct 배열의 값이 동일할 때는 플레이어의 승리,

동일하지 않을 때는 플레이어의 패배로 결정됩니다.

if (level > 4) {
      setCardsNum(9);
      setWidth("510px");
      setColumns("1fr 1fr 1fr");
      setTimer(1);
    }
    if (level === 10) {
      console.log("finish");
    }

우선 level에 따라 게임 환경을 정리해줍니다.

level 10이 최고치이므로 level 10일때 콘솔에 finish를 출력해주었습니다.

console.log("done");
setHowMany(howMany.sort(() => Math.random() - 0.5));
setLevel(level + 1);
setTimer(1);
setOrder([]);
setIntervals(Math.random());

모든 순서를 올바르게 맞추었을 때 콘솔에 done이 출력되며,

카드의 순서를 다시 섞고 레벨을 증가시킨 뒤 timer를 재설정하여 카드 순서를 다시 확인할 수 있도록 합니다.

마지막으로 order 배열을 비워 다시 카드 선택을 할 수 있도록 만들었으며,

intervals에 난수를 채워넣어 제한시간을 설정하는 useEffect 코드가 동작하도록 하였습니다.

console.log("fail");
setTimer(1);
setOrder([]);
setIntervals(Math.random());

실패시에도 타이머와 order 배열을 초기화하고,

intervals에 난수를 채워넣어 제한시간을 재설정합니다.

<>
  {level < 11 ? (
    <div style={{ textAlign: "center" }}>
      <div>level : {level}</div>
      <div>timer : {time}</div>
      <Grid width={width} columns={columns}>
        {howMany
          ? howMany.map((a, b) => {
              return (
                <ClickedCards
                  key={b}
                  onClick={() => {
                    clickHandler(a, b);
                  }}>
                  {timer === 0 ? null : a}
                </ClickedCards>
              );
            })
          : null}
      </Grid>
    </div>
  ) : (
    <div>Finished</div>
  )}
</>

UI 부분의 코드입니다.

삼항연산자를 이용하여 level이 11 미만일때만 카드 게임을 표시하고,

그 외에는 Finished라는 글자를 보여줍니다.

 

각각의 카드는 map 함수를 이용하여 렌더링 하였으며,

역시 삼항연산자를 활용해서 howMany 배열에 데이터가 들어있을때만 카드를 렌더링하도록 하였습니다.


완성된 게임 화면

 

게임은 아래 링크에서 체험하실 수 있습니다.

https://42cardgame.netlify.app/

 

React App

 

42cardgame.netlify.app

반응형