Skip to content

Conversation

@huniversal
Copy link
Collaborator

@huniversal huniversal commented Nov 11, 2025

💡 기본 과제

  • Context API, 전역상태 라이브러리 사용 X (ThemeProvider 제외)
  • CSS 라이브러리 사용하기
  • 외부 UI 라이브러리 없이 직접 컴포넌트를 구현합니다.
  • UI 디자인 및 레이아웃은 아래 요구사항을 충족하는 범위에서 자유롭게 구성
  1. 헤더

    • 헤더에는 제목과 2개의 탭(게임, 랭킹)이 위치한다.
    • 탭을 클릭하면 각 탭에 맞는 화면을 렌더링한다. (라우팅 X, URL은 안 바뀜)
  2. 숫자 카드 짝 맞추기 게임

    • 기본 보드 크기는 4 x 4로 고정한다.
    • 게임 시작 시 무작위로 섞인 덱을 생성한다. (예시 코드의 buildDeck 사용 가능)
    • 제한 시간 내 모든 짝을 맞추면 승리한다. (기본 제한 시간 45초로 가정)
    • 승리 시 축하 메시지, 제한 시간 만료 시 패배 메시지를 표시한다.
    • 승리 또는 패배 시 3초 후 게임을 초기화한다. (보드, 선택 상태, 타이머 초기화)

    2.1 게임 보드

    • 모든 카드는 시작 시 뒷면이며, 위치는 매 게임마다 랜덤이다.
    • 카드를 클릭하면 앞면으로 뒤집힌다.
    • 동시에 뒤집을 수 있는 카드는 최대 두 장이다.
    • 두 장의 숫자가 같으면 매치로 처리되어 열린 상태로 유지된다.
    • 숫자가 다르면 잠시 후(예. 700ms) 두 장 모두 뒷면으로 돌아간다.
    • 모든 쌍을 맞추면 즉시 게임 종료 처리한다.

    2.2 게임 진행 상황

    • 남은 시간과 맞춘 쌍 수를 표시한다.
    • 뒤집은 카드 쌍의 히스토리를 최근순으로 출력한다. (예. 3,7 → 실패 / 4,4 → 성공)
    • 게임이 초기화되면 리스트도 초기화된다.
  3. 랭킹 기능

    • localStorage를 사용해 클리어 기록을 저장한다. (성공한 판만 저장)
    • 저장 항목에는 현재 시각, 레벨, 클리어 시간이 포함된다. (클리어 시간은 소수점 둘째 자리까지)
    • 기본 정렬은 클리어 시간 오름차순이다. (빠른 기록이 위)
    • 초기화 버튼을 누르면 랭킹 보드와 localStorage가 초기화된다.

🔥 심화 과제

  1. 게임 레벨 기능

    • 레벨 선택 기능을 추가한다. (UI는 자유)
      Level 1: 4 x 4 8쌍 제한 시간 45초
      Level 2: 4 x 6 12쌍 제한 시간 60초
      Level 3: 6 x 6 18쌍 제한 시간 100초
    • 게임 리셋 버튼을 통해 즉시 초기화한다.
  2. 안내 메시지

    • 유효하지 않은 동작에 대한 안내 메시지가 출력된다. (예. 이미 선택한 카드 클릭)
  3. 시각 효과 추가

    • 카드 뒤집기와 매치 성공 시 시각적 효과를 추가한다. (예. flip 애니메이션, 매치 하이라이트)
    • 게임 종료 시 alert 대신 React의 createPortal로 Modal을 구현한다.
      참고: https://ko.react.dev/reference/react-dom/createPortal
  4. 랭킹 정렬 기능

    • 레벨 내림차순과 클리어 시간 오름차순으로 정렬한다.
      높은 레벨이 위쪽, 같은 레벨에서는 빠른 시간이 위쪽으로 정렬

🔧 구현 요약 및 새로 배운 점

폴더 구조

src/
├── components/
│   ├── Board/
│   │   └── Board.jsx
│   ├── Card/
│   │   └── Card.jsx
│   ├── Container/
│   │   └── Container.jsx
│   ├── Game/
│   │   └── Game.jsx
│   ├── GameHistory/
│   │   └── GameHistory.jsx
│   ├── GameStatus/
│   │   └── GameStatus.jsx
│   ├── Header/
│   │   └── Header.jsx
│   └── Ranking/
│       └── Ranking.jsx
│
├── utils/
│   ├── deckUtils.js
│   └── rankingUtils.js
│
├── App.jsx
├── index.css
└── main.jsx

CSS 라이브러리 선정

이번에 TailwindCSS를 사용하여 스타일링을 구현하였습니다!
초기 설정에서 Emotion CSS-in-JS 라이브러리를 사용하려 했으나,
패키지 관리자와 의존성 충돌 문제가 지속적으로 발생하여 빌드 환경 안정화에 어려움을 겪었습니다.

