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주차 기본/심화/공유 과제 ] 재미있는 카드게임 #3

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

Conversation

imddoy
Copy link
Contributor

@imddoy imddoy commented May 1, 2024

✨ 구현 기능 명세

🧩 기본 과제

  1. 전체적인 game flow

    • 카드 두개를 오픈해서 같은 카드라면 카드를 오픈 상태에 두고 score를 올립니다.
    • score를 모두 채웠을 때 게임이 종료되고 축하 모달이 뜹니다.
    • 최초 카드 데이터는 데이터 상수 파일로 생성해서 사용합니다.
  2. header 구현

    • 페이지의 이름과 score 현황을 구현합니다
    • 카드의 짝을 맞출 때 마다 score을 상승시킵니다.
    • 최초데이터 데이터 상수 파일 생성
  3. card section 구현

    • 카드가 선택되었을 때 카드를 open합니다.

    • 두 번째 카드가 클릭될 때 까지 카드는 open상태여야합니다.

    • 두 번째 카드를 클릭하였을 때

    • 정답일 경우 카드 두개를 open 상태로 고정하고 score를 올립니다.

    • 오답일 경우 카드 두개를 다시 close합니다.

  4. 게임에 성공하였을 때

    • 축하 모달이 뜨도록 합니다.
    • 모달을 닫았을 때 카드를 모두 close하고 셔플합니다.

🔥 심화 과제

  1. 난이도 설정

    • 난이도를 선택 기능 구현
    • 난이도를 선택시 카드는 랜덤으로 배치, 셔플
    • 게임 중 난이도 변경할 시 리셋
  2. 게임 reset

    • reset버튼 fixed로 생성하여 클릭시 카드 리셋
  3. 카드 뒤집기 애니메이션

    • 카드를 open, close할 때 애니메이션 생성
  4. 모달

    • 백그라운드(back drop)를 어둡게 처리

공유과제

  • React에 대하여
  • 컴포넌트를 분리하는 기준과 방법

링크 첨부(팀 블로그 링크) : https://forweber.palms.blog/nowsopt-react-imddoy


📌 내가 새로 알게 된 부분

  • 랜덤 요소 뽑기
    const randomElement = arr[Math.floor(Math.random() * arr.length)]; 가 랜덤으로 요소를 뽑을 수 있다는 것을 알게 되었습니다.
    저는 이 요소들을 가지고 새로운 카드들을 만들어줘야 하기 때문에 반복문을 사용해서 새로 만들어주었습니다.
  • reuce
    reduce 함수를 이번 기회에 처음 사용해봤습니다.
    배열을 순회하면서 각 요소에 대해 콜백 함수를 실행시키고, 콜백 함수의 반환 값을 accumulator에 계속해서 누적하고 accumulator의 최종 값을 반환하는 함수였는데, 이 함수를 사용해서 새로운 카드들을 만들 수 있었습니다.
  • props 소문자, 대문자 혼용
    무지성으로 코드를 작성하다가 isOpen prop에서 lower이라면서 에러 메세지가 떴습니다.
    HTML 어트리뷰트는 대소문자 구분이 없기 때문에 브라우저는 대문자를 소문자로 변경하여 읽어서 소문자 대문자를 혼용하기 위해서는 $를 사용해야 한다고 해서 해결해주었습니다.

💎 구현과정에서의 고민과정(어려웠던 부분) 공유!

  • 렌더링
    image
    처음에 렌더링이 4번 되고, 첫번째 카드 클릭할 때 2번, 두번째 클릭할 때 4번이 리렌더링되는데,,,
    클릭할 때 상태를 계속 바꿔서 렌더링이 되는건 이해가 되는데, 왜 처음에 렌더링이 많이 되는지 아직도 잘 모르겠습니다.
    그리고 렌더링이 너무 많이 되는게 스트레스인데,,,, 아직 해결 못했습니다. 너무 어렵네요..🥲🥲
    상태도 가장 적게 만든다고 작성하긴 했는데 너무 많은 것 같아서 찝찌입하네요...

  • id 비교

 // 카드 쌍 만들기
    const initialCards = () => {
        const selectedCards = getRandomElement(CARDLIST);
        const cardPairs = selectedCards.reduce(
            (acc, card) => [
                ...acc,
                { ...card, id: card.id + 1000 }, // ID 충돌 방지
                { ...card, id: card.id + 2000 }, // ID 충돌 방지
            ],
            [],
        );
        return cardPairs.sort(() => 0.5 - Math.random());
    };

