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주차 과제]  🌼 쵸키 푸키를 찾아라 💖 #8

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

Conversation

Arooming
Copy link
Contributor

@Arooming Arooming commented May 5, 2023

🌈 기본 과제

✨ 게임 난이도 선택

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

✨ 정답 수 노출

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

✨ 카드 선택

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

✨ 카드 배열 순서

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

🌈 심화 과제

✨ 애니메이션

  • 카드를 선택

    → 뒤집어지는 기깔나는 애니메이션을 적용해주세요!!

  • 카드 쌍을 맞춘 경우

    → 저는 현재 스코어가 빛나는 애니메이션을 적용했습니다! 마음대루!!


✨ theme + styled-components :: 적용

  • globalstyle

  • theme

    → 전역에서 사용할 수 있도록 적용해보세요!


✨ 게임 초기화 버튼

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

✨ createPortal

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

🌼 PR Point

🍟 카드 랜덤으로 출력

  • 카드 리스트를 상수 배열로 만들고, Math.random() 메소드를 활용해서 랜덤으로 리스트가 섞이도록 구현했어요.
  • Math.random() - 0.5는 계산 결과가 음수나 양수 중 하나로 나오기 때문에, 이를 활용해서 카드 리스트의 요소가 무작위로 정렬될 수 있게 해줬어요.
  • 난이도 별 리스트가 호출될 때마다 카드 이미지가 랜덤으로 재 정렬되도록 mix 함수를 새로운 상수에 할당하여 export 했어요.
function mixEasyCardList() {
  const mixedEasyCardList = cardImgList
    .sort(() => Math.random() - 0.5)
    .slice(0, 5);
  return mixedEasyCardList.concat(mixedEasyCardList);
}

function mixNormalCardList() {
  const mixedNormalCardList = cardImgList
    .sort(() => Math.random() - 0.5)
    .slice(0, 7);
  return mixedNormalCardList.concat(mixedNormalCardList);
}

function mixHardCardList() {
  const mixedHardCardList = cardImgList.sort(() => Math.random() - 0.5);
  return mixedHardCardList.concat(mixedHardCardList);
}

export const easyCardList = mixEasyCardList();

export const normalCardList = mixNormalCardList();

export const hardCardList = mixHardCardList();

🍟 난이도 별 페이지 동적으로 구현

  • 처음에는 난이도 별로 하나씩 페이지를 생성했었는데, 기능을 구현하면서 중복 코드가 너무 많이 생기게 되더라구요..!

  • 난이도 별로 카드 리스트만 다르게 받을 수 있다면, 하나의 페이지를 두고 동적으로 사용할 수 있을 것 같았어요.

  • 동적으로 페이지를 구현한다면 중복 코드도 없애고 코드 유지 보수도 쉽게 할 수 있을 것 같아서 동적으로 페이지를 구현하는 쪽으로 코드를 수정했습니다.

    // Button.jsx
    const levelData = ["EASY", "NORMAL", "HARD"];
    const [isClick, setIsClick] = useState(0);
    
    <LevelBtnContainer>
            {levelData.map((data, idx) => {
              return (
                <LevelButton
                  key={idx}
                  type="button"
                  isClick={idx === isClick}
                  onClick={() => {
                    setIsClick(idx);
                  }}
                >
                  {data}
                </LevelButton>
              );
            })}
          </LevelBtnContainer>
    • levelData에 난이도 정보를 담고, 난이도 별 버튼을 생성했어요.
    • 난이도 선택 버튼을 클릭했을 때 setIsClick에 idx를 넣어 현재 렌더링 되는 데이터의 index인 idx와 클릭된 데이터의 index인 isClick의 값이 같아지도록 구현했어요.
    // Button.jsx
    const [currentPage, setCurrentPage] = useState(<CommonPage />);
    
    useEffect(() => {
        switch (isClick) {
          case 0:
            setCurrentPage(<CommonPage cardList={easyCardList} />);
            break;
    
          case 1:
            setCurrentPage(<CommonPage cardList={normalCardList} />);
            break;
    
          case 2:
            setCurrentPage(<CommonPage cardList={hardCardList} />);
            break;
    
          default:
            break;
        }
      }, [isClick, shuffle]);
    
    return (
    	{currentPage};
    )
    • isClick은 levelData의 현재 렌더링되는 데이터의 인덱스 값과 같기 때문에 0: “Easy’, 1: “Normal”, 2: “Hard” 레벨을 의미해요.
    • switch 문을 활용해서 CommonPage의 prop을 다르게 주어 난이도 별로 출력되는 카드 쌍의 개수를 조절해주었어요.