커스텀 변수 설정 구조

export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      // 1. 색상
      colors: {
        "primary-600": "#0A70EB",
        "primary-500": "#188CFF",
        "primary-400": "#28A6FF",
        "primary-300": "#50C4FF",
        "primary-200": "#B9E7FF",
        "primary-100": "#D7F1FF",
        "bg-white": "#F7F9FF",
        black: "#232323",
        white: "#FFFFFF",
      },
... 코드 일부분
  • tailwind.config.js 파일의 theme.extend 섹션에 모든 기본 변수를 정의했습니다.
  • 이 설정을 통해 CSS 변수 대신 bg-primary-500 또는 p-large와 같은 의미 있는 Tailwind 클래스를 JSX에서 직접 사용할 수 있게 되었습니다.

Card.jsx 동적 클래스 배열 패턴

  const cardClasses = [
    "relative",
    "transition-transform",
    "duration-300",
    "transform",
    "cursor-pointer",
    "shadow-md",
    "rounded-medium", 
    "text-3xl",
    "flex items-center justify-center",
    "aspect-square", // 카드를 정사각형으로 유지
  ];

  // 1. 매치된 카드 스타일
  if (isMatched) {
    cardClasses.push("bg-green-300", "shadow-none", "opacity-70");
  }
  // 2. 뒤집힌 카드 스타일
  else if (isFlipped) {
    cardClasses.push("bg-white", "shadow-xl");
  }
  // 3. 뒷면 카드 스타일
  else {
    cardClasses.push("bg-primary-500", "hover:bg-primary-400"); 
  }

card 컴포넌트에서 카드 상태에 따라 스타일이 조건부로 바뀌는 로직을 구현하기 위해 클래스 배열 구성 패턴을 사용하였습니다.

  • 패턴 개요 : 변경되지 않은 basic 스타일을 먼저 정의하고, if/else 구문을 통해 컴포넌트의 상테에 따라 클래스의 스타일을 배열에 .push하여 추가했습니다.
  • 해당 조건부 스타일링을 통해 복잡한 삼항 연산자의 가독성을 높일 수 있었습니다.

utils 게임 알고리즘 로직 분리

  • 과제의 핵심 로직을 분리하여 utils 폴더에 모았습니다.

1. deckUtils.js (게임 덱 생성 알고리즘)

  • 역할 : 게임 시작에 필요한 카드 덱 생성
  • 핵심 기능 :Fisher-Yates Shuffle 알고리즘을 사용하여 배열을 랜덤으로 섞음
  • 장점 : 해당 로직 분리를 바탕으로 Game.jsx에서는 해당 함수를 호출하기만 하면 되기 때문에 코드 가독성 향상

2.rankingUtils.js (랭킹 기록 관리 로직)

  • 역할 : 게임 기록 관리 및 localStroage에 저장 및 로드
  • 핵심 기능:
    • 새로운 기록을 받아 localStroage에 추가
    • 기존 목록을 오름차순으로 정렬
    • 랭킹 초기화 버튼 클릭시 localStroage 초기화
/*
 * 새로운 클리어 기록을 저장하고 랭킹을 다시 정렬
*/
export const saveRanking = (record) => {
  // 1. 기존 랭킹 목록을 불러오기
  const rankings = getRankings();

  // 2. 현재 시각을 한국 표준 시간 포맷으로 생성

    // 3. 새로운 기록 객체를 생성하고 타임스탬프를 추가
    const newRecord = {
      ...record, // clearTime, level 등 전달된 모든 속성을 복사
      timestamp: timestamp, // 현재 기록 시각 추가
    }

    // 4. 새로운 기록을 기존 랭킹 배열의 끝에 추가
    rankings.push(newRecord);

    // 5. 랭킹 목록을 클리어 시간 오름차순으로 다시 정렬
    // 클리어 시간(clearTime)이 빠른 기록이 배열의 앞으로 오도록 함
    rankings.sort((a, b) => a.clearTime - b.clearTime);

    // 6. 갱신된 랭킹 목록을 localStorage에 JSON 문자열로 저장합니다.
    try {
      localStorage.setItem(RANKING_KEY, JSON.stringify(rankings));
    } catch (err) {
      console.error("error", err);
    }
}
  • 데이터 로드 및 결합 : getRankings()를 통해 현재 기록을 불러옴
  • 정렬 : 새로운 기록을 추가한 뒤 ranking.sort()를 통해 오름차순 정렬
  • 덮어쓰기 : 정렬된 전체 배열을 localStroage에 덮어쓰기로 저장

Game.jsx

  • Game.jsx는 모든 게임 상태를 책임지고 관리하는 컴포넌트로
    • 타이머 관리
    • 매치 팥정
    • 히스토리 기록
    • 랭킹 저장
      등을 제어하는 역할을 수행하고 있습니다.

아키텍쳐 설계 방식

  • useCallbackuseEffect를 활용해서 게임 로직과 UI를 분리하는 방식을 사용하였습니다.

비동기 타이머 및 상태 관리

  • 타이머는 게임에서 모든 충돌을 유발할 수 있는 비동기 요소이기 떄문에 이를 안정화하기 위해
    timerRef.current를 통해 타이머의 존재 여부를 체크 및 setInterval을 사용하여 한번만 설정하도록 하였습니다.
  • useRef를 통해 DOM 외부에서 비동기 작업을 안전하기 진행하였습니다.

useEffect 분리를 통한 역할 부여

단일 useEffect으로 로직을 구현하다 보니 코드의 복잡성이 너무 높아져
각 역할에 따라 3개의 독립적인 useEffect를 사용하였습니다.

  • 매치 판정 : 카드가 뒤집혔을때 카드 배열의 변화에만 반응하여 매치 판정을 진행합니다.
  • 히스토리 기록 저장 : 매치 판정 직후 setHistory를 호출하여 배열에 새로운 기록을 추가하고 GameStatus에 반영합니다.

랭킹 및 데이터

  • IsGameWontrue인 경우 시간을 계산하여 rankingUtils.jssaveRanking 함수를 호출

Header 및 페이지 뷰 전환

  • URL 라우팅을 사용하지 않고 React의 상태 관리를 통해 뷰를 전환하는 방식으로 구현하였습니다.

상태 기반 뷰 제어

  • 최상위 컴포넌트(App.jsx)에서 activeTab 상태 관리를 통해 상태를 정의하였습니다.
  const [activeTab, setActiveTab] = useState("Game");
  • 이 상태는 현재 사용자에게 Game 컴포넌트를 보여줄지, Ranking를 보여줄지 결정하는 플래그 역할을 합니다.
2025-11-11.14.28.24.mov

Header 제어

header 컴포넌트는 activeTab의 상태를 props로 받아 뷰 전환에 사용합니다.

조건부 렌더링

최상위 컴포넌트에서 activeTab 상태 업데이트를 감지하여 전환

const renderContent = () => {
    if (activeTab === 'Game') return <Game />;
    if (activeTab === 'Ranking') return <Ranking />;
    return null;
};

GameStatus 컴포넌트

해당 컴포넌트는 게임의 모든 실시간 정보를 통합하여 사용자에게 정보를 제공하는 사이드 패널 컴포넌트입니다.
Game.jsx에서 Props를 받아 UI를 갱신하는 방식으로 구현하였습니다.

      <div className="w-full mt-6 md:w-2/5 lg:w-2/4 md:ml-6 md:mt-0">
        <GameStatus
          time={time}
          challenge={challenge}
          matchedPairs={matchedPairs}
          totalPairs={totalPairs}
          level={level}
          setLevel={setLevel}
          isGameStarted={isGameStarted}
          isGameOver={isGameOver}
          infoMessage={infoMessage}
          history = {history}
        />
      </div>

StatusBox UI 컴포넌트 사용

const StatusBox = ({ title, value, unit = "" }) => (
  <div className="p-2 border rounded-large bg-primary-100">
    <p className="mb-1 text-base font-medium text-gray-600">{title}</p>
    <p className="text-gray-800 text-h3">
      {value}
      {unit && <span className="ml-1 text-sm font-normal">{unit}</span>}
    </p>
  </div>
);

컴포넌트 내에서 정보를 표시하기 위해 분리된 재사용 가능한 UI 컴포넌트를 구현하였습니다.

😀 그럼 왜 해당 UI 컴포넌트를 따로 파일로 분리하지 않았는가??
-> 해당 컴포넌트는 GameStatus 외에 재사용하지 않기 때문에 오히려 불필요한 계층이 생긴다고 판단하였습니다.

정보 표시 및 상태 동기화

  • 역할 분리 : Game.jsx에서 계산된 time, challenge, matchedPairs 등을 전달받아 표시하는 역할 수행

  • 상태를 기반으로 게임 종료 및 시작에 대한 안내 메시지를 최우선으로 반환

  const getMessage = useCallback(() => {
    if (isGameOver) {
      if (matchedPairs === totalPairs) {
        return "게임 종료: 승리!";
      }
      return "게임 종료: 패배!";
    }
    if (!isGameStarted) {
      return "카드를 눌러 게임을 시작";
    }
    return infoMessage;
  }, [isGameOver, isGameStarted, matchedPairs, totalPairs, infoMessage]);

안내 메시지 로직

getMessage 함수는 useCallback으로 메모이제이션되어 있으며 게임 진행 상황에 맞게 메시지를 표시해줍니다.

2025-11-11.14.55.38.mov

Ranking.jsx 랭킹 시스템

2025-11-11.15.04.09.mov

