Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3주차 기본/심화/생각 과제] 춘식이 카드 뒤집기 게임 #7

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

borimong
Copy link
Contributor

@borimong borimong commented May 5, 2023

✨ 구현 기능 명세

🌈 구현사항

✅ 게임 난이도 선택V

  1. 난이도의 종류
    1. easy → 10개 :: 5쌍 맞추기
    2. normal → 14개 :: 7쌍 맞추기
    3. hard → 18개 :: 9쌍 맞추기
  2. 난이도 중간에 바꿀시, 카드 모두 뒤집어서 처음으로 돌아가기

✅ 정답 수 노출 V

  1. (현재 나의 스코어) / 전체 정답 으로 상단에 노출

✅ 카드 선택 V

  1. 2개의 카드를 선택하면 다른 카드는 선택할 수 없습니다.
  2. 해당 카드의 일치 여부를 조사
    1. 정답일 경우
      1. 정답 스코어 증가
      2. 카드 뒤집힌 채로 유지
    2. 오답일 경우
      1. 카드 다시 뒷면으로 돌리기

✅ 카드 배열 순서V

  1. 카드가 배열되는 순서는 반드시 랜덤으로 지정
  2. 난이도에 따라 지정되는 쌍도 랜덤으로 지정

🌈 심화과제

✅ 애니메이션 V

  1. 카드를 선택
    1. 뒤집어지는 기깔나는 애니메이션을 적용해주세요!!
  2. 카드 쌍을 맞춘 경우
    1. 저는 현재 스코어가 빛나는 애니메이션을 적용했습니다! 마음대루!!

✅ theme + styled-components :: 적용 V

  1. globalstyle
  2. theme
    1. 전역에서 사용할 수 있도록 적용해보세요!

✅ 게임 초기화 버튼 V

  1. 게임 도중 ‘다시하기’ 버튼을 누르면 모든 게임 설정이 처음으로 돌아갑니다.

✅ createPortal V

  1. 모든 카드 맞추기가 끝난 후 보여주는 모달을 Portal을 활용해 만들어주세요!

🌼 PR Point

  • ~ 부분 이렇게 구현했어요, 피드백 부탁해요!
  • 저는 clickedCards 와 matchedCards, cards 이렇게 세 가지의 상태를 적절히 활용해서 구현했어요!
  • 아래와 같은 코드를 통해 선택된 카드의 id 가 들어있는 clickedCards 의 길이가 최대 2가 되도록 했구요,
 //카드 클릭 시, clickedCards 배열에 카드 id 추가
  const handleCardClick = (cardId) => {
    //clickedCards 최대 길이 2로 제한
    if (clickedCards.length < 2) {
      setClickedCards((prev) => [...prev, cardId]);
    }
  };

아래와 같은 코드를 통해 클릭될 때마다 새롭게 실행되는 구간을 만들었어요! 한 가지 예외 케이스들에 대해 설명드리면, 먼저 동일한 카드를 두 번 연속 클릭한 경우는, 매칭이 되면 안 되고 하나만 클릭했다고 생각하게 해야 해요! 그래서 splice 를 통해 하나를 지워주고 하나는 그대로 남겨 주었어요!
다음으론 이 과제의 메인이라 할 수 있는..! 이미지 url 이 서로 같은 경우..! 이 경우는 이미 matched 가 된 카드는 score 를 증가 시키면 안 되니 !matchedCards.includes(card1) 를 사용해서 필터해 주었구요, 매칭된 카드는 matchedCards 에 추가해 주었어요!
match 가 되었다면 clicked 는 비워줘야 다음 click 을 받아올 수 있으니, 약 1초 후에 빈 배열로 초기화를 해 주었구요, match 가 되지 않았다면 약간의 시간이 흐른 뒤에 카드를 다시 뒤집어주어야 하니 마찬가지로 1초 후에 빈 배열로 초기화를 해 주었어요!

//클릭된 카드가 서로 같은 카드인지 체크하고, 같은 카드이면 matchedCards 배열에 저장
  useEffect(() => {
    //클릭된 카드가 2개 미만이거나 같은 카드를 연속 클릭 시 조기 리턴
    if (clickedCards.length < 2) {
      return;
    }

    const [card1, card2] = clickedCards;
    if (card1 === card2) {
      clickedCards.splice(1, 1);
      return;
    }

    if (data[card1].cardImg === data[card2].cardImg) {
      if (!matchedCards.includes(card1)) {
        changeScore(score + 1);
      }
      setMatchedCards((prev) => [...prev, card1, card2]);
      setTimeout(() => setClickedCards([]), 1000); 
    } else {
      setTimeout(() => setClickedCards([]), 1000);
    }
  }, [clickedCards]);

저는 레벨이 바뀌면 선택되는 카드쌍이 랜덤이 되면서 랜덤으로 배열되는 기능을 구현하기 위해 같은 이미지 url 을 가지는 데이터를 2개씩 만들어 주었어요. 그리고 이 둘의 순서가 바뀌지 않게, 하나로 묶어서 랜덤 배열을 해 주고,