사실 처음에는 id를 중복되지 않도록 id 뒤에 level만큼 더해서 level을 나눈 나머지로 비교를 하려고 코드를 짰는데
level 선택하도록 구현을 하고 나니까 id가 잘 짝이 안맞아지더라구요... 그래서 하드코딩?으로 1000더하고 2000더하고 1000으로 나눈 나머지로 비교를 하도록 했는데 이렇게 하면 id가 잘 비교가 되는데 이유를 모르겠습니다....ㅜㅜ


🥺 소요 시간

  • 3일

🌈 구현 결과물

https://week3-cardgame.vercel.app/

@imddoy imddoy changed the title Week3 cardgame [ 3주차 기본/심화/공유 과제 ] 재미있는 카드게임 May 1, 2024
@imddoy imddoy self-assigned this May 1, 2024
Comment on lines +1 to +25
### 🧩 기본 과제

1. **전체적인 game flow**

- [x] 카드 두개를 오픈해서 같은 카드라면 카드를 오픈 상태에 두고 score를 올립니다.
- [x] score를 모두 채웠을 때 게임이 종료되고 축하 모달이 뜹니다.
- [x] 최초 카드 데이터는 데이터 상수 파일로 생성해서 사용합니다.

2. **header 구현**
- [x] 페이지의 이름과 score 현황을 구현합니다.
- [x] 카드의 짝을 맞출 때 마다 score을 상승시킵니다.
- [x] 최초데이터 데이터 상수 파일 생성
3. **card section 구현**

- [x] 카드가 선택되었을 때 카드를 open합니다.
- [x] 두 번째 카드가 클릭될 때 까지 카드는 open상태여야합니다.

- 두 번째 카드를 클릭하였을 때
- [x] 정답일 경우 카드 두개를 open 상태로 고정하고 score를 올립니다.
- [x] 오답일 경우 카드 두개를 다시 close합니다.

4. **게임에 성공하였을 때**

- [x] 축하 모달이 뜨도록 합니다.
- [x] 모달을 닫았을 때 카드를 모두 close하고 셔플합니다.
Copy link
Member

Choose a reason for hiding this comment

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

이야 이렇게 리드미로 구현사항 적용하면서 하는 분은 처음 보네요 ㅎㅎ!
꼼꼼한 모습 너무 보기 좋습니다 :)

Copy link
Member

Choose a reason for hiding this comment

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

저도 처음 보네요..... 이게 파워 J인가...?

Choose a reason for hiding this comment

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

우앙... 저는 맨날 노션 들어가서 하나씩 확인했는데 이렇게 해두면 구현할 때도 편하고 PR 날릴 때도 리드미만 복사해오면 되겠네요!! 짱이에요,,,,,,,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

자꾸 제가 뭘했는지 뭐 해야하는지 까먹어서 기능 구현하면 하나씩 체크 표시하게 되었습니당...ㅎ

Comment on lines 9 to 16
<BrowserRouter>
<GlobalWrapper>
<Routes>
<Route path="/" element={<GamePage />} />
</Routes>
</GlobalWrapper>
</BrowserRouter>
</>
Copy link
Member

Choose a reason for hiding this comment

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

리액트 라우터가 업데이트 되면서,
공식문서에서 createBrowserRouter 사용을 더 권하는 것 같아요!
링크 남겨둘테니 저희 합세에선 이렇게 적용해봅시다 ㅎㅎ!

https://reactrouter.com/en/main/start/overview

Copy link
Member

Choose a reason for hiding this comment

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

옹 하나 배워갑니다!!! @ExceptAnyone

Choose a reason for hiding this comment

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

우앙 처음 알았네요....!! 배워갑니다 ✨

Copy link
Contributor Author

@imddoy imddoy May 7, 2024

Choose a reason for hiding this comment

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

오 처음 보는건데 신기하네요! 합세때 적용할 수 있도록 수정했습니다

Comment on lines +39 to +43
useEffect(() => {
if (score === level) {
setModalOpen(true);
}
}, [score]);
Copy link
Member

