-
Notifications
You must be signed in to change notification settings - Fork 0
[3주차 기본 과제] 1 to 50 게임 #5
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
base: main
Are you sure you want to change the base?
Conversation
| for (let i = 0; i < localStorage.length; i++) { | ||
| const gameDataItem = JSON.parse(localStorage.getItem(localStorage.key(i))); | ||
| gameData.push(gameDataItem); | ||
| } |
There was a problem hiding this comment.
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); | ||
| }; | ||
|
|
There was a problem hiding this comment.
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} // 빈칸은 클릭 비활성화 |
There was a problem hiding this comment.
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"> | |||
There was a problem hiding this comment.
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 (일단 심화과제 할 수 있으면 하기) |
There was a problem hiding this comment.
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); /* 살짝 눌리는 효과 */ |
There was a problem hiding this comment.
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> | ||
| )) | ||
| // 아니라면 저장된 랭킹 없지롱~ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p5: 랭킹이 없을 때도 신경 써주시구... 섬세해요👍😊
|
안녕하세요 :) |
m2na7
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
과제 고생하셨어요 !! 😊
폴더 구조도 깔끔하고, App.jsx에서 컴포넌트들을 조립하는 방식으로 리액트답게 잘 구현하셨네요 👍
고민하신 부분은 코멘트에 남겨두었으니 해결해보시고 의견 남겨주세요 !!
| 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); | ||
| }, []); |
There was a problem hiding this comment.
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와 같은 단일 키에 배열로 저장하면, 각 기록을 조회하거나 새로운 기록을 추가할 때 효율적으로 처리할 수 있어요 !
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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> |
There was a problem hiding this comment.
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}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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} /> |
There was a problem hiding this comment.
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(자식) 컴포넌트로 넘겨주는 방식으로 수정하면 해결 가능하다고 생각해요 !
| 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(); | ||
| } | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleNumberClick 함수가 너무 많은 역할을 담당하고 있어요.
함수는 SRP원칙에 맞게 최대한 하나의 역할을 수행하도록 설계하는게 좋아요 ! (가독성, 유지보수성, ... 등등의 이유로)
- 타이머 시작 로직
- 클릭한 숫자에 따라 다음 숫자를 업데이트하는 로직
- 게임을 종료하는 로직
이런 방식으로 분리해나갈 수 있을 것 같아요 !
| useEffect(() => { | ||
| setNumbers(shuffleArray([1, 2, 3, 4, 5, 6, 7, 8, 9])); | ||
| setAvailableNumbers(shuffleArray([10, 11, 12, 13, 14, 15, 16, 17, 18])); | ||
| }, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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))); | |
| }, []); |
이런식으로 동적 배열을 생성하면 숫자를 반복적으로 나열하지 않아도 배열을 생성할 수 있어요 !
| else if (currentNumber > 9) { | ||
| updatedNumbers[index] = null; // 빈칸으로 설정 | ||
| setNumbers(updatedNumbers); | ||
| } | ||
|
|
||
| setCurrentNumber(currentNumber + 1); // 다음 숫자로 업데이트 | ||
|
|
||
| // 18까지 모두 클릭한 경우 게임 종료 | ||
| if (currentNumber === 18) { |
There was a problem hiding this comment.
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에서 레벨에 맞게 해당 상수들을 넘겨주면 레벨 기능 구현할 수 있을 거예요 !!
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reset.css와 , App.jsx의 스타일링을 관리하는 코드는 따로 분리해서 관리해주는게 좋아요 !
| .GameWrapper{ | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| padding: 3rem; | ||
| height: 100vh; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헤더와 컨텐츠들이 분리되어있고,
컨텐츠의 높이를 100vh로 화면 전체로 설정하면 헤더 높이 때문에 실제로 화면에 맞게 높이가 설정이 안돼요. 그러면 스크롤이 생기게 됩니다.
height에서 헤더 높이만큼 빼준다면 스크롤 문제를 해결할 수 있을 거예요!
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헤더도 컴포넌트로 분리해주면 더 좋을 것 같아요 😀
✨ 구현 기능 명세
💡 기본 과제
18까지 클릭해야한다면, 처음에는 19까지의 숫자가 랜덤으로 보여짐현재 시각,게임의 레벨,플레이 시간3개의 정보를 localStorage에 저장 (랭킹에서 사용)🔥 심화 과제
Level 1:
3 x 3, Level 2:4 x 4, Level 3:5 x 5createPortal
공유과제
제목:
링크 첨부 :
❗️ 내가 새로 알게 된 점
sort()활용하기❓ 구현 과정에서의 어려웠던/고민했던 부분
🥲 소요 시간
35h🖼️ 구현 결과물
20241108_025823.mp4