🍟 카드 쌍을 맞춘 경우 애니메이션 적용

  • animate가 true인 경우에만 className(scale)이 할당되고, 해당 className에 보여주고자 하는 애니메이션을 저장했어요.

  • 결과적으로, 카드 쌍을 맞췄을 때 animate는 true → className: scale → 보여주고자 하는 애니메이션이 실행됩니다

    const [animate, setAnimate] = useState(false);
    
    return (
    	<AnswerCounter className={animate ? "scale" : ""}>
    		<strong>
    			{counter} / {length}
    		</strong>
    	</AnswerCounter>
    )
    const AnswerCounter = styled.h1`
      margin: 1.5rem 0rem;
      text-align: center;
    
      color: ${({ theme }) => theme.colors.lightPink};
    
      font-size: 2rem;
      text-shadow: 0 1px 0 #ccc, 0 2px 0 #ccc, 0 3px 0 #ccc,
        0 10px 10px rgba(0, 0, 0, 0.4);
    
      &.scale {
        transform: scale(1.5, 1.5);
        color: ${({ theme }) => theme.colors.lightYellow};
        text-shadow: 0 1px 0 #fec086, 0 2px 0 #fec086, 0 3px 0 #fec086,
          0 10px 10px rgba(0, 0, 0, 0.4);
      }
    `;

🥺 소요 시간, 어려웠던 점

12h