Choose a reason for hiding this comment

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

저도 이번에 주용님께 배웠는데, 요기 useEffect는 없어도 될거에요!
한 번 지우고 if문만 남겨서 해보시는 방법도 좋을 것 같습니당

Copy link
Contributor Author

Choose a reason for hiding this comment

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

혹시 왜 그런것일까요..? 저는 useEffect를 지우니까 안돌아가서요...

Copy link
Member

Choose a reason for hiding this comment

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

아마 무한 리렌더링 발생 방지 경고가 뜰 수도 있어요!
저희 금잔디 조원인 이진님도 같은 오류가 발생했었는데요,
if문 안에 조건에 상태 조건을 하나 추가해줘야해요!
한 번 어떤 조건이 들어가야하는지 고민해보시길 ㅎㅎ

안되면 바로 콜해주세요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이건 좀더 고민해보겠습니다.....ㅠ 감사합니다

Copy link
Contributor

Choose a reason for hiding this comment

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

이 로직을 그대로 useEffect없이 updateScore 로직에 넣으면 적용이될 것 같은데요? 한번 시도해보세요~~

  const updateScore = (newScore) => {
        setScore(newScore);
          if (newScore === level) {
            setModalOpen(true);
        }
    };

@@ -0,0 +1,13 @@
import React from 'react';
import * as H from './HeaderStyle';
Copy link
Member

Choose a reason for hiding this comment

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

헤더는 H라서 H dot naming을 하신걸까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵 맞습니다! 여러개 import해올 때 헷갈려서 저렇게 쓰게 되었는데 호옥시 다른 방법이 있는걸까요??

Copy link
Member

Choose a reason for hiding this comment

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

아 아니요! 단순히 궁금했습니다 ㅎㅎ!


export default function GamePage() {
const [score, setScore] = useState(0);
const [level, setLevel] = useState(5);
Copy link
Member

Choose a reason for hiding this comment

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

조금 디테일적인 부분이긴한데, 지금 저희는 같은 과제 속에 같은 기능을 구현하고 있기에
이 숫자 5가 뭘 의미하는지 잘 알고있죠 ㅎㅎ

그러나 추후 합세와 웹잼에서는 서로 다른 기능을 개발하고 있기에 코드리뷰를 받을 때
다른 사람입장에서 이 숫자5가 무엇을 의미하는지 모를 수도 있어요!

그래서 저는 이런 부분을 상수로 빼두고 의미있는 네이밍을 하려고 노력하는데 한 번 고민해보셔도 좋을 것 같아요 ㅎㅎ!!

export const GAME_LEVEL = Object.freeze({
    EASY: 5,
    NORMAL:7,
    HARD:9
})

이런 방법이 있겠군요 ㅎㅎ

제 이번 카드 뒤집기 게임 과제 코드 중 constant 속을 한 번 참고해보셔도 좋을 것 같습니다!

Copy link
Member

Choose a reason for hiding this comment

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

조금 디테일적인 부분이긴한데, 지금 저희는 같은 과제 속에 같은 기능을 구현하고 있기에 이 숫자 5가 뭘 의미하는지 잘 알고있죠 ㅎㅎ

그러나 추후 합세와 웹잼에서는 서로 다른 기능을 개발하고 있기에 코드리뷰를 받을 때 다른 사람입장에서 이 숫자5가 무엇을 의미하는지 모를 수도 있어요!

그래서 저는 이런 부분을 상수로 빼두고 의미있는 네이밍을 하려고 노력하는데 한 번 고민해보셔도 좋을 것 같아요 ㅎㅎ!!

export const GAME_LEVEL = Object.freeze({
    EASY: 5,
    NORMAL:7,
    HARD:9
})

이런 방법이 있겠군요 ㅎㅎ

제 이번 카드 뒤집기 게임 과제 코드 중 constant 속을 한 번 참고해보셔도 좋을 것 같습니다!

좋네요...!! 저도 놓쳤던 부분인데 리팩때 꼭 적용해봐야겠네요 👍🏻👍🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오오 섬세하게 봐주시고 코드까지 보여주시다니! 이해가 잘되었습니다! 감사합니다 :)

