Skip to content

[3주차 과제] 두더지 게임 만들기#4

Open
yumin-kim2 wants to merge 24 commits into
mainfrom
Week3/Homework
Open

[3주차 과제] 두더지 게임 만들기#4
yumin-kim2 wants to merge 24 commits into
mainfrom
Week3/Homework

Conversation

@yumin-kim2

@yumin-kim2 yumin-kim2 commented May 1, 2026

Copy link
Copy Markdown
Collaborator

🏡 과제 명세

💡 기본 과제

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

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

    • 기본 보드 크기는 2 x 2로 고정한다.
    • 제한 시간동안 두더지 또는 폭탄을 클릭한다. (기본 제한 시간 15초로 가정)
    • 게임 종료 시, 최종 점수 메시지를 표시한다. (alert 사용)

    2.1 게임 보드

    • 랜덤한 위치에서 두더지 또는 폭탄이 열린다.
    • 두더지를 클릭하면 총 점수에 +1점, 폭탄을 클릭하면 -1점
    • 두더지를 클릭하면 공격받은 이미지로 잠시 변경 후(예. 700ms) 구멍으로 돌아간다.
    • 폭탄을 클릭하면 바로 구멍으로 돌아간다.

    2.2 게임 진행 상황

    • 남은 시간과 총 점수를 표시한다.
    • 게임이 초기화되면 점수와 타이머, 성공/실패 개수가 초기화된다.
  3. 게임 레벨 기능

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

🔥 심화 과제

  1. 게임 레벨 기능
    • 레벨 선택 기능을 추가한다. (UI 위치는 자유)
      Level 1: 2 x 2 제한 시간 15초
      Level 2: 3 x 3 제한 시간 20초
      Level 3: 4 x 4 제한 시간 30초
    • 게임 진행 시에는 level을 변경할 수 없다.
    • '중단' 버튼을 통해 즉시 초기화한다.
  2. 게임 보드의 추가 기능
    • 성공과 실패 개수를 표시한다.
    • 성공/실패 여부를 안내 메시지로 표시한다.
  3. 모달 추가
    • 게임 종료 시 alert 대신 React의 createPortal로 Modal을 구현한다.
  4. 랭킹 페이지
    • 저장 항목에 성공 시간을 추가한다. (레벨, 점수, 성공 시간 총 3개)
    • 레벨 내림차순점수 내림차순으로 정렬한다. (높은 레벨이 위쪽, 같은 레벨에서는 높은 점수가 위쪽으로 정렬)
    • window.confirm() 을 통해 초기화 여부를 한 번 더 묻는다.

공유과제

제목: Virtual DOM이 빠르다는 말, 반은 맞고 반은 틀리다

링크 첨부 : https://velog.io/@novaris/Virtual-DOM%EC%9D%B4-%EB%B9%A0%EB%A5%B4%EB%8B%A4%EB%8A%94-%EB%A7%90-%EB%B0%98%EC%9D%80-%EB%A7%9E%EA%B3%A0-%EB%B0%98%EC%9D%80-%ED%8B%80%EB%A6%AC%EB%8B%A4


✍️ 구현 요약 및 새로 배운 점

이번 과제에서는 Context API나 전역 상태 라이브러리 없이 useState, useEffect, useRef만 사용해서 두더지 게임과 랭킹 기능을 구현했습니다. 스타일링은 emotion의 styled를 사용했고, 폴더 구조는 게임, 랭킹,

폴더 구조

App.jsx에 모든 JSX와 스타일이 모여 있던 구조에서, 화면 역할에 따라 game, ranking 폴더로 분리했습니다!

src/
  App.jsx
  App.styles.jsx
  game/
    Game.jsx
    Game.styles.js
  ranking/
    Ranking.jsx
    Ranking.styles.js
  assets/
  • App.jsx: 탭 전환, 게임 상태, 랭킹 데이터 관리
  • game/Game.jsx: 게임 화면 UI
  • game/Game.styles.js: 게임 화면 스타일
  • ranking/Ranking.jsx: 랭킹 화면 UI
  • ranking/Ranking.styles.js: 랭킹 화면 스타일
  • App.styles.jsx: 페이지, 헤더, 탭 등 공통 스타일

게임 흐름 관리