shufflePairs(data)
        .slice(0, levels[level] * 2)

이 코드를 통해 내가 선택해야 하는 카드 * 2 개 만큼 잘라와서 사용했습니다!

//쌍을 유지하면서 순서 섞기
  function shufflePairs(arr) {
    const pairs = [];
    for (let i = 0; i < arr.length; i += 2) {
      pairs.push([arr[i], arr[i + 1]]);
    }
    for (let i = pairs.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [pairs[i], pairs[j]] = [pairs[j], pairs[i]];
    }
    const shuffled = pairs.flatMap((pair) => pair);
    return shuffled;
  }

🥺 소요 시간, 어려웠던 점

  • 10h
  • state 를 최대한 적게 사용하기 위해서, 첫 구조를 잘 잡고 코드를 짜야겠다는 생각에 구조를 열심히 고민했던 것 같아요!! 덕분에 카드 렌더링 state, 클릭 감지 state, match 판단 state 크게 이 세 가지로 구현할 수 있었습니다!

🤩구조를 짤 때 했던 생각

  1. 변경점이 무엇인가?
    카드를 클릭해서 뒤집는 게임이다. 이미지가 서로 같으면 뒤집어 놓고, 아니면 다시 원 상태로 복구시켜야 한다.
    => 클릭을 감지하는 state 와 match 된 카드를 저장하는 배열 두 개는 무조건 있어야 겠다.
    난이도에 따라서 렌더링되는 카드가 달라야 한다.
    => 난이도를 나누는 state 는 무조건 있어야 하고, 그에 따라 카드를 매번 랜덤으로 선택해야 하니 카드 렌더링도 state 로 만들어야겠다.
  2. 예상되는 어려움은 없는가?
    똑같은 카드를 2개 만들어야 한다. 이 둘은 이미지는 같지만, id 는 달라야 한다. -> 단순히 map 으로 구현해서 두 개 복제하면 안 되겠다. 그럼 아예 데이터를 만들 때, 똑같은 이미지 url 을 가졌지만 id 는 서로 다른 카드를 2장씩 만들고, 이 둘은 인접하게 놓자. 그리고 나서 랜덤하게 섞을 때는, 인접한 2개의 순서는 바뀌지 않게 섞으면 되겠다!
  3. score 에 대한 생각.
    사실 score 는 matchedCard 의 길이로 얻어낼 수는 있다. 그렇지만 다른 컴포넌트에 전달할 일도 있고, 이예 딸린 애니메이션도 있으니 state 로 구현하는 편이 편하겠다!
  • ~ 부분에서 이래서 시간을 얼만큼 썼어요..
//쌍을 유지하면서 순서 섞기
  function shufflePairs(arr) {
    const pairs = [];
    for (let i = 0; i < arr.length; i += 2) {
      pairs.push([arr[i], arr[i + 1]]);
    }
    for (let i = pairs.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [pairs[i], pairs[j]] = [pairs[j], pairs[i]];
    }
    const shuffled = pairs.flatMap((pair) => pair);
    return shuffled;
  }

이 부분 구현하는 게 생각보다 쉽지 않더라구요.. 거의 알고리즘 문제 하나 푸는 시간이 걸린 것 같습니다..하하..
또 Cards 부분에서 처음에는 Card 컴포넌트 렌더링하는 걸, 카드를 랜덤하게 배열하는 곳에다가 했는데, 이 useEffect 의 의존성 배열엔 level 만 들어있었어요..! 그러다 보니 레벨을 바꾸지 않는 한 state 를 읽어오지 못하더라구요..! 그렇다고 의존성 배열에 clickedCards 를 넣자니 카드가 클릭될 때마다 랜덤하게 배열되어버리고..! 그래서 랜덤하게 배열하는 곳에서는 card 를 렌더링하지 않고, setCards 를 이용해 cards 값만 바꿔주고, 렌더링은 return 문에서 cards 를 읽어와서 해 주었어요! 그제서야 state 를 바르게 읽어와서 문제를 해결했습니다..!


🌈 구현 결과물

춘식이를 찾아라! 게임 하러가기

@borimong borimong changed the title Week3 card matching game [3주차 기본/심화/생각 과제] 춘식이 카드 뒤집기 게임 May 5, 2023
Copy link

@kwonET kwonET left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭐랄까..!!
현수가 말한 대로 정말 계획을 깔끔하게 명세서같이 짜고 한 게 읽히는 느낌??

보면서 굉장히 깔끔한 코드라는 생각이 들었어 // 특히 카드 셔플에 고민을 많이 한 흔적이 보여서 ㅎㅎ
넘 수고했어@~@

const j = Math.floor(Math.random() * (i + 1));
[pairs[i], pairs[j]] = [pairs[j], pairs[i]];
}
const shuffled = pairs.flatMap((pair) => pair);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flat flatmap 이라는 평탄화용 메소드가 있구나 !!
배열의 메소드 세계란.. 너무 유용한 것 같은데 ~~