Comment on lines 6 to 14
const levelSettings = [
{ label: 'Easy', level: 5 },
{ label: 'Normal', level: 7 },
{ label: 'Hard', level: 9 },
];
return (
<BtnWrapper>
{levelSettings.map((setting) => (
<LevelBtn
Copy link
Member

Choose a reason for hiding this comment

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

여기도 constant폴더 속 상수로 분리해도 좋고,
아니면 상태에 따라 변하는 부분이 아니니깐 컴포넌트 밖에서 선언해도 괜찮아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵 감사합니다! 반영했습니다!

Comment on lines 4 to 8
export default function Card(props) {
return (
<C.CardWrapper $isOpen={props.open}>
<C.FrontCard $img={props.img}></C.FrontCard>
<C.BackCard></C.BackCard>
Copy link
Member

Choose a reason for hiding this comment

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

구조분해할당으로 받아서 가독성을 높여봅시다 ㅎㅎ!

Copy link
Contributor Author

@imddoy imddoy May 7, 2024

Choose a reason for hiding this comment

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

넵!! 충고 감사합니다! 수정했습니다!

Copy link
Member

@jungwoo3490 jungwoo3490 left a comment

Choose a reason for hiding this comment

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

잘한다는 얘기 많이 들었는데 역시... 전체적으로 JS의 내장 함수들을 잘 사용한다는 느낌을 많이 받았어요!! 특히 기억에 남는 건 reducer 메소드인데, 이건 저도 잘 몰랐던 부분이라 덕분에 좋은 메소드 하나 공부하고 알아가네요 ㅎㅎㅎ

질문 남겨주신 것에 대해 답변을 해볼게요!!

1. 렌더링
처음에 2번씩 찍히는 이유는 바로 StrictMode 때문입니다. StrictMode는 개발 환경에서만 활성화되는 기능으로, 컴포넌트의 예상치 못한 부작용을 감지하기 위해 렌더링을 두 번 수행하는 모드입니다!! 이게 적용되어 있는 부분은 main.jsx에서 확인할 수 있는데, React.StrictMode로 감싼 부분이 해당 부분입니다!! 제거하려면 감싸는 태그를 제거하면 끝입니다! 실제로 이것을 제거하고 채현이가 캡쳐한 상황을 재연해보면 다음과 같이 1번씩만 찍히는 것을 볼 수 있습니다!!
image

초기 렌더 시 useEffect가 동작해서 useEffect 내부 함수들을 모두 실행하므로 콘솔에 한 번씩 찍히고, state가 변경되니 리렌더링이 1번 일어나서 "렌더링"이 콘솔에 추가적으로 한 번 더 찍히는 정상적인 상황입니다!!

여기서 연속으로 4개의 state를 바꿨으니 리렌더링이 4번 일어나야 하는데 왜 렌더링이 1번 찍히지?라고 궁금하실 수도 있어서 오지랖을 좀 부려보자면.... React는 state를 업데이트 할 때 성능 및 리렌더링 최적화를 위해 batch 기법이라는 것을 사용해요!! batch 기법은 여러 개의 상태 업데이트를 그룹화해서 한 번의 리렌더링으로 처리해버리는 기법이에요!! 그래서 4개의 상태 업데이트가 있었지만, 리렌더링은 1번 발생한 것입니다 ㅎㅎㅎ

Strict(안전) 모드라고는 하지만, 제거한다고 성능상 문제가 발생하지는 않으니, 지우고 개발 진행하셔도 됩니다!!

2. id 비교
제가 잘 이해한 게 맞다면, id 뒤에 level만큼 더해서 level을 나눈 나머지로 id를 세팅하면

 const cardPairs = selectedCards.reduce(
            (acc, card) => [
                ...acc,
                { ...card, id: card.id + level }, // ID 충돌 방지
                { ...card, id: card.id + level * 2 }, // ID 충돌 방지
            ],
            [],
        );

이런 식으로 작성했다고 이해를 했는데요!!
만약 level이 5라면, card.id가 0인 경우와 card.id가 5인 경우가 (card.id + level) % level 값이 같으므로 제대로 동작하지 않을 가능성이 있습니다!!
1000처럼 큰 수가 아니더라도, 더하는 수를 카드들의 id 중 최댓값 * 2보다 크게 해준다면, 비교가 꼬이지 않을 것 같네요!!

정안님이 남겨주신 리뷰들도 정말 유익하고 좋은 리뷰들이니 꼭 읽어보고 적용해봤으면 좋겠어요!!!
@ExceptAnyone 최고의 OB... 👍🏻👍🏻👍🏻

이번 과제 난이도가 높았는데 심화까지 완성하시느라 고생했어요!!!!! 👍🏻👍🏻

Comment on lines 9 to 16
<BrowserRouter>
<GlobalWrapper>
<Routes>
<Route path="/" element={<GamePage />} />
</Routes>
</GlobalWrapper>
</BrowserRouter>
</>
Copy link
Member

Choose a reason for hiding this comment

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

옹 하나 배워갑니다!!! @ExceptAnyone

Comment on lines 20 to 22
const GlobalWrapper = styled.main`
margin: 0 auto;
`;
Copy link
Member

Choose a reason for hiding this comment

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

이 요소가 Routes 상위에 있는 걸로 보아 전역 스타일 역할을 하는 것 같은데 GlobalStyle로 빼주는 건 어떨까요~?

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 getRandomElement = (arr) => {
console.log('getRandomElement');
Copy link
Member

Choose a reason for hiding this comment

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

console문 지워주기!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이걸 놓쳤네요......ㅎ 감사합니다

Comment on lines +15 to +20
let result = new Set();
while (result.size < level) {
const randomElement = arr[Math.floor(Math.random() * arr.length)];
result.add(randomElement);
}
return Array.from(result);
Copy link
Member

Choose a reason for hiding this comment

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

중복제거로 Set 사용한 거 신박하네요!!!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

구글링하다가 좋아 보여서 하게 되었습니당....ㅎ

}, [level, shuffle]);

function chooseCard(e, id) {
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.

혹시 이렇게 이벤트 전파를 방지시킨 특별한 이유가 있을까요??
코드 구조를 보면 CardHandler 상위 요소에 특별히 click 이벤트 핸들러가 바인딩되어있지 않은 것 같아서 이 부분이 없어도 클릭 이벤트가 의도대로 잘 동작할 것 같다는 생각이 듭니다!!

만약 이유가 있다고 하더라고, e.stopPropagation()으로 강제로 이벤트 전파를 막는 방식은 지양해야 합니다!! 무엇이든지 앞에 "강제"가 붙는 행위는 사이드 이펙트가 발생할 가능성을 내포하고 있기 때문이에요!!

만약 나중에 이벤트 전파를 막아야 하는 상황이 온다면, 그것은 코드 구조를 잘못 짰다는 방증이니 최대한 코드 구조를 수정해서 이벤트 전파를 막는 방향으로 수정하는 것이 맞습니다!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

헉....저를 정확히 알고 계십니다.....
클릭 이벤트가 자꾸 다른 카드로 영향이 가서 검색하다가 저 코드를 사용하라고 나와서 적용했는데
그냥 저의 코드 문제였습니다...ㅎ
로직 변경을 해서 오류를 고쳤는데 저 코드를 아무생각없이 남겨 두었습니다....
코드 리뷰 감사합니다! 좋은 정보를 얻어갑니다!

height: 6rem;
background-color: ${(props) => props.theme.colors.grey};
border: none;
border-radius: 20px;
Copy link
Member

Choose a reason for hiding this comment

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

단위 px인 거 확인하기!!

Comment on lines 3 to 36
export const ModalContainer = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
`;

export const ModalWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 50rem;
height: 10rem;
background-color: ${(props) => props.theme.colors.white};
border-radius: 10px;
font-size: 3rem;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
`;

export const ConfirmBtn = styled.button`
padding: 10px 20px;
background-color: ${(props) => props.theme.colors.pink};
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 20px;
`;
Copy link
Member

Choose a reason for hiding this comment

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

CSS를 작성할 때 같은 속성을 가진 CSS끼리 묶어주고, 속성별로 개행을 해주면 가독성이 훨씬 좋아져요!! 저는 Mozilla의 속성 정렬 순서에 맞춰 정렬하고 있어요!! 참고해보시면 좋을 것 같아요!!

https://milooy.github.io/TIL/CSS/css-property-order.html#intro

또 이 파일에서 전체적으로 px단위로 설정되어 있는데 단위 한 번 확인 부탁드립니다아~!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오 급하게 한다고 코드가 정말 개판이었네요.....부끄럽습니다ㅎㅎㅎㅎ 감사합니다 :)


export default function GamePage() {
const [score, setScore] = useState(0);
const [level, setLevel] = useState(5);
Copy link
Member

Choose a reason for hiding this comment

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

조금 디테일적인 부분이긴한데, 지금 저희는 같은 과제 속에 같은 기능을 구현하고 있기에 이 숫자 5가 뭘 의미하는지 잘 알고있죠 ㅎㅎ

그러나 추후 합세와 웹잼에서는 서로 다른 기능을 개발하고 있기에 코드리뷰를 받을 때 다른 사람입장에서 이 숫자5가 무엇을 의미하는지 모를 수도 있어요!

그래서 저는 이런 부분을 상수로 빼두고 의미있는 네이밍을 하려고 노력하는데 한 번 고민해보셔도 좋을 것 같아요 ㅎㅎ!!

export const GAME_LEVEL = Object.freeze({
    EASY: 5,
    NORMAL:7,
    HARD:9
})

이런 방법이 있겠군요 ㅎㅎ

제 이번 카드 뒤집기 게임 과제 코드 중 constant 속을 한 번 참고해보셔도 좋을 것 같습니다!

좋네요...!! 저도 놓쳤던 부분인데 리팩때 꼭 적용해봐야겠네요 👍🏻👍🏻

return (
<CardContainer>
{cards.map((card, index) => (
<CardWrapper key={index} onClick={(e) => chooseCard(e, card.id)}>
Copy link
Member

Choose a reason for hiding this comment

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

key 값으로 map의 순회 index를 주는 것은 지양해야 합니다!!
idx가 변경되는 이벤트가 발생하면 렌더되는 컴포넌트에서 사용하는 state가 의도하지 않은 방식으로 바뀔 수도 있기 때문이에요!!

고유하게 사용할 마땅한 값이 없으면 다음과 같이 해주는 방법도 많이 사용합니다!!

Suggested change
<CardWrapper key={index} onClick={(e) => chooseCard(e, card.id)}>
<CardWrapper key={`card-${index}`} onClick={(e) => chooseCard(e, card.id)}>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오 이런 방법이 있다니 신세계입니다! 적용 완료했습니다~~ㅎㅎ

Comment on lines +1 to +25
### 🧩 기본 과제

1. **전체적인 game flow**

- [x] 카드 두개를 오픈해서 같은 카드라면 카드를 오픈 상태에 두고 score를 올립니다.
- [x] score를 모두 채웠을 때 게임이 종료되고 축하 모달이 뜹니다.
- [x] 최초 카드 데이터는 데이터 상수 파일로 생성해서 사용합니다.

2. **header 구현**
- [x] 페이지의 이름과 score 현황을 구현합니다.
- [x] 카드의 짝을 맞출 때 마다 score을 상승시킵니다.
- [x] 최초데이터 데이터 상수 파일 생성
3. **card section 구현**

- [x] 카드가 선택되었을 때 카드를 open합니다.
- [x] 두 번째 카드가 클릭될 때 까지 카드는 open상태여야합니다.

- 두 번째 카드를 클릭하였을 때
- [x] 정답일 경우 카드 두개를 open 상태로 고정하고 score를 올립니다.
- [x] 오답일 경우 카드 두개를 다시 close합니다.

4. **게임에 성공하였을 때**

- [x] 축하 모달이 뜨도록 합니다.
- [x] 모달을 닫았을 때 카드를 모두 close하고 셔플합니다.
Copy link
Member

Choose a reason for hiding this comment

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

저도 처음 보네요..... 이게 파워 J인가...?

Copy link

@sinji2102 sinji2102 left a comment

Choose a reason for hiding this comment

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

style이랑 handler 분리한 거 보고 완전 감탄했습니다.... 🫣 사실 다들 하시는 건데 제가 아직 감자라서 몰랐던 거일 수도 있지만,,, 아무튼 정리 너무너무 잘해두셔서 감자인데도 불구하고 술술 읽히는 코드였던 것 같아요!! 너무 배워가기만 하고 알려드리는 게 없어 쪼금 민망한 코드리뷰지만,,, OB 분들이 꼼꼼하게 코드리뷰 남겨주신 것 같아 감탄만 하고 그것까지 배워가면서 코리 남기고 갑니다!! 수고하셨어요 최고 🩷✨

Comment on lines 9 to 16
<BrowserRouter>
<GlobalWrapper>
<Routes>
<Route path="/" element={<GamePage />} />
</Routes>
</GlobalWrapper>
</BrowserRouter>
</>

Choose a reason for hiding this comment

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

우앙 처음 알았네요....!! 배워갑니다 ✨

Choose a reason for hiding this comment

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

우앙 Style 파일 별도로 정리한 거 너무 보기 깔끔하고 좋은 것 같아요...!! ✨ 한 번도 Style 파일을 별도로 정리한 적이 없어서 많은 역할을 하는 컴포넌트는 스타일까지 넣으면 너무 길어지곤 했는데,,, 좋은 방법 배워갑니다!! 🩷 동시에 별도로 정리할 경우 태그가 어떤 역할을 하는 친구인지 확인하려면 스타일을 적용하고자 하는 컴포넌트에서 확인해야 하니 태그 이름을 지을 때 더욱 신중해야겠다는 생각이 드네요... 🧐

Copy link
Contributor Author

Choose a reason for hiding this comment

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

코드가 너무 길어져서 따로 임포트해와서 쓰게 되었습니당....


export default function LevelBtn({ children, isActive, onClick }) {
return (
<B.Button isActive={isActive} onClick={onClick}>

Choose a reason for hiding this comment

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

isActive를 어디서 받아오는 건가 했는데 handler에서 받아오는 거군요.....!! 저는 하나의 컴포넌트에서 기능과 스타일까지 다 적용해서 길어지고 가독성도 낮아지기 일쑤였는데... handler를 굳이 분리해야 하나?? 싶다가도 채현님 코드 깔끔한 걸 보니 분리하는 게 좋겠다는 생각이 왕창 드네요... 🔥진짜 배워가는 게 너무너무 많은 코드입니다!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

코드가 너무 길어지게 되어서 보기가 너무 힘들더라구요...그래서 그냥 따로 하는게 관리하기 편할 것 같아서 따로 빼주었습니다!

Comment on lines +1 to +25
### 🧩 기본 과제

1. **전체적인 game flow**

- [x] 카드 두개를 오픈해서 같은 카드라면 카드를 오픈 상태에 두고 score를 올립니다.
- [x] score를 모두 채웠을 때 게임이 종료되고 축하 모달이 뜹니다.
- [x] 최초 카드 데이터는 데이터 상수 파일로 생성해서 사용합니다.

2. **header 구현**
- [x] 페이지의 이름과 score 현황을 구현합니다.
- [x] 카드의 짝을 맞출 때 마다 score을 상승시킵니다.
- [x] 최초데이터 데이터 상수 파일 생성
3. **card section 구현**

- [x] 카드가 선택되었을 때 카드를 open합니다.
- [x] 두 번째 카드가 클릭될 때 까지 카드는 open상태여야합니다.

- 두 번째 카드를 클릭하였을 때
- [x] 정답일 경우 카드 두개를 open 상태로 고정하고 score를 올립니다.
- [x] 오답일 경우 카드 두개를 다시 close합니다.

4. **게임에 성공하였을 때**

- [x] 축하 모달이 뜨도록 합니다.
- [x] 모달을 닫았을 때 카드를 모두 close하고 셔플합니다.

Choose a reason for hiding this comment

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

우앙... 저는 맨날 노션 들어가서 하나씩 확인했는데 이렇게 해두면 구현할 때도 편하고 PR 날릴 때도 리드미만 복사해오면 되겠네요!! 짱이에요,,,,,,,

imddoy added 10 commits May 8, 2024 17:01
null은 falsy한 값이므로 똑같게 사용됨
 null인 경우와 다른 falsy한 값(0 등)일 경우가 다른 경우로 분기되어야 하는 상황은 제외
코드를 봤을때 이해하기 쉽도록
5로 하드코딩하는 것이 아닌 상수에서 값을 가져오기
Copy link
Contributor

@ljh0608 ljh0608 left a comment

Choose a reason for hiding this comment

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

처음에 렌더링이 4번이라... 맛있는 useEffect 잔치가 벌어진 것 같아요 useEffect 실행되는 순서가 기억나시나요? side effect를 다루기에 score나 전체적인 state가 렌더링 된 후 다시 useEffect로 돌아가서 trigger가 성립하면 한 번 더 리렌더링을 시키기 때문에 이부분이 가장 유력하다고 볼 수 있습니다!

코리 이해 안가시면 오늘, 내일 카톡하거나 디코와주세요~

Comment on lines +6 to +9
<C.CardWrapper $isOpen={open}>
<C.FrontCard $img={img}></C.FrontCard>
<C.BackCard></C.BackCard>
</C.CardWrapper>
Copy link
Contributor

Choose a reason for hiding this comment

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

보통은 S dot네이밍이라고 하는데 이렇게 구현하신 이유가 궁금합니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

styled-components를 사용할 때 폴더를 분리하는 방법을 선택하신 것 같군요!
보통 style 파일을 따로 만들 때는 HeaderStyle.jsx 보단 Header.style.jsx 같은 방식을 많이 사용합니다~

setOpenCards([]);
}, [level, shuffle]);

function chooseCard(e, id) {
Copy link
Contributor

Choose a reason for hiding this comment

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

이부분도 화살표함수로 사용하면 더 좋을 것 같은데 사용하신 다른 이유가 있으신가요?

Comment on lines +27 to +33
export const FrontCard = styled.article`
background-image: url(${(props) => props.$img});
background-size: contain;
background-repeat: no-repeat;
background-position: center;
`;
export const BackCard = styled.article`
Copy link
Contributor

Choose a reason for hiding this comment

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

Semantic 태그 잘 사용하셨네요 굿굿!


export const Title = styled.h1`
text-align: center;
font-size: 5rem;
Copy link
Contributor

Choose a reason for hiding this comment

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

font-size도 theme에 정의해서 사용하면 더 유지보수 하기 편리할 것 같아요!

Comment on lines +14 to +32
// 점수 업데이트
const updateScore = (newScore) => {
setScore(newScore);
};
// 난이도 업데이트
const updateLevel = (level) => {
setLevel(level);
};
// 게임 리셋
const resetGame = () => {
setLevel(LEVEL[0].level);
setScore(0);
setShuffle(!shuffle);
};

const closeModal = () => {
setModalOpen(false);
resetGame();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

깔끔하게 잘 짜셨네요 완전 가독성좋은 코드!

Comment on lines +39 to +43
useEffect(() => {
if (score === level) {
setModalOpen(true);
}
}, [score]);
Copy link
Contributor

Choose a reason for hiding this comment

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

이 로직을 그대로 useEffect없이 updateScore 로직에 넣으면 적용이될 것 같은데요? 한번 시도해보세요~~

  const updateScore = (newScore) => {
        setScore(newScore);
          if (newScore === level) {
            setModalOpen(true);
        }
    };

Comment on lines +19 to +21
const updateLevel = (level) => {
setLevel(level);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

level이 변경되면 score0되는 로직 여기에 넣으시면 되겠네요!
setScore(0) 추가해볼까요?

Comment on lines +23 to +28
const resetGame = () => {
setLevel(LEVEL[0].level);
setScore(0);
setShuffle(!shuffle);
};

Copy link
Contributor

Choose a reason for hiding this comment

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

이부분에 이전에 useEffect에서 사용하던 로직을 추가해주고 이 resetGame을 updateLevel함수에도 넣어주면 될 것 같아요!

setCards(initialCards());
        setSelectedCard(null);
        setMatchCards([]);
        setOpenCards([]);

Comment on lines +37 to +42
useEffect(() => {
setCards(initialCards());
setSelectedCard(null);
setMatchCards([]);
setOpenCards([]);
}, [level, shuffle]);
Copy link
Contributor

Choose a reason for hiding this comment

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

이건 연관된게 많아 확실하진 않지만 level을 변경해주는 함수인 updateLevel함수와 reset하는 함수 resetGame에 필요한 로직인 것 같네요 그러면 resetGame에만 useEffect내부 로직을 적용해주고 resetGame함수를 이곳에 호출해볼까요?

const updateLevel = (level) => {
          resetGame()      
        setLevel(level);
    };

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

Successfully merging this pull request may close these issues.

5 participants