본 카드 게임에서 가장 중요한건 카드가 매 게임마다 랜덤으로 섞여 있어야 한다는 점이다.
처음에는 한 쌍의 데이터로 구성된 데이터이니 한 짝씩만 목데이터로 저장해놓고 다음과 같이 작성하면 된다고 생각했다.
하지만 이런 경우, key가 꼬여버린다는 문제가 발생해 재렌더링마다 새로운 카드들이 추가되는 에러가 발생했다. 결국 모든 데이터를 가지고 있는 목데이터를 만들게 되었고, 해당 카드는 id값과 index값 그리고 카드에 보여질 과일 데이터를 가지게 되었다.
해당 카드 데이터들을 섞어주기 위해 검색을 통해 다음과 같은 인사이트를 얻었다. - 참고한 사이트
const MixedCardList = [...GAMEDATA, ...GAMEDATA].sort(
(a, b) => 0.5 - Math.random(),
)
코드를 살펴보면, sort로 분류시 0~1사이의 값을 임의로 리턴하는 Math.random()에 0.5를 빼 랜덤으로 -1 혹은 1이 나오게 만든다. 이렇게 랜덤으로 sort되기 때문에, 랜덤으로 섞인 결과값이 나오게 된다.
랜덤으로 섞인 값들이 게임 중 재렌더링 될 때는 유지되다가, 게임이 재시작하면서 재렌더링 되는 경우 재배열해줘야하므로 useMemo
를 사용해 저장된 결과값을 사용하다가 게임이 끝나는 경우 (finished가 변경되는 경우)에만 다시 재배열되도록 해주었다.
const MixedCardList = useMemo(() => MixedFruit(), [finished])
카드를 뒤집었을 때 생각보다 많은 제약사항이 있었다.
- 짝이 맞는 카드가 나오면 이 후 다른 카드들을 클릭했을 때 유지가 되어야 하며,
- 카드의 짝이 맞지 않으면, 알아서 뒤집어져야 한다.
- 짝이 맞는 카드는 다시 클릭했을 때, 뒤집어지면 안된다.
- 하나의 카드만 남은 상태에서 클릭했던 한 장을 다시 클릭했을 때, 에러가 없어야한다.
전체적인 요구사항을 구현하기 위해, 유저가 클릭한 카드의 데이터를 저장하는 배열을 만들어주었다.
// 유저가 클릭한 카드 데이터를 저장하는 state
const [clicked, setClicked] = useState(Array.from([]))
클릭된 카드의 인덱스를 받아와, 카드 클릭시 카드를 뒤집어주는 handleClick을 제작했다.
const handleClick = (idx) => {
// 선택한 카드 뒤집기
MixedCardList.some((e) => {
if (e.idx === idx) {
e.status = !e.status
return true
}
return false
})
setClicked([...clicked, idx])
handleCheck(idx)
}
const handleCheck = (idx) => {
if (clicked.length === 2) {
let a = MixedCardList.find((e) => e.idx === clicked[0]).id
let b = MixedCardList.find((e) => e.idx === clicked[1]).id
// 두 카드가 같지 않은 경우, 해당 카드만 다시 뒤집기
if (a !== b) {
MixedCardList.forEach((e) => {
if (e.idx === clicked[0] || e.idx === clicked[1]) {
e.status = false
}
})
}
// 두장이 클릭된 상태에서 이미 클릭된 카드를 클릭하는 경우, 클릭된 두장의 카드 뒤집기
if (clicked.includes(idx)) {
setClicked(Array.from([]))
} else {
setClicked(Array.from([idx]))
}
}
}
//모든 카드의 상태가 true면 게임 끝냄
useEffect(() => {
if (MixedCardList.every((e) => e.status === true)) {
setFinished(true)
}
}, [clicked])
두 장의 카드가 클릭된 뒤, 결과를 체크하고 알아서 다시 뒤집혀야하는데 다음 카드를 클릭해야 카드가 비로소 뒤집어지는 문제가 발생했다.
useEffect와 setTimeout을 사용해 카드가 완전히 뒤집어진 뒤에 카드 일치 여부를 체크하도록 했다.
useEffect(() => {
setTimeout(() => {
if (clicked.length === 2) {
let a = MixedCardList.find((e) => e.idx === clicked[0]).id
let b = MixedCardList.find((e) => e.idx === clicked[1]).id
// 두 카드가 같지 않은 경우
if (a !== b) {
MixedCardList.forEach((e) => {
if (e.idx === clicked[0] || e.idx === clicked[1]) {
e.status = false
}
})
}
setClicked(Array.from([]))
}
}, 500)
}, [clicked])
다만 이 경우, 빨리 카드를 클릭하는 경우 스택에 3개 이상의 카드가 쌓여 카드의 값을 서로 체크하는 함수에서 문제가 발생하게 된다. 이 경우를 위해 clicked의 갯수가 두개보다 많은 경우 카드의 클릭을 제한시켰다.
{MixedCardList.map((e, i) => (
<Card
data={e}
key={`${e.idx}-${e.item}`}
handleClick={clicked.length < 2 ? handleClick : null}
/>
))}
사실 이게 올바른 방법인지는 모르겠다. 카드 게임에 어떤 제한을 두느냐에 따라 다른 카드가 오픈된 상태에서 빠르게 카드를 클릭하도록 해야할 것 같은데, 지금처럼 다른 카드가 닫힐 동안 유저가 기다려야하는 경우엔 시간 제한을 두기는 어려울 듯 싶어 횟수 제한을 두는 방향으로 수정했다.
횟수 제한을 두는 경우, 게임 시작 전 유저에게 카드 앞면을 전체적으로 보여주고 시작하라는 의견이 있어서 해당 기능을 추가하기로 했다.
3,4 번을 위해, 카드의 상태가 true인 경우 click이벤트를 없애주었다.
return (
<CardFrame onClick={() => status || handleCard(idx)}>
<CardPiece checked={status}>
<CardFront />
<CardBack>{item}</CardBack>
</CardPiece>
</CardFrame>
)
https://velog.io/@dfd1123/react-create-portal-%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC
createPortal은 부모 컴포넌트의 DOM 계층 구조 바깥으로 자식을 렌더링하는 기능을 제공한다.
외부에 존재하는 DOM 노드가 React App DOM 계층 안에 존재하는 것처럼 연결을 해주는 포탈 기능을 제공하기 때문에 주로 모달 기능을 생성할 때 사용된다고 한다.
사실 지금 프로그램 상에서는 여러 페이지로 이동하거나 화면 전환이 다양한 상황이 아니기 때문에 굳이 사용할 필요는 없지만, 연습삼아 createPortal을 이용해 모달을 구현해보기로 했다.
//Modal.jsx
const ModalPortal = ({ children }) => {
const modalElement = document.querySelector(".modal")
return reactDom.createPortal(
<Modal>
<ModalBox>{children}</ModalBox>
</Modal>,
modalElement,
)
}
이렇게 코드를 작성하게 된다면 html 문서에 modal class를 갖는 노드를 직접 작성해주어야한다.
<body>
<div class="modal"></div>
<div id="root"></div>
</body>
놀랄 만큼 쉽고, 믿기지 않을 만큼 재밌네요! 고생하셨습니다 ~~!