Comment on lines +46 to +49
//클릭된 카드가 2개 미만이거나 같은 카드를 연속 클릭 시 조기 리턴
if (clickedCards.length < 2) {
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조기리턴으로 2개 미만의 카드 선택시의 상황을 아주 깔끔하게 만들어주었구나..
비슷하게 매치된 애들 / 클릭되어 후보인 애들로 나눠서 상태를 관리한 것 같은데 전체적인 코드 가독성은
좀 더 좋은 것 같아서 많이 참고해가 ㅎㅎ

똒똒해 ... ### 👍

return (
<ModalPortal>
<ModalContainer onClick={onClose}>
<ModalContent onClick={(e) => e.stopPropagation()}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 e.stopPropagation()의 의도는 아래의 닫기버튼을 위에 전달하는 걸 방지하기 위함일까??
찾아보니 상위 엘리먼트들로의 이벤트 전파를 중단시키는 용도로 사용된다고 하는데, 의도가 궁금해 !!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금해 22...

changeScore={changeScore}
resetClicked={resetClicked}
/>
<Modal isOpen={showModal} onClose={handleClose} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체적으로 하위 -> 상위로 전달되는 핸들러는 최상단 컴포넌트에서 지정해주고,
많은 props drilling이 되는 부분은 딱 공통된 상단에 묶어놓은 게 인상깊다 !

pr point를 읽으면서도 구현계획이 명확해서 신기했는데,
혹시 이런 props를 설계할 때 이건 어떤 부분에서 많이 사용될거니까 여기에 지정하는 게 낫겠다라고 생각하는
주요 포인트가 있을까??

적재적소에 잘 불러서 깔끔하게 짠 것 같아서 넘 잘 읽었어 🥹🔥

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최상단에서 필요한 state와 변수, 함수들을 딱 정리해서 필요한 컴포넌트에 내려줬다는게 한눈에 보인다! 너무 깔끔해..!!!!

Copy link
Member

@simeunseo simeunseo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와우 현수 진짜 똑똑혀... 전반적으로 보면서 느낀건 함수명 변수명이 굉장히 직관적이고 깔끔하다! 근데 이게 구조자체를 명확하게 설계하다보니 함수랑 변수까지도 기능별로 명확하게 정의될 수 있었던 것 같아~~ 나도 이렇게 생각이 묻어나는 코드를 짜야겠다는 반성!
넘 많이 배워간다 ㅎㅎㅎ 진짜 멋짐!!!!!! 고생했어!!! 🤩

Comment on lines +52 to +55
if (card1 === card2) {
clickedCards.splice(1, 1);
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

연속클릭되는 상황은 생각을 못했는데 꼼꼼한 예외처리 진짜 멋지다!! 그냥 냅두고 넘어갈수도 있었을텐데!!! 👍 나도 리팩토링해보께

changeScore={changeScore}
resetClicked={resetClicked}
/>
<Modal isOpen={showModal} onClose={handleClose} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최상단에서 필요한 state와 변수, 함수들을 딱 정리해서 필요한 컴포넌트에 내려줬다는게 한눈에 보인다! 너무 깔끔해..!!!!

<HeaderContainer score={score}>
<h2>춘식이를 맞춰주세요<button onClick={reset}>Reset</button></h2>
<div className="score">
<span style={{ transform: `scale(${scale})` }}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style 값을 state로 관리하다니 이거 정말 창의적인데..? 현수 코드 볼 때마다 항상 내가 생각지도 못한 방식으로 구현해내서 넘 신기해.... 😆

Comment on lines +11 to +14
const [score, setScore] = useState(0);
const [level, setLevel] = useState("EASY");
const [resetClicked, setResetClicked] = useState(false);
const [showModal, setShowModal] = useState(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니 상태관리 진짜 깔끔하다. 나는 현수랑 다르게 카드와 관련된 상태를 모두 최상단에서 만들어서 내려줬거든. 그렇게 했던 이유가 level이 바뀔 때마다 카드도 바꿔줘야하니까 그랬던건데 밑에 보니까 현수는 useState로 level이 바뀌면 카드 리스트를 새로 만들어내도록 했구나... useState를 어떻게 써야하는지 잘 몰라서 복잡하게 구현했던 것 같아 ㅋㅋㅋ 이렇게 깔끔하게 할 수 있군!!

return;
}

if (data[card1].cardImg === data[card2].cardImg) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모든 card 각각을 구분하는 것과 card img의 일치 여부를 분리하는게 되게 복잡했는데, 이런식으로 한건 또 처음본다!! 나는 카드가 섞이면서 계속 번호가 바뀌니까 이렇게 할 생각은 못했는데.... 굉장히 합리적인 방법인 것 같아 오래 고민한게 느껴지는 깔끔한 로직...👍

return (
<ModalPortal>
<ModalContainer onClick={onClose}>
<ModalContent onClick={(e) => e.stopPropagation()}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금해 22...

Comment on lines +104 to +109
background-image: url(${(props) =>
props.matchedCards
? props.cardImg
: props.clickedCards
? props.cardImg
: backCardImg});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style에서 props나 비교연산자를 쓸 생각은 못했는데 똑똑한 방법 배워간다..!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants