Skip to content

Conversation

@ayla-12
Copy link
Contributor

@ayla-12 ayla-12 commented Nov 7, 2024

✨ 구현 기능 명세

💡 기본 과제

  • Context API, 전역상태 라이브러리 사용 X (ThemeProvider 제외)
  1. 헤더
  • 게임/랭킹 2개의 메뉴 선택 가능
  • 게임 선택 시 헤더 우측에 레벨 선택 Select와 타이머 표시
  • 게임 선택 시 게임 판 출력
  • 랭킹 선택 시 헤더 우측엔 아무것도 나오지 않음
  • 랭킹 선택 시 랭킹 보드 출력
  1. 게임
  • (기본) 한 종류의 레벨만 구현
  • 숫자는 항상 랜덤으로 표시됨. (초기 표시 숫자들도, 이후 열리는 숫자들도 모두 랜덤)
  • 처음에 표시되는 숫자는 클릭해야 하는 숫자의 앞에 절반임. 만약 level 1이라 118까지 클릭해야한다면, 처음에는 19까지의 숫자가 랜덤으로 보여짐
  • 게임판 위쪽에 다음으로 클릭해야할 숫자를 표시
  • 1을 누르는 순간 게임이 시작되며 헤더 우측의 타이머가 동작. 타이머는 소수점 2번째 자리까지 측정.
  • 마지막 숫자 클릭시 게임 종료
  • 게임 종료 시, 타이머를 멈추고 alert 창을 띄워주며 걸린 시간을 표시
  • 게임 종료 시, 현재 시각, 게임의 레벨, 플레이 시간 3개의 정보를 localStorage에 저장 (랭킹에서 사용)
  • 종료 창에서 확인 누르면 다시 시작할 수 있는 상태로 게임 초기화
  • 게임 중 level 변경 시 다시 시작할 수 있는 상태로 게임 초기화
  1. 랭킹
  • localStorage에서 데이터 불러오기
  • 플레이 시간 오름차순으로 보여야 함 (빨리 깬 기록이 위쪽)
  • 우측 상단의 초기화 버튼 누르면 대시보드 초기화 (localStorage도 초기화)

🔥 심화 과제

  1. 게임
  • Level 선택 가능
    Level 1: 3 x 3, Level 2: 4 x 4, Level 3: 5 x 5
  • 숫자 클릭할 때 클릭되는 것 같은 효과 (예시: 깜빡거림)
  • 게임 종료 alert 대신, React의 createPortal을 사용하여 Modal 구현
    createPortal
  1. 랭킹
  • Level 내림차순 & 시간 오름차순 정렬(정렬 기준이 2개). 높은 Level이 위쪽으로, 같은 레벨 중에선 플레이 시간이 짧은게 위쪽으로 정렬

공유과제

제목:

링크 첨부 :


❗️ 내가 새로 알게 된 점

  • 랜덤 구현하는 방법을 새롭게 배웠습니다. 그런 방법도 있구나 싶었던... sort() 활용하기
  • inteval 에 관련된 것을 학습했습니다. 근데 아직 완전히 이해하지 못한 것 같아 조금 더 파봐야할 것 같습니다.
  • state와 prop의 차이점, 그리고 상태 관리의 흐름에 대해서 배웠습니다. 무조건 위에서 아래로밖에 못내려준다!!

❓ 구현 과정에서의 어려웠던/고민했던 부분

  • 레벨 다르게 구현을 어떻게 해야할지 모르겠어요. 일단 레벨 state자체는 만들어뒀는데 활용하지 못하였습니다.
  • 헤더에 떠있는 시간과, alert창에 뜨는 시간이 다릅니다. 이걸 해결하기 위해 현재 시간과 플레이 시작한 시간을 빼는 식으로 타이머를 구현했음에도 불구하고 다르더라구요. 어카죠? ...
  • 로컬 스토리지 정렬 구현을 아직 못했습니다. 근데 이건 팟짱님이 알려주셔서 구현 혼자 해내보겠습니다!!!

🥲 소요 시간

  • 35h
  • 기존보다 투자를 많이 못했어요.
  • 일단 코리는 받아야하니까 올립니다 ... 더 노력하겠습니다!!!

🖼️ 구현 결과물

20241108_025823.mp4

for (let i = 0; i < localStorage.length; i++) {
const gameDataItem = JSON.parse(localStorage.getItem(localStorage.key(i)));
gameData.push(gameDataItem);
}
Copy link
Member

Choose a reason for hiding this comment

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

p5: 오름차순으로 정렬하려면 sort() 함수를 사용해서 배열 속 두 요소 a와 b를 비교해주면 돼요! 두 값을 빼서 더 작은 값이 앞에 오도록 하면, 배열이 작은 값부터 큰 값 순으로 정렬됩니다!

const handleTimeUpdate = (time) => {
setPlayTime(time);
};

Copy link
Member

Choose a reason for hiding this comment

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

p5: 레벨 변경에 따른 컴포넌트를 업데이트 하기 위한 handleLevelChange 함수를 두고 값이 변경 될 때 업데이트 해줄 수 있어요 저는 .. 코드가 많이 겹치는 걸 알고 있지만, 방법을 잘 몰라 레벨마다 컴포넌트를 각각 만들어서 조건부로 렌더링 해주었던 것 같아요!

key={index}
className={styles.GameBtn}
onClick={() => handleNumberClick(num, index)}
disabled={num === null} // 빈칸은 클릭 비활성화
Copy link
Member

Choose a reason for hiding this comment

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

p4: handleNumberClick 중 사용된 숫자를 slice로 availableNumbers 배열에서 제거하면서 null 값을 설정해주고 있어서 disabled를 지워주어도 게임이 잘 진행되고 있어요!

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
Copy link
Member

Choose a reason for hiding this comment

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

p1: ko 해주세요ㅎㅎㅎㅎ

const [startTime, setStartTime] = useState(0);
const [playTime, setPlayTime] = useState(0);

const [level, setLevel] = useState(1); // 기본 레벨 1 (일단 심화과제 할 수 있으면 하기)
Copy link
Member

Choose a reason for hiding this comment

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

p5: 꼭 심화과제까지 잘 완료하셨으면 좋겠어요 !! 응원해요 👍


.GameBtn:active {
background-color: #45776A;
transform: scale(0.95); /* 살짝 눌리는 효과 */
Copy link
Member

Choose a reason for hiding this comment

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

p5: 효과가 귀여워요! 누를 때 효과가 있어서 누르는 느낌이 더 크게 느껴져서 게임이 더 재밌는 것 같아요

<td>{data.playTime}s</td>
</tr>
))
// 아니라면 저장된 랭킹 없지롱~
Copy link
Member

Choose a reason for hiding this comment

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

p5: 랭킹이 없을 때도 신경 써주시구... 섬세해요👍😊

@hansoojeongsj
Copy link
Member

안녕하세요 :)
저도 이번에 많이 어려워서 한참 걸렸던 것 같아요..! 그리고 프로젝트를 처음 실행했을 때, alert와 header의 시간이 달라서 프로젝트를 다시 만들어야 했던 기억이 나네여..ㅎㅎ 저는 타이머 기능과 게임 기능을 분리하지 못하고 게임 컴포넌트에 다 몰아넣었는데, 좋은 해결 방법이 있으시면 알려주세요! 같이 열심히 공부하면서 성장해요 😊 파이팅 !!

Copy link

@m2na7 m2na7 left a comment

Choose a reason for hiding this comment

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

과제 고생하셨어요 !! 😊

폴더 구조도 깔끔하고, App.jsx에서 컴포넌트들을 조립하는 방식으로 리액트답게 잘 구현하셨네요 👍
고민하신 부분은 코멘트에 남겨두었으니 해결해보시고 의견 남겨주세요 !!

Comment on lines +7 to +21
const getGameDataFromLocalStorage = () => {
const gameData = [];
//for문을 돌면서 데이터를 들고옴.
for (let i = 0; i < localStorage.length; i++) {
const gameDataItem = JSON.parse(localStorage.getItem(localStorage.key(i)));
gameData.push(gameDataItem);
}
return gameData;
}

// 랭킹 데이터를 로컬스토리지에서 불러온 다음 data로 설정함.
useEffect(() => {
const data = getGameDataFromLocalStorage();
setRankingData(data);
}, []);
Copy link

Choose a reason for hiding this comment

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

현재 App.jsx에서 게임이 끝날 때마다 localStorage에 고유한 키(game_${Date.now()})를 사용하여 데이터를 개별적으로 저장하고 있어요.
해당 방식은 기록이 많아진다면 localStorage의 모든 데이터를 순회해야해서 비효율적이고, 코드의 복잡도가 증가한다고 생각해요.
실제로 게임데이터를 순회하여 랭킹판에 출력할 때, 모든 로컬스토리지에 있는 값을 순회해야해서 Ranking.jsx에 불필요하게 코드가 길어지고 있어요.

모든 게임 기록을 gameRecordsData와 같은 단일 키에 배열로 저장하면, 각 기록을 조회하거나 새로운 기록을 추가할 때 효율적으로 처리할 수 있어요 !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

안그래도 팟짱님이 말씀해주신 방법이예요. 한 key에 저장하는 방식으로 바꿔볼게요!

level,
playTime
};
localStorage.setItem(`game_${Date.now()}`, JSON.stringify(gameData));
Copy link

Choose a reason for hiding this comment

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

Suggested change
localStorage.setItem(`game_${Date.now()}`, JSON.stringify(gameData));
const data = JSON.parse(localStorage.getItem('gameRecordsData')) || [];
data.push(gameData);
localStorage.setItem('gameRecords', JSON.stringify(data));

이런식으로 수정해서 gameRecordsData라는 단일 키로 기록을 관리하면 코드가 더 깔끔해질 거예요 !

<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
Copy link

Choose a reason for hiding this comment

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

검색 엔진 최적화를 위해 lang과 더불어서 title도 설정해주시는거 잊지마세요 !!
저도 자주 깜빡해서 항상 프로젝트 세팅하면 index.html부터 건드리고 시작합니다 ,, ㅎ

if (currentNumber === 18) {
setIsRunning(false);
stopGame();
alert(`Game Over! 걸린 시간 : ${playTime}`);
Copy link

Choose a reason for hiding this comment

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

Suggested change
alert(`Game Over! 걸린 시간 : ${playTime}`);
alert(`Game Over! 걸린 시간 : ${playTime.toFixed(2)}`);

이렇게 하면 임시적으로 헤더에 떠있는 시간과, alert창에 뜨는 시간을 맞출 수 있을 거예요.

<option value="3">Level 3</option>
</select>
{/* 타이머로 타이머 돌아가는지랑, 설정된 시간 보내줘야함*/}
<Timer isRunning={isRunning} onTimeUpdate={handleTimeUpdate} playTime={playTime} />
Copy link

Choose a reason for hiding this comment

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

헤더와 alert에서 시간이 다르게 뜨는 근본적인 이유는 Timer 컴포넌트와 App.jsx의 시간과 관련된 상태관리 방식에서 발생하는 미세한 차이 때문인 것 같아요.

Timer컴포넌트에서 시간을 측정하기 시작하고, App.jsx에서 해당 시간을 props로 넘겨받아 측정하는 과정에서 미세한 오차가 생겨 시간이 다르게 표시되고 있다고 보입니다 ...

중앙화된 로직으로 playTime을 App.jsx(부모) 에서 Timer, Game(자식) 컴포넌트로 넘겨주는 방식으로 수정하면 해결 가능하다고 생각해요 !

Comment on lines +34 to +67
const handleNumberClick = (num, index) => {
if (num === currentNumber) {
//1 클릭하면 타이머 시작
if (currentNumber === 1) {
setIsRunning(true);
startGame();
}

const updatedNumbers = [...numbers];

// 1~9를 순서대로 클릭하는 중이라면, 사용 가능한 숫자 중 하나를 랜덤으로 선택하여 대체
if (currentNumber <= 9) {
const nextNumber = availableNumbers[0]; // 사용 가능한 숫자 중 첫 번째 숫자 선택
updatedNumbers[index] = nextNumber;
setAvailableNumbers(availableNumbers.slice(1)); // 사용된 숫자 제거
setNumbers(updatedNumbers);
}
// 10~18을 순서대로 클릭하는 중이라면, 클릭한 위치를 빈칸으로 변경
else if (currentNumber > 9) {
updatedNumbers[index] = null; // 빈칸으로 설정
setNumbers(updatedNumbers);
}

setCurrentNumber(currentNumber + 1); // 다음 숫자로 업데이트

// 18까지 모두 클릭한 경우 게임 종료
if (currentNumber === 18) {
setIsRunning(false);
stopGame();
alert(`Game Over! 걸린 시간 : ${playTime}`);
resetGame();
}
}
};
Copy link

Choose a reason for hiding this comment

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

handleNumberClick 함수가 너무 많은 역할을 담당하고 있어요.
함수는 SRP원칙에 맞게 최대한 하나의 역할을 수행하도록 설계하는게 좋아요 ! (가독성, 유지보수성, ... 등등의 이유로)

  1. 타이머 시작 로직
  2. 클릭한 숫자에 따라 다음 숫자를 업데이트하는 로직
  3. 게임을 종료하는 로직

이런 방식으로 분리해나갈 수 있을 것 같아요 !

Comment on lines +18 to +21
useEffect(() => {
setNumbers(shuffleArray([1, 2, 3, 4, 5, 6, 7, 8, 9]));
setAvailableNumbers(shuffleArray([10, 11, 12, 13, 14, 15, 16, 17, 18]));
}, []);
Copy link

Choose a reason for hiding this comment

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

Suggested change
useEffect(() => {
setNumbers(shuffleArray([1, 2, 3, 4, 5, 6, 7, 8, 9]));
setAvailableNumbers(shuffleArray([10, 11, 12, 13, 14, 15, 16, 17, 18]));
}, []);
useEffect(() => {
setNumbers(shuffleArray(Array.from({ length: 9 }, (_, i) => i + 1)));
setAvailableNumbers(shuffleArray(Array.from({ length: 9 }, (_, i) => i + 10)));
}, []);

이런식으로 동적 배열을 생성하면 숫자를 반복적으로 나열하지 않아도 배열을 생성할 수 있어요 !

Comment on lines +52 to +60
else if (currentNumber > 9) {
updatedNumbers[index] = null; // 빈칸으로 설정
setNumbers(updatedNumbers);
}

setCurrentNumber(currentNumber + 1); // 다음 숫자로 업데이트

// 18까지 모두 클릭한 경우 게임 종료
if (currentNumber === 18) {
Copy link

Choose a reason for hiding this comment

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

9, 18 등의 매직넘버는 상수로 관리해주는게 좋아요.
코드에 의미를 담을 수 있고, 유지보수하기도 용이합니다 !

예를 들어, 레벨기능을 구현한다고 하면 9, 18이 아니라
25, 50 이런식으로 변하게 되고 해당 숫자를 하나하나 찾아서 리팩토링하다보면 실수가 발생할 수 있어요.

처음에 설계할 때 부터 상수로 관리해주는게 유지보수, 가독성 측면에서 더 유리해요 ! 😃

추가적으로 레벨에 맞게 상수를 설정해두면, 레벨 기능 구현하는데 감이 좀 잡힐거라고 생각합니다 ...

export const GAME_LEVEL = {
  LEVEL_1: {
    LEVEL: 1,
    START_NUM: 1,
    END_NUM: 18,
    GRID_SIZE: 3,
  },
  LEVEL_2: {
    LEVEL: 2,
    START_NUM: 1,
    END_NUM: 32,
    GRID_SIZE: 4,
  },
  LEVEL_3: {
    LEVEL: 3,
    START_NUM: 1,
    END_NUM: 50,
    GRID_SIZE: 5,
  },
};

이런식으로 구성하고, App.jsx에서 레벨에 맞게 해당 상수들을 넘겨주면 레벨 기능 구현할 수 있을 거예요 !!

Comment on lines +60 to +82
header{
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #16423C;
padding: 1rem 6rem;
width: 100%;
box-sizing: border-box;
}

.HeaderLeft{
display: flex;
flex-direction: row;
gap: 1rem;
font-size: 1.6rem;
color: white;
}

.HeaderRight{
display: flex;
flex-direction: row;
gap: 1rem;
}
Copy link

Choose a reason for hiding this comment

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

reset.css와 , App.jsx의 스타일링을 관리하는 코드는 따로 분리해서 관리해주는게 좋아요 !

Comment on lines +5 to +11
.GameWrapper{
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem;
height: 100vh;
}
Copy link

Choose a reason for hiding this comment

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

헤더와 컨텐츠들이 분리되어있고,
컨텐츠의 높이를 100vh로 화면 전체로 설정하면 헤더 높이 때문에 실제로 화면에 맞게 높이가 설정이 안돼요. 그러면 스크롤이 생기게 됩니다.

height에서 헤더 높이만큼 빼준다면 스크롤 문제를 해결할 수 있을 거예요!

Comment on lines +47 to +62
<header>
<div className='HeaderLeft'>
<h1>1 to 50</h1>
<button onClick={() => handleViewChange('game')}>게임</button>
<button onClick={() => handleViewChange('ranking')}>랭킹</button>
</div>
<div className='HeaderRight' style={{ display: view === 'game' ? 'flex' : 'none' }} >
<select name="level" id="level">
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
</select>
{/* 타이머로 타이머 돌아가는지랑, 설정된 시간 보내줘야함*/}
<Timer isRunning={isRunning} onTimeUpdate={handleTimeUpdate} playTime={playTime} />
</div>
</header>
Copy link

Choose a reason for hiding this comment

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

헤더도 컴포넌트로 분리해주면 더 좋을 것 같아요 😀

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