게임은 isPlaying 상태를 기준으로 시작/종료 흐름을 제어했습니다.
게임 시작 시에는 점수, 성공/실패 횟수, 남은 시간, 안내 메시지, 현재 타겟 정보를 초기화한 뒤 랜덤 타겟을 보여주도록 했습니다. 게임이 종료되면 타이머를 멈추고, 보드에 남아 있는 두더지/폭탄을 숨긴 뒤 게임 종료 모달을 띄우도록 처리했습니다.

랜덤 타겟 생성

두더지와 폭탄은 getRandomTarget 로직을 통해 결정되도록 했습니다.
이 로직에서는 2x2 보드 중 하나의 위치를 랜덤으로 고르고, 해당 위치에 두더지 또는 폭탄 중 하나가 나오도록 했습니다. 게임 중에는 일정 시간마다 새로운 타겟이 등장하도록 했고 두더지를 맞춘 직후에는 맞은 이미지가 잠시 보여야 해서 랜덤 변경이 바로 일어나지 않도록 조절했습니다.

클릭 처리

보드 칸을 클릭했을 때 현재 타겟이 두더지인지 폭탄인지에 따라 다른 동작을 하도록 분기했습니다.
두더지를 클릭하면 점수와 성공 횟수를 증가시키고, 안내 메시지와 맞은 두더지 이미지를 잠시 보여준 뒤 다음 타겟으로 넘어가게 했습니다. 폭탄을 클릭하면 점수를 감소시키고 실패 횟수를 증가시킨 뒤, 폭탄은 바로 사라지고 잠시 후 다음 타겟이 나오도록 처리했습니다. 이 과정에서 클릭 후 안내 메시지가 타겟과 함께 자연스럽게 사라지도록 타이밍을 맞추는 부분을 고민했습니다.

타이머 처리

타이머는 useEffect에서 관리했습니다.
게임이 진행 중일 때만 interval이 실행되도록 했고, 시간이 0이 되면 게임을 종료시키도록 했습니다. 처음에는 클릭이나 상태 변경에 따라 타이머나 랜덤 변경 속도가 이상해지는 문제가 있었는데, cleanup 처리를 통해 interval이 중복 실행되지 않도록 수정했습니다.

랭킹 저장

게임 종료 시 점수가 1점 이상인 경우만 랭킹에 저장되도록 했습니다. 랭킹 데이터는 localStorage에 저장했고, 랭킹 탭에서는 저장된 데이터를 불러와 점수 기준으로 정렬해 보여주도록 했습니다. 기록 초기화 버튼을 누르면 화면 상태와 localStorage 기록이 함께 삭제되도록 했습니다.


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

React를 처음 사용하다 보니 처음에는 코드가 어느 파일에 들어가야 하는지부터 헷갈렸어요.
기존에는 HTML, CSS, JS를 파일별로 나눠 작성했는데, React에서는 JSX 안에서 화면 구조와 동작을 함께 작성하고 emotion으로 스타일 컴포넌트를 만드는 방식이라 익숙하지 않았습니다. 특히 emotion의 styled로 만든 컴포넌트는 선언만 한다고 화면에 보이는 것이 아니라, return 안에서 직접 태그처럼 사용해야 한다는 점을 이해하는 데 시간이 걸렸습니다..!

처음에는 대부분의 상태, 함수, JSX를 App.jsx에 작성해서 기능 흐름을 파악하기는 쉬웠지만, 코드가 길어지면서 어떤 기준으로 파일을 나누어야 할지 고민이 되었습니다. 컴포넌트 분리 개념은 이론으로는 이해했지만, 실제 코드에서는 어디까지를 하나의 컴포넌트로 봐야 하는지 감이 잘 잡히지 않았어요. 우선 화면 역할을 기준으로 Game, Ranking 컴포넌트를 분리했고, 스타일도 각 화면별 styles 파일로 나누었습니다. 이후에는 게임 로직을 custom hook으로 분리하는 방향도 고민해보고 싶습니다.

랭킹이 두 번씩 저장되는 문제가 있었습니다. React state는 값을 변경해도 즉시 반영되지 않기 때문에, 중복 저장 여부를 useState가 아니라 useRef로 관리하도록 수정했습니다.

const hasSaveRecordRef = useRef(false);
if (hasSaveRecordRef.current) return;
hasSaveRecordRef.current = true;

이 과정을 통해 화면에 보여줄 값은 useState, 렌더링과 직접 관련 없지만 즉시 기억해야 하는 값은 useRef를 사용하는 것이 적절하다는 점을 배웠습니다.!