데이터 관리

  • 비즈니스 로직 분리 : 데이터 로드, 저장, 삭제 로직은 utils/rankingUtils.js에 분리하였습니다.
  • 데이터 로드 : getRankings 함수를 사용하여 loacalStroage에서 데이터를 불러와 오름차순으로 정렬하였습니다.
  • 저장 : 게임 승리 시 saveRankgin를 호출하여 클리어 시간, 레벨, 시간 정보를 저장합니다.

컴포넌트 상태 관리

  • loadRankings : 컴포넌트가 마운트될 떄 함수를 호출하여 초기화
  • handleDelete : '초기화' 버튼을 클릭하여 유저의 확인 후 deleteRanking 함수를 호출하여 localStroage를 비우고 loadRanking를 호출하여 UI를 업데이트합니다.

게임 실행

2025-11-11.15.05.19.mov

🥲 구현 과정에서 어려웠던 & 고민했던 부분

  • 우선 전반적으로 과제가 어려웠어요 팟장님

타이머 재시작 실패 및 충돌

  • 타이머가 리셋 후 재시작하지 않는 이슈가 있었습니다. startGame 함수 내부에서 setIsGameStated(true)와 setInterval()이 거의 동시에 실행하면서 상태가 업데이트 되면서 리렌더링이 setInterval의 콜백함수 실행을 중단시키는 이슈가 있었습니다.
    그래서 해당 이슈를 해결하기 위해 타이머의 실행 환경을 독립적으로 구분하여 의존성을 낮췄습니다.

🔭 리뷰 요청 포인트 & 질문

  • 우선 폴더 구조가 올바른지 궁금합니다... 게임 로직이 Game.jsx 한 파일에 모두 들어가있는데 다른 분들은 어떻게 구현하였는지 궁금합니다
  • 그리고 게임 덱 랜덤 알고리즘을 혹시 다른 방식으로 구현하신 천재 개발자가 있는지 궁금합니다.
  • 저는 일단 TailwindCSS를 사용하였는데 다른 라이브러리를 사용하신 분들은 어떤 장점이 있어서 사용하였는지 궁금합니다.

@huniversal huniversal requested a review from eojindesu November 11, 2025 06:16
@huniversal huniversal requested a review from mimizae November 11, 2025 06:16
@huniversal huniversal self-assigned this Nov 11, 2025
Copy link

@mimizae mimizae left a 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> */}
Copy link

Choose a reason for hiding this comment

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

요 주석은 어떤 의미인가요??!!

Copy link

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 = () => {
// ------------------- 게임 상태 관리 -------------------
//-------------------------------------------------------
Copy link

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: 레벨 드롭다운을 클릭했을 때 변경되는 로직 구현
Copy link

Choose a reason for hiding this comment

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

이런 TODO로 되어있는 것들 리팩토링 기간 때 적용 하면 참 맛있겠다 😋

Copy link

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`;
}
};
Copy link

Choose a reason for hiding this comment

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

getTabClass로 버튼 스타일 로직을 공통 함수로 분리한 부분 정말 좋네요!!
activeTab 상태에 따라 스타일을 일일이 조건부로 쓰는 대신, 이렇게 베이스 클래스와 상태별 클래스를 합치는 구조가 재사용성도 높고 가독성도 좋은 것 같아요 🤩

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
Copy link

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",
Copy link

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", // 카드를 정사각형으로 유지
];
Copy link

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); // 시도 횟수
Copy link

Choose a reason for hiding this comment

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

오 기본 과제 명세랑 조금 다른 것 같은데 의도하신 걸까요??

Copy link

@eojindesu eojindesu left a 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)`,
};

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(" : ");

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]);

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`;
}
};

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.

색상, 타이포그래피, 여백 등 핵심 토큰들을 체계적으로 정의해주셔서 재사용성이랑 유지보수성 측면에서 너무 좋은 것 같아요 !!! 많이 배우고 갑니다✍🏻✍🏻

Tailwind는 유틸리티 퍼스트 방식의 CSS 프레임워크라 간편하고 일관된 스타일 적용이 가능하다는 점이 큰 장점인 것 같아요~~
그리고 Vanilla Extract을 사용했는데 토큰 관리나 타입 안정성 측면에 강점이 있어서 현재 설정해두신 스타일 토큰 구조를 좀 더 시스템적으로 확장할 때 도움이 될 수 있어 나중에 고려해보셔도 좋을 것 같아요!

Choose a reason for hiding this comment

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

전체 게임 로직이 Game.jsx에 모두 들어있다 보니 상태, 타이머, 매치 판정 로직 등이 한 파일에서 관리되고 있는데요! 추후 유지보수성을 위해 일부 로직 (ex. useTimer) 을 커스텀 훅으로 분리해도 좋을 것 같아요🤩🤩

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.

4 participants