-
Notifications
You must be signed in to change notification settings - Fork 0
[3주차 과제] 숫자 카드 짝 맞추기 게임 #6
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
mimizae
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.
3주차 과제... 정말 난해한 과제였죠... 수고 많으셨습니다 😂...!!!!!
리뷰 요청 달아주신 Game.tsx에 대해서는 아래에 코멘트 남겨두었고, 폴더 구조는 지금 깔끔하니 좋은 것 같아요 ㅎㅎㅎ
(tailwind의 장점... 파일이 많아지지 않는다... 저는 이번 과제에 css.ts 파일이 컴포넌트마다 추가되다 보니 폴더 depth가 너무 깊어지는 걸 방어하는 데 신경을 썼거든요... ㅜㅜ)
이번 리팩토링 기간에는 TODO: 라고 적어주신 부분 (+ 심화 ㅎㅎ) 계속 더 진행해 보셔도 좋을 것 같습니다 ㅎㅎ 수고 많으셨어요!
| if (activeTab === "Game") { | ||
| return ( | ||
| <div className="p-6 border rounded-large bg-primary-100 min-h-[700px] w-full"> | ||
| {/* <h2 className="text-xl font-semibold">게임 탭</h2> */} |
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.
요 주석은 어떤 의미인가요??!!
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.
색상, 간격, 폰트 등을 재사용 가능한 토큰 형태로 정의한 것 너무너무 좋네요!!!! 👍🏻
혹시 base: "16px", // --font-size-base -> base라서 px으로 설정하신 걸까요??
|
|
||
| const Game = () => { | ||
| // ------------------- 게임 상태 관리 ------------------- | ||
| //------------------------------------------------------- |
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.
가독성을 위해, 요런 주석은 삭제하는 게 좋을 것 같습니다!!!
이 파일은 거의 모든 변수마다 주석으로 설명이 추가되었고 이 파일 외, 전반적으로 주석이 꽤 보이는 것 같아요!!
이미 명확한 변수명들을 사용하고 계시고, 좀 더 모호하다면 길더라도 정확한 변수명 또는 함수명 사용하는 방식으로 주석 최소화 리팩토링 하면 좋을 것 같아요 👍🏻
|
|
||
| const handleLevelChange = () => { | ||
| // const newLevel = parseInt(e.target.value, 10); | ||
| // TODO: 레벨 드롭다운을 클릭했을 때 변경되는 로직 구현 |
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.
이런 TODO로 되어있는 것들 리팩토링 기간 때 적용 하면 참 맛있겠다 😋
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.
하나의 파일에 기능이랑 UI가 같이 있어서 읽을 때 살짝 복잡하게 느껴지는 것 같아요 🥲
저도 처음에는 이번 과제가 라우팅 없이, 상태 기반 렌더링이라 중앙 제어가 필요한 경우니까 Game.tsx의 코드가 한 없이 길어졌었는데, 기능을 커스텀 훅으로 나누고 컴포넌트는 UI를 담당하게 해서 명확하게 역할 분담을 했습니다
그러니 확실히 코드 수가 줄고 깔끔해 지긴 하더라고요!
커스텀 훅을 이런 용도로 써도 되나? 하는 의문이 들었지만 재사용성이 크지 않더라도 기능 단위로 역할을 명확히 분리한다는 점에서 의미가 큰 것 같아요.
(로직의 응집도를 높이기 위한 훅이라면 충분히 쓸 가치 있다는 의미...)
이번 리팩토링 기간 때 커스텀 훅을 이용해서 주요 기능을 분리해 보면 어떨까요?? 🔥
| } else { | ||
| return `${baseClasses} bg-white text-gray-500`; | ||
| } | ||
| }; |
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.
getTabClass로 버튼 스타일 로직을 공통 함수로 분리한 부분 정말 좋네요!!
activeTab 상태에 따라 스타일을 일일이 조건부로 쓰는 대신, 이렇게 베이스 클래스와 상태별 클래스를 합치는 구조가 재사용성도 높고 가독성도 좋은 것 같아요 🤩
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.
저도 !!! 민재님 의견에 동참해요 탭마다 스타일을 직접 넣지 않고 공통 함수로 분리해 관리하신 점이 정말 좋네요🙆🏻♀️🙆🏻♀️ 확장성까지 고려된 구조라 유지보수도 훨씬 수월해질 것 같아요
| @@ -1,2 +1,3 @@ | |||
| .history | |||
| .DS_Store | |||
| .DS_Store | |||
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.
오잉 왜 동일한 게 추가되었나요??
| "eslint-plugin-react-refresh": "^0.4.22", | ||
| "globals": "^16.4.0", | ||
| "postcss": "^8.5.6", | ||
| "tailwindcss": "^3.4.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.
안녕 테일윈드...//
| "text-3xl", | ||
| "flex items-center justify-center", | ||
| "aspect-square", // 카드를 정사각형으로 유지 | ||
| ]; |
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.
저는 tailwind로 조건부 스타일링 하면 clsx만 활용해 봤는데, 카드 상태별로 스타일을 cardClasses 배열로 묶어서 관리하고 조건부로 push하는 구조는 처음 본 것 같아요...!!!! 클래스가 길어지는 게 딱 방지 되어서 가독성도 살리고 명확해서 유지보수하기 좋은 구조 같아요 ㄷㄷㄷㄷ
| // 배열의 길이가 2가 되면 매치 판정 시작 | ||
|
|
||
| const [time, setTime] = useState(TIME_LIMIT); | ||
| const [challenge, setChallenge] = useState(0); // 시도 횟수 |
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.
오 기본 과제 명세랑 조금 다른 것 같은데 의도하신 걸까요??
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.
3주차 과제 하시느라 수고 많으셨습니다!!
저도 정말 쉽지 않은.. 과제였는데🥲 꼼꼼한 주석도 그렇고 세심한 부분까지 고민하고 신경 쓴 코드인 것 같아요 덕분에 많이 배워갑니다🙌🏻🙌🏻
기능 단위로 커밋을 잘 쪼개주셔서 작업 과정이 한 눈에 보이는 점이랑 심화로 구현할 기능은 TODO로 작성해두신 점도 좋았습니다ㅎㅎ 리뷰 요청 포인트에 관한 저의 의견은 코드에서 말씀드릴게요! 고생 많으셨습니다~~
| const gridStyle = { | ||
| gridTemplateColumns: `repeat(${cols}, 1fr)`, | ||
| }; | ||
|
|
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.
저는 레벨별 그리드 설정을 switch문으로 처리했는데, 이렇게 매핑 객체로 관리하는 방식이 훨씬 직관적이고 확장성도 좋을 것 같네요 !! 배우고 갑니다..🤓
| <ul className="p-0 m-0 space-y-1 text-sm list-none "> | ||
| {history.map((item, index) => { | ||
| const [cardValues, result] = item.message.split(" : "); | ||
|
|
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.
message.split(" : ") 같이 문자열 포맷에 의존하는 방식은 message 형식이 바뀌면 예상치 못한 에러가 발생할 수도 있을 것 같아요..!
필요한 값을 개별 변수 형태로 전달받는 방식도 고려해 보시면 좋을 것 같습니당
| } | ||
| return infoMessage; | ||
| }, [isGameOver, isGameStarted, matchedPairs, totalPairs, infoMessage]); | ||
|
|
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.
안내 메시지 로직을 별도의 getMessage 함수로 분리해 주셔서 가독성이 좋네요👍🏻👍🏻
| } else { | ||
| return `${baseClasses} bg-white text-gray-500`; | ||
| } | ||
| }; |
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.
저도 !!! 민재님 의견에 동참해요 탭마다 스타일을 직접 넣지 않고 공통 함수로 분리해 관리하신 점이 정말 좋네요🙆🏻♀️🙆🏻♀️ 확장성까지 고려된 구조라 유지보수도 훨씬 수월해질 것 같아요
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.
색상, 타이포그래피, 여백 등 핵심 토큰들을 체계적으로 정의해주셔서 재사용성이랑 유지보수성 측면에서 너무 좋은 것 같아요 !!! 많이 배우고 갑니다✍🏻✍🏻
Tailwind는 유틸리티 퍼스트 방식의 CSS 프레임워크라 간편하고 일관된 스타일 적용이 가능하다는 점이 큰 장점인 것 같아요~~
그리고 Vanilla Extract을 사용했는데 토큰 관리나 타입 안정성 측면에 강점이 있어서 현재 설정해두신 스타일 토큰 구조를 좀 더 시스템적으로 확장할 때 도움이 될 수 있어 나중에 고려해보셔도 좋을 것 같아요!
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.
전체 게임 로직이 Game.jsx에 모두 들어있다 보니 상태, 타이머, 매치 판정 로직 등이 한 파일에서 관리되고 있는데요! 추후 유지보수성을 위해 일부 로직 (ex. useTimer) 을 커스텀 훅으로 분리해도 좋을 것 같아요🤩🤩
💡 기본 과제
헤더
숫자 카드 짝 맞추기 게임
4 x 4로 고정한다.buildDeck사용 가능)2.1 게임 보드
2.2 게임 진행 상황
랭킹 기능
현재 시각,레벨,클리어 시간이 포함된다. (클리어 시간은 소수점 둘째 자리까지)클리어 시간오름차순이다. (빠른 기록이 위)🔥 심화 과제
게임 레벨 기능
Level 1:
4 x 48쌍 제한 시간 45초Level 2:
4 x 612쌍 제한 시간 60초Level 3:
6 x 618쌍 제한 시간 100초안내 메시지
시각 효과 추가
참고: https://ko.react.dev/reference/react-dom/createPortal
랭킹 정렬 기능
높은 레벨이 위쪽, 같은 레벨에서는 빠른 시간이 위쪽으로 정렬
🔧 구현 요약 및 새로 배운 점
폴더 구조
CSS 라이브러리 선정
이번에 TailwindCSS를 사용하여 스타일링을 구현하였습니다!
초기 설정에서 Emotion CSS-in-JS 라이브러리를 사용하려 했으나,
패키지 관리자와 의존성 충돌 문제가 지속적으로 발생하여 빌드 환경 안정화에 어려움을 겪었습니다.
커스텀 변수 설정 구조
Card.jsx동적 클래스 배열 패턴card 컴포넌트에서 카드 상태에 따라 스타일이 조건부로 바뀌는 로직을 구현하기 위해 클래스 배열 구성 패턴을 사용하였습니다.
if/else구문을 통해 컴포넌트의 상테에 따라 클래스의 스타일을 배열에.push하여 추가했습니다.utils게임 알고리즘 로직 분리utils폴더에 모았습니다.1.
deckUtils.js(게임 덱 생성 알고리즘)Game.jsx에서는 해당 함수를 호출하기만 하면 되기 때문에 코드 가독성 향상2.
rankingUtils.js(랭킹 기록 관리 로직)getRankings()를 통해 현재 기록을 불러옴ranking.sort()를 통해 오름차순 정렬localStroage에 덮어쓰기로 저장Game.jsx
등을 제어하는 역할을 수행하고 있습니다.
아키텍쳐 설계 방식
useCallback과useEffect를 활용해서 게임 로직과 UI를 분리하는 방식을 사용하였습니다.비동기 타이머 및 상태 관리
timerRef.current를 통해 타이머의 존재 여부를 체크 및setInterval을 사용하여 한번만 설정하도록 하였습니다.useRef를 통해 DOM 외부에서 비동기 작업을 안전하기 진행하였습니다.useEffect 분리를 통한 역할 부여
단일
useEffect으로 로직을 구현하다 보니 코드의 복잡성이 너무 높아져각 역할에 따라 3개의 독립적인
useEffect를 사용하였습니다.setHistory를 호출하여 배열에 새로운 기록을 추가하고GameStatus에 반영합니다.랭킹 및 데이터
IsGameWon이true인 경우 시간을 계산하여rankingUtils.js의saveRanking함수를 호출Header 및 페이지 뷰 전환
상태 기반 뷰 제어
2025-11-11.14.28.24.mov
Header 제어
header컴포넌트는activeTab의 상태를props로 받아 뷰 전환에 사용합니다.조건부 렌더링
최상위 컴포넌트에서
activeTab상태 업데이트를 감지하여 전환GameStatus컴포넌트해당 컴포넌트는 게임의 모든 실시간 정보를 통합하여 사용자에게 정보를 제공하는 사이드 패널 컴포넌트입니다.
Game.jsx에서 Props를 받아 UI를 갱신하는 방식으로 구현하였습니다.StatusBox UI 컴포넌트 사용
컴포넌트 내에서 정보를 표시하기 위해 분리된 재사용 가능한 UI 컴포넌트를 구현하였습니다.
😀 그럼 왜 해당 UI 컴포넌트를 따로 파일로 분리하지 않았는가??
-> 해당 컴포넌트는 GameStatus 외에 재사용하지 않기 때문에 오히려 불필요한 계층이 생긴다고 판단하였습니다.
정보 표시 및 상태 동기화
역할 분리 : Game.jsx에서 계산된 time, challenge, matchedPairs 등을 전달받아 표시하는 역할 수행
상태를 기반으로 게임 종료 및 시작에 대한 안내 메시지를 최우선으로 반환
안내 메시지 로직
getMessage 함수는 useCallback으로 메모이제이션되어 있으며 게임 진행 상황에 맞게 메시지를 표시해줍니다.
2025-11-11.14.55.38.mov
Ranking.jsx랭킹 시스템2025-11-11.15.04.09.mov
데이터 관리
utils/rankingUtils.js에 분리하였습니다.getRankings함수를 사용하여loacalStroage에서 데이터를 불러와 오름차순으로 정렬하였습니다.saveRankgin를 호출하여 클리어 시간, 레벨, 시간 정보를 저장합니다.컴포넌트 상태 관리
deleteRanking함수를 호출하여localStroage를 비우고loadRanking를 호출하여 UI를 업데이트합니다.게임 실행
2025-11-11.15.05.19.mov
🥲 구현 과정에서 어려웠던 & 고민했던 부분
타이머 재시작 실패 및 충돌
그래서 해당 이슈를 해결하기 위해 타이머의 실행 환경을 독립적으로 구분하여 의존성을 낮췄습니다.
🔭 리뷰 요청 포인트 & 질문