랜덤으로 두더지와 폭탄을 보여주는 부분도 고민이 많았습니다.
처음에는 클릭해야만 다음 이미지가 나타나는 구조였는데, 실제 두더지 게임처럼 클릭하지 않아도 일정 시간마다 자동으로 타겟이 바뀌어야 자연스럽다는 것을 깨달았습니다.

useEffect(() => {
  if (!isPlaying) return;

  const targetTimerId = setInterval(() => {
    if (activeType === "hit") return;
    if (message) return;

    showRandomTarget();
  }, 1300);

  return () => clearInterval(targetTimerId);
}, [isPlaying, activeType, message, showRandomTarget]);

또 두더지를 클릭했을 때 바로 다음 타겟으로 넘어가면 맞은 이미지가 보이지 않아서, activeType을 "hit"으로 잠시 바꾸고 setTimeout으로 700ms 후 다음 타겟이 나오도록 구현했습니다.

setActiveType("hit");
setMessage("두더지를 잡았다!");

setTimeout(() => {
  setMessage("");
  showRandomTarget();
}, 700);

폭탄을 클릭했을 때는 자동 타겟 변경 타이머와 클릭 시 즉시 변경 로직이 겹쳐서 화면이 너무 빨리 바뀌는 것처럼 느껴졌습니다. 그래서 폭탄 클릭 시에는 먼저 현재 타겟을 비우고, 짧은 시간 후 다음 타겟이 나오도록 조정했습니다.

setActiveIndex(null);
setActiveType(null);

setTimeout(() => {
  setMessage("");
  showRandomTarget();
}, 500);

🙋‍♀️ 리뷰 요청 포인트 & 질문

  • emotion styled 컴포넌트 사용 방식이 과하거나 어색한 부분은 없는지 확인받고 싶습니다!

  • 외부 UI 라이브러리 없이 직접 UI 컴포넌트는 구현했지만 아직 컴포넌트 분리는 충분히 하지 못했습니다..! 현재 코드 구조가 제가 봐도 복잡한 상태인거같아요.. 컴포넌트 분리 관련 개념이 이론으로 들었을때는 이해가 되었는데 막상 직접 구현하려고 하니 어떤 기준으로 컴포넌트를 나누어야 할지 감이 안 잡히더라고요......... (컴포넌트 관련 피드백 주시면 코드 리뷰 반영때 열심히 고쳐보겠습니다!!!!)
    코드 리뷰를 반영하면서 컴포넌트 분리를 꼭 시도해보고 싶은데, 현재 코드 기준으로 어떤 단위로 파일을 나누면 좋을지 피드백 받고 싶습니다!

  • localStorage에 랭킹 데이터를 저장하고 불러오는 로직이 적절한지 궁금합니다!


📷 결과물

2026-05-01.11.23.46.mov

@yumin-kim2 yumin-kim2 self-assigned this May 1, 2026
@yumin-kim2 yumin-kim2 removed the 심화 label May 8, 2026

@soyyyyy soyyyyy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

리액트가 처음이라고 하셨는데 제가 처음 했을 때 보다 잘 하시는 것 같아요... 저도 많이 어려웠는데 여러가지 고민하신 흔적이 있어서 인상깊었습니다!
지금은 App.jsx에 로직이 많이 모여 있지만, 처음에는 이렇게 쭉 써보고 어떤걸 커스텀 훅으로 분리할지, 유틸 함수로 분리할지 UI는 이렇게 컴포넌트로 빼는게 나을지 고민하는 방식으로 연습하는 방식이 도움이 많이 됐던 것 같습니다!

몇 가지 코멘트 남겼으니 확인 부탁드려요. 수고하셨습니다 💕

Comment thread Week3/Homework/index.html Outdated
Comment thread Week3/Homework/index.html Outdated
Comment thread Week3/Homework/src/App.jsx Outdated
@@ -0,0 +1,214 @@
import { useCallback, useEffect, useRef, useState } from "react";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR에 적어주신대로 App 컴포넌트가 게임 상태, 타이머 로직, 랭킹 처리까지 너무 많은 책임을 지고 있는 것 같습니다.
게임 로직, 랭킹 저장 로직을 훅이나 유틸로 분리하고, UI 컴포넌트도 세분화하면 더 좋을 것 같아요!

Comment thread Week3/Homework/src/App.jsx Outdated

const RANKING_STORAGE_KEY = "mole-game-rankings";

const getRandomTarget = () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