🍟 하나의 카드 선택 시, 같은 이미지를 가진 카드가 공개됨

  • 하나의 카드를 선택하니까 같은 이미지를 가진 카드가 공개돼버리는 문제가 있었습니다..! 이 부분을 어떻게 해결해야 할 지 고민해봤는데 도무지 방법을 모르겠더라구요?…

  • 결국 두 개의 카드가 같은 요소를 참조하고 있기 때문에 발생되는 문제인 것 같아서 이런 저런 자료를 찾아보다가 깊은 복사까지 가게 됐어요,,,,

    JSON.parse(JSON.stringify(obj))를 활용해서 깊은 복사를 통해 서로 다른 객체를 참조하도록 구현했고, 이 방법으로 문제를 해결하긴 했는데,, 이게 맞는 방법인지 잘 모르겠어요ㅠ

    이 문제에 대한 리뷰 ,,,, (다른 부분에 대한 리뷰도 당연히) 언제나 환영입니당 부탁드려요ㅜ_ㅠ

    // JSON.parse(JSON.stringify(obj)): 깊은 복사
    const copied = JSON.parse(
          JSON.stringify(
            // Object.keys(): 객체를 문자열 배열로 변환
            Object.keys(cardList)
              .map((item) => cardList[item])
              // flat(): 하나의 배열로 만들고자 사용
              .flat()
          )
    

🌈 구현 결과물

_1.mp4

→ 난이도 별로 카드 수 변경됨


_2.mp4

→ 정답 맞출 시, 애니메이션 적용/ 모든 카드를 찾았을 시, 모달 띄워짐/ reset 클릭 시, 카드 순서 변경됨


useMemo() 훅 활용하여 cardList가 변경되지 않을 경우, 이전 값을 재 사용하도록 구현
JSON.parse(JSON.Stringify(obj)) 활용하여 깊은 복사가 가능하도록 수정
Copy link

@iamphj3 iamphj3 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 16 to 31
switch (isClick) {
case 0:
setCurrentPage(<CommonPage cardList={easyCardList} />);
break;

case 1:
setCurrentPage(<CommonPage cardList={normalCardList} />);
break;

case 2:
setCurrentPage(<CommonPage cardList={hardCardList} />);
break;

default:
break;
}
Copy link

Choose a reason for hiding this comment

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

요거 isClick을 요기서 switch문으로 나누지 말고 요렇게 바로 넣고, 공통적인 함수로 수행할 수 있도록 리팩토링 할 수도 있을 것 같아! ㅎㅎ 밑에 mixCardList부분에도 달아놨당!!

Suggested change
switch (isClick) {
case 0:
setCurrentPage(<CommonPage cardList={easyCardList} />);
break;
case 1:
setCurrentPage(<CommonPage cardList={normalCardList} />);
break;
case 2:
setCurrentPage(<CommonPage cardList={hardCardList} />);
break;
default:
break;
}
setCurrentPage(<CommonPage cardList={mixCardList(isClick)} />);

Comment on lines 59 to 76
function mixEasyCardList() {
const mixedEasyCardList = cardImgList
.sort(() => Math.random() - 0.5)
.slice(0, 5);
return mixedEasyCardList.concat(mixedEasyCardList);
}

function mixNormalCardList() {
const mixedNormalCardList = cardImgList
.sort(() => Math.random() - 0.5)
.slice(0, 7);
return mixedNormalCardList.concat(mixedNormalCardList);
}

function mixHardCardList() {
const mixedHardCardList = cardImgList.sort(() => Math.random() - 0.5);
return mixedHardCardList.concat(mixedHardCardList);
}
Copy link

Choose a reason for hiding this comment

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

머ㅓ 이런식으로...?! 아름이가 구현한 방식도 직관적이고 넘 좋은데 난 뭔가 공통된 기능을 수행하는 함수들은 하나로 묶는게 좋을 것 같다구 갠적으로 생각해써 ㅎㅎㅎ 취향차이일수도 ㅎㅎㅎㅎ

Suggested change
function mixEasyCardList() {
const mixedEasyCardList = cardImgList
.sort(() => Math.random() - 0.5)
.slice(0, 5);
return mixedEasyCardList.concat(mixedEasyCardList);
}
function mixNormalCardList() {
const mixedNormalCardList = cardImgList
.sort(() => Math.random() - 0.5)
.slice(0, 7);
return mixedNormalCardList.concat(mixedNormalCardList);
}
function mixHardCardList() {
const mixedHardCardList = cardImgList.sort(() => Math.random() - 0.5);
return mixedHardCardList.concat(mixedHardCardList);
}
function mixCardList(isClick) {
let cardNum;
switch (isClick) {
case 0:
cardNum = 5;
break;
case 1:
cardNum = 7;
break;
case 2:
cardNum = 9;
break;
default:
break;
}
const mixedCardList = cardImgList
.sort(() => Math.random() - 0.5)
.slice(0, cardNum);
return mixedCardList.concat(mixedCardList);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

헐 구러네? 난 이 생각은 못했던 것 같아! 위에 switch 문빼고 이렇게 묶으니까 훨씬 기능도 직관적으로 잘 보여서 좋은 것 같아 나두 공통된 기능하는 함수들 하나로 묶으려고 생각 많이 했던 것 같은데 요건 몰랐네..~~ 리팩토링때 참고해야ㅜ지

Comment on lines 10 to 11
// 기본 난이도 'EASY'로 설정
const [isClick, setIsClick] = useState(0);
Copy link

Choose a reason for hiding this comment

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

0, 1, 2로 난이도 구분하는건가? 그러면 아예 상수로 const EASY = 5, const NORMAL = 7 요런식으로 선언하는건 어땡?!

Choose a reason for hiding this comment

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

위에 현지 말대루 난이도 easy 가 0 이라서 기본 난이도 설정을 easy로 해두는걸 useState(0)으로 한건가??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@SynthiaLee 웅 마자 난이도 0,1,2 로 구분하고 잇써! 그래서 기본 난이도 설정을 easy로 하려고 useState 기본 값을 0으로 한고야!

Copy link
Contributor Author

@Arooming Arooming May 10, 2023

Choose a reason for hiding this comment

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

@iamphj3 (아래 51~54 번째 줄) click한 값과 levelData의 인덱스가 같으면 해당 난이도가 선택됐다는 걸 나타내기 위해서 난이도를 0, 1, 2로 구분해줬어..!!

const levelData = ["EASY", "NORMAL", "HARD"];
// 기본 난이도 'EASY'로 설정
const [isClick, setIsClick] = useState(0);
const [currentPage, setCurrentPage] = useState(<CommonPage />);
Copy link

Choose a reason for hiding this comment

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

초기값에 currentPage 컴포넌트에 cardList props 안들어가도 되는거야?! 밑에 난이도 설정에는 있길래!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

초기값으로는 props없이 페이지만 리턴하고 아래서 난이도 별로 props를 준다고 생각해서 이렇게 구현했더..! 초기값에도 props를 주는게 좋을까?

<ResetButton
type="button"
onClick={() => {
setShuffle((prev) => prev + 1);
Copy link

Choose a reason for hiding this comment

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

왜 리셋버튼을 누르면 setShuffle에 +1을 하는거야??!

Choose a reason for hiding this comment

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

나두 궁금!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아! 이거 reset 버튼이 클릭될 때마다 위에 페이지들을 다시 렌더링시켜서 카드 배열을 변하게 해주려고 shuffle을 생성했어! 그래서 버튼이 클릭될 때마다 shuffle 값에 변화를 줘서 useEffect가 실행될 수 있게 구현해봤어..!!

<>
<ChokiPokiHeader>
<strong> 💗 쵸키랑 푸키를 맞춰주세요! 💗</strong>
<AnswerCounter className={animate ? "scale" : ""}>
Copy link

Choose a reason for hiding this comment

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

animate 이면 scale 효과를 주려고 넣은건강?! 그러면 그냥 요런식으로 props로 넘겨줘서 조건부로 스타일 적용할 수 있지 않으까??!

Suggested change
<AnswerCounter className={animate ? "scale" : ""}>
<AnswerCounter animate={animate}>

Comment on lines 51 to 60
@font-face {
font-family: "UhBeeSe_hyun";
src: url("https://cdn.jsdelivr.net/gh/projectnoonnu/[email protected]/UhBeeSe_hyun.woff")
format("woff");
font-weight: normal;
font-style: normal;
}

font-family: "UhBeeSe_hyun";
font-size: 1rem;
Copy link

Choose a reason for hiding this comment

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

요것두 theme으로 적용해주쟈!!!

Comment on lines +1 to +24
.card {
position: relative;
}

.card .front {
transform: rotateY(90deg);
transition: all ease-in 0.1s;
position: absolute;
}

.flipped .front {
transform: rotateY(0deg);
transition-delay: 0.1s;
}

.card .back {
transition: all ease-in 0.1s;
transition-delay: 0.1s;
}

.flipped .back {
transform: rotateY(90deg);
transition-delay: 0s;
}
Copy link

Choose a reason for hiding this comment

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

요건 왜 js안에 안넣구 따로 css 파일로 빼준걸까??!?!?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

js에서 한번에 정의하기가 헷갈려서 css로 빼서 먼저 기능 구현하고 js에 넣으려고 했는데 그냥 올려버렸....ㅎㅎ 다시 해봐야쥥

};
return (
<div className="card">
<div className={flipped ? "flipped" : ""}>
Copy link

Choose a reason for hiding this comment

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

이것도 일케 바로 prop으로 넘겨주면된당!!!

Suggested change
<div className={flipped ? "flipped" : ""}>
<div flipped={flipped}>

Comment on lines +52 to +54
flippedCard.length === copiedCardList.length
? setModalOn(true)
: setModalOn(false);
Copy link

Choose a reason for hiding this comment

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

Suggested change
flippedCard.length === copiedCardList.length
? setModalOn(true)
: setModalOn(false);
setModalOn(flippedCard.length === copiedCardList.length);

요렇게 하면 좀 더 간결하게 쓸 수 있겠당!ㅎㅎ

{levelData.map((data, idx) => {
return (
<LevelButton
key={idx}

Choose a reason for hiding this comment

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

키값 잊지않구 준거 칭찬해~!

const cardImgList = [
{
name: img_1,
alt: "첫 번째 카드",

Choose a reason for hiding this comment

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

alt도 따로 줬구나! 나는 아예 name으로 alt에도 같이 쓸 수 있는걸 줘서 그걸 재활용했는데 alt랑 이름이랑 많이 다른거 아닌 경우에는 추천해!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

마쟈!! 그렇게 쓰는 것도 좋은 것 같아! 근데 alt는 이미지를 설명하는 글이라 좀 더 직관적이게 해주는게 좋다고 해서 따로 한글로 정의해줬어ㅎㅎ


return (
<>
<Header counter={counter} length={copiedCardList.length / 2} />

Choose a reason for hiding this comment

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

요기 현지가 위에 counter score로 바꾸는거 말한거처럼 length 도 �totalScore 같은걸로 바꿔주면 더 직관적일거 같아!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

죠아!

const flippedCard = document.getElementsByClassName("flipped");

// useMemo() 활용하여 cardList가 변경되지 않을 경우, 이전 값을 재사용하도록 구현
const copiedCardList = useMemo(() => {

Choose a reason for hiding this comment

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

오 useMemo 사용햇구나 싱기...!

Copy link
Contributor Author

@Arooming Arooming May 10, 2023

Choose a reason for hiding this comment

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

웅 useMemo에 dependency로는 cardList를 줘서 리스트가 변하지 않을 경우에는 카드 배열이 변하지 않도록 했어..!! 안그러면 카드를 하나 클릭할 때마다 계속 카드 배열이 변하더라구 흑흑


export default Modal;

const ModalContainer = styled.div`

Choose a reason for hiding this comment

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

div 대신 section 으로 시맨틱하게 바꿔도 좋을거 같아~!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

옹 코드 구현할 때 section 태그는 그룹화하는 역할이라고 생각을 해서 모달 생성에 section을 쓰는건 의도와 맞지 않다고 생각햇던 것 같아! 다시 잘 생각해보고 리팩토링때 참고할게!!

@SynthiaLee
Copy link

어디서 아루미처럼 큐티뽀쨕한 캐릭터를 들고왓서...🥺 수고해따아💓

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.

3 participants