본문 바로가기

코딩/ReactJS

리액트 토이 프로젝트 - 기억력 게임 개선하기

반응형

지난번에 리액트를 이용한 토이 프로젝트인 '기억력 게임'을 만들어보았습니다.

2023.10.04 - [코딩/ReactJS] - 리액트 토이 프로젝트 - 기억력 게임 만들기(feat: 42경산 기억력 테스트)

 

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

42서울과 42경산의 기억력 테스트를 간단하게 맛볼 수 있도록 하기 위해서 순서에 맞춰 카드를 클릭하면 승리하는 게임을 리액트로 만들어보았습니다. 물론 에꼴42에서 주관하는 기억력 테스트

bebutae.tistory.com

위 포스팅을 작성한 당시에는 시작 버튼도 없고, 카드를 선택해도 색상 변경이 되지 않는 문제가 있었습니다.

그래서 개선을 해보았습니다.


개선 후의 버전은 1.20버전이며,

시작하기 버튼이 생겼고,

선택한 카드가 표시되도록 하였습니다.


우선 시작버튼의 로직부터 알아보겠습니다.

const [start, setStart] = useState(false);

우선 start에 false를 할당해주었습니다.

      {level < 11 && start ? (
        ...
        생략
        ...
      ) : (
        <button onClick={startHandler}>시작하기</button>
      )}

삼항연산자를 이용해 start에 들어있는 값이 false라면 시작하기 버튼을 보여주고,

true라면 생략된 코드를 보여주도록 하였습니다.

(생략된 코드는 카드게임을 렌더링 하는 코드입니다.)

  const startHandler = () => {
    setStart(true);
  };

또한, 시작하기 버튼을 클릭하면 start 의 값이 true가 되도록 함수를 작성하였습니다.

  {level < 11 && start ? (
    <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}
                  bg={a.color}
                  onClick={() => {
                    clickHandler(a);
                  }}>
                  {timer === 0 ? null : a.id}
                </ClickedCards>
              );
            })
          : null}
      </Grid>
    </div>
  ) : (
    <button onClick={startHandler}>시작하기</button>
  )}

이건 아까 생략했던 부분을 포함한 코드입니다.

 

이렇게 하면 시작버튼을 눌렀을 때 카드게임이 렌더링되며 시작됩니다.


하지만 최초 시작하기 버튼 로직을 작성할 때 크리티컬한 문제가 있었습니다.

아직 카드게임이 렌더링 되지 않았고,

시작하기 버튼만 렌더링 된 상태에서도 타이머가 동작했고,

시작하기 버튼을 늦게 누르면 이미 타이머가 0이 된 상태로 카드게임이 렌더링 되기 때문에

카드의 순서를 확인할 수 없게 되었습니다.

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

결국 타이머 코드에 if문을 활용하여 start에 true가 할당되었을 경우에만 타이머가 동작하도록 수정해주었습니다.


카드를 선택했을 때, 해당 카드의 색상을 변경하기 위해서는 각 카드에 색상 값을 넣어주어야 했습니다.

  const [cardsNum, setCardsNum] = useState(4);

하지만 기존에는 위와 같이 단순히 카드의 갯수를 지정하고,

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

카드의 갯수에 따라 리스트를 생성한 뒤 셔플하고,

howMany에 저장하여 map함수로 렌더링해주었기 때문에,

각각의 카드가 가지는 값이 순서를 맞추기 위한 숫자 값 뿐이었습니다.

  const [cardsNum, setCardsNum] = useState([
    { id: 1, color: "grey" },
    { id: 2, color: "grey" },
    { id: 3, color: "grey" },
    { id: 4, color: "grey" },
  ]);

하지만 이렇게 카드 값에 순서를 맞추기 위한 id값을 부여하고,

부가적으로 color 값도 지정해주었습니다.

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

이후 cardsNum에 담긴 리스트를 셔플하고,

howMany에 저장하여 map함수로 렌더링하였습니다.

이렇게 하면 각각의 카드가 순서를 맞추기 위한 id값과,

색깔을 지정하기 위한 color 값을 동시에 가질 수 있습니다.

const ClickedCards = styled.div`
  background: ${(props) => props.bg};
  ...
  생략
  ...
`;

이번에는 styled components로 작성한 카드 스타일을 수정하여,

props 값을 이용해 카드 색상을 수정할 수 있도록 코드를 작성해주었습니다.

  const clickHandler = (a) => {
    if (time === 0) {
      setOrder([...order, a.id]);
      a.color = "green";
    }
  };

마지막으로 클릭 시 실행되는 clickHandler 함수를 수정하여,

클릭된 카드의 색상을 초록색으로 바꾸도록 코드를 작성하였습니다.


하지만 이렇게만 코드를 작성하면,

순서 맞히기를 실패하거나 성공하더라도,

바뀐 카드 색깔이 초록색으로 유지되는 오류가 발생합니다.

console.log("done");
setHowMany(howMany.sort(() => Math.random() - 0.5));
setLevel(level + 1);
setTimer(1);
setOrder([]);
setIntervals(Math.random());
cardsNum.map((a, b) => {
  a.color = "grey";
});
console.log("fail");
setTimer(1);
setOrder([]);
setIntervals(Math.random());
cardsNum.map((a, b) => {
  a.color = "grey";
});

그래서 map 함수를 이용하여 정답일 경우와 오답일 경우 모두,

각 카드의 색상을 다시 grey로 돌려놓았습니다.


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

const ClickedCards = styled.div`
  background: ${(props) => props.bg};
  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 [count, setCount] = useState(4);
  const [cardsNum, setCardsNum] = useState([
    { id: 1, color: "grey" },
    { id: 2, color: "grey" },
    { id: 3, color: "grey" },
    { id: 4, color: "grey" },
  ]);
  const [howMany, setHowMany] = useState(0);
  const [correct, setCorrect] = useState(0);
  const [order, setOrder] = useState([]);
  const [timer, setTimer] = useState(1);
  const [time, setTime] = useState(1);
  const [intervals, setIntervals] = useState();
  const [start, setStart] = useState(false);

  const startHandler = () => {
    setStart(true);
  };

  const clickHandler = (a) => {
    if (time === 0) {
      setOrder([...order, a.id]);
      a.color = "green";
    }
  };
  useEffect(() => {
    if (start) {
      if (level < 6) {
        setTime(6 - level);
      } else {
        setTime(11 - level);
      }
    }
  }, [start, intervals]);
  useEffect(() => {
    if (start) {
      const id = setInterval(() => {
        setTime((time) => time - 1);
      }, 1000);
      if (time === 0) {
        setTimer(0);
        clearInterval(id);
      }
      return () => clearInterval(id);
    }
  }, [time]);
  useEffect(() => {
    if (order.length === cardsNum.length) {
      if (order.toString() === correct.toString()) {
        if (level > 4) {
          setCardsNum([
            { id: 1, color: "grey" },
            { id: 2, color: "grey" },
            { id: 3, color: "grey" },
            { id: 4, color: "grey" },
            { id: 5, color: "grey" },
            { id: 6, color: "grey" },
            { id: 7, color: "grey" },
            { id: 8, color: "grey" },
            { id: 9, color: "grey" },
          ]);
          setWidth("510px");
          setColumns("1fr 1fr 1fr");
          setTimer(1);
          setCount(9);
        }
        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());
        cardsNum.map((a, b) => {
          a.color = "grey";
        });
      } else {
        console.log("fail");
        setTimer(1);
        setOrder([]);
        setIntervals(Math.random());
        cardsNum.map((a, b) => {
          a.color = "grey";
        });
      }
    }
  }, [order]);
  useEffect(() => {
    console.log("error?");
    console.log(cardsNum);
    setHowMany(cardsNum.sort(() => Math.random() - 0.5));

    setCorrect(Array.from({ length: count }, (v, i) => i + 1));
  }, [cardsNum]);
  return (
    <>
      {level < 11 && start ? (
        <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}
                      bg={a.color}
                      onClick={() => {
                        clickHandler(a);
                      }}>
                      {timer === 0 ? null : a.id}
                    </ClickedCards>
                  );
                })
              : null}
          </Grid>
        </div>
      ) : (
        <button onClick={startHandler}>시작하기</button>
      )}
    </>
  );
}

export default Card;

전체 코드입니다.

반응형