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 배열에 데이터가 들어있을때만 카드를 렌더링하도록 하였습니다.
완성된 게임 화면
게임은 아래 링크에서 체험하실 수 있습니다.
'코딩 > ReactJS' 카테고리의 다른 글
리액트, 원하는 게시글의 댓글 목록만 열기(feat. map함수) (1) | 2023.11.15 |
---|---|
리액트 토이 프로젝트 - 기억력 게임 개선하기 (0) | 2023.10.12 |
리액트 JWT 로그인 및 로그인 유지(유저 인증) 방법 (0) | 2023.09.09 |
react에서 게시글 생성 실시간 적용하기 (0) | 2023.09.08 |
리액트에서 반복문으로 게시글 역순 렌더링하기 (0) | 2023.09.07 |