랜덤 위치와 타입을 만드는 로직은 렌더링과 직접 관련된 로직이 아니기 때문에 별도로 분리한 점이 좋은 것 같습니다! 이런 부분을 util 함수로 별도의 파일에 분리해도 좋을 것 같아요!

</Header>

{activeTab === "game" && (
<Game

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Game 컴포넌트가 받는 props가 많은 것 같습니다. 게임 안에서 게임 상태, 보드, 모달 처럼 UI 단위를 더 나누면 props도 자연스럽게 분산될 것 같습니다.

Comment thread Week3/Homework/src/game/Game.jsx Outdated
<>
<GameLayout>
<StatusPanel>
<Card>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

다른 곳에서 한 번 언급하긴 했는데 컴포넌트 UI를 어떻게 분리해야할지 잘 모르겠다면 우선 반복적으로 사용되는 것들부터 분리하는 연습을 하면 좋을 것 같아요! 대표적으로 button, input(textField), card 등등... 여기서도 Card가 반복적으로 사용되고 있으니 별도의 파일/컴포넌트로 분리해 props로 내용을 관리하면 좋을 것 같아요. 버튼도!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

또, Game.jsx는 상태 패널, 조작 버튼, 보드, 모달을 모두 렌더링합니다. 이 파일은 UI 중심이지만, JSX가 길어질 수 있으니 아래처럼 나누면 좋을 것 같습니다.
예를 들면 이런 느낌으로..?

game/
Game.jsx
GameStatusPanel.jsx
GameControls.jsx
GameBoard.jsx
GameOverModal.jsx
Game.styles.js

Comment thread Week3/Homework/src/App.jsx Outdated
Comment thread Week3/Homework/src/App.jsx Outdated
Comment thread Week3/Homework/src/App.jsx Outdated

@hdg0116 hdg0116 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

컴포넌트 분리에 대해 많은 고민을 하신 게 잘 보였어요..! 저도 처음에 어디까지 분리해야할지 고민을 많이 했는데, 역할별로 세세하게 쪼개는 연습을 하다 보니 나름대로의 기준이 생긴 것 같아요. 유민님도 역할별로 로직을 분리하는 연습을 계속 해보시는 걸 추천드려요! App.jsx에 로직을 몰아넣기보다는, 기능별로 컴포넌트 파일을 따로 분리해서 로직을 분산하는 구조가 더 좋을 것 같습니다!
너무 수고 많으셨습니다🔥

Comment thread Week3/Homework/src/App.jsx Outdated
Comment on lines +25 to +40
timeLeft,
score,
successCount,
failCount,
message,
isPlaying,
isGameOver,
activeIndex,
activeType,
moleImg,
bombImg,
hitMoleImg,
onStartGame,
onStopGame,
onClickHole,
onCloseModal,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Game 컴포넌트가 받는 props가 많아서, 게임 화면 안에서도 상태 패널, 보드, 조작 버튼, 모달 정도로 한 번 더 나눠보면 좋을 것 같아요!

Comment thread Week3/Homework/src/App.jsx Outdated

@gyeongbibin gyeongbibin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3주차 과제가 많이 어려웠는데, 많이 고민하면서 노력하신 흔적이 보이네요 ! 고생하셨습니다.
개선할 부분은 코멘트로 남겨두었으니 확인부탁드려요 !

Comment thread Week3/Homework/src/header/Header.styles.js
Comment thread Week3/Homework/src/game/Game.styles.js Outdated
Comment thread Week3/Homework/src/game/Game.styles.js Outdated
Comment thread Week3/Homework/src/App.jsx Outdated
Comment thread Week3/Homework/src/game/Game.jsx Outdated
Comment thread Week3/Homework/src/App.jsx Outdated
const [activeType, setActiveType] = useState(null);
const [rankingRecords, setRankingRecords] = useState([]);
const hasSaveRecordRef = useRef(false);
const [message, setMessage] = useState("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

App.jsx는 전체 페이지들을 연결하는 최상위 라우팅 진입점 역할이라고 생각하시면 될 것 같습니다!
그래서 보통 App 컴포넌트에는 비즈니스 로직이나 개별 스타일을 많이 넣기보다는, 라우터·레이아웃·Provider 정도만 구성하고 최대한 가볍게 유지하는 방향으로 구현하는 방식으로 진행해보시면 좋을 것같아요 !

Comment thread Week3/Homework/src/App.jsx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3주차 공유 New feature or request 기본

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants