Skip to content

Conversation

@yooncandooit
Copy link
Collaborator

@yooncandooit yooncandooit 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. 랭킹 정렬 기능

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


공유과제

제목: useEffect 더 잘 다루기 (ft. 의존성 배열 & clean up 함수)

링크 첨부 : https://velog.io/@dotsi0/useEffect-%EB%8D%94-%EC%9E%98-%EB%8B%A4%EB%A3%A8%EA%B8%B0-ft.-%EC%9D%98%EC%A1%B4%EC%84%B1-%EB%B0%B0%EC%97%B4-clean-up-%ED%95%A8%EC%88%98


🔧 구현 요약 및 새로 배운 점

🛸 폴더 구조

|── src
│   ├── App.jsx
│   ├── components
│   │   ├── Card.jsx
│   │   ├── Card.styles.js
│   │   ├── Header.jsx
│   │   ├── Header.styles.js
│   │   ├── Modal.jsx
│   │   └── Modal.styles.js
│   ├── hooks
│   │   ├── useGame.js
│   │   └── useTimer.js
│   ├── main.jsx
│   ├── pages
│   │   ├── Game.jsx
│   │   ├── Game.styles.js
│   │   ├── Ranking.jsx
│   │   └── Ranking.styles.js
│   ├── styles
│   │   ├── reset.js
│   └── utils
│       ├── gameUtils.js
│       ├── levelConfig.js
│       └── localStorage.js
└── vite.config.js

CSS 라이브러리 선택 이유

  • tailwind CSS 라이브러리를 주로 사용했어서, sass와 tailwind 다음으로 점유율이 높은 emotion 라이브러리를 사용했어요. 1, 2주차에서 사용한 styled.component와 적용 방식이 비슷해서 익숙한 느낌이었어요! 아래는 시간대별로 CSS 라이브러리의 인기를 확인할 수 있는 npm trend라는 사이트예요!
image
  • localStorage 관련 부분은 상태 관리할 부분이 따로 없이 save된 상태를 get 또는 clear만 하면 된다고 생각해서 리렌더링을 유발하는 훅 보단 유틸 함수로 구현했어요.

  • 0.01초 단위로 카운트다운하는 타이머를 setIntervaluseRef로 구현했어요. 클리어 시간은 현지 시간에서 초기 시간을 빼서 계산했고, stopTimer가 시간을 반환하도록 했어요. 추가로 useCallback을 사용해서 함수 메모이제이션 부분을 적용해서 무한 루프를 방지했어요.
    사실 3차 세미나 때 잠깐 언급되었던 useRef가 조금은 생소했어요. 대부분의 상태 관리는 useState로만 했었고 둘의 차이를 명백하게는 몰랐는데 이번 기회에 정리할 수 있어 유익했어요 ☺️

useState: 상태 유지 값과 그 값을 갱신하는 함수를 반환해요. setState 함수는 새 state를 받아 컴포넌트 리렌더링 큐에 등록합니다. 컴포넌트는 다음 렌더링 시에 useState를 통해 반환받은 첫번째 값은 항상 갱신된 최신 state가 돼요.

useRef: DOM 노드나 React 엘리먼트에 접근하는 방법이예요. 공식 문서에서는 ref의 사용 사례를 아래와 같이 제시했어요

  1. 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때
  2. 애니메이션을 직접적으로 실행시킬 때
  3. 서드 파티 DOM 라이브러리를 React와 같이 사용할 때

👾 결국 React에서 ref는 DOM을 조작하기 위해 사용하는 건데, 전 아래와 같이 사용했어요.

  const intervalRef = useRef(null);

useRef는 .current 프로퍼티로 전달된 인자(intervalRef)로 초기화된 변경 가능한 ref 객체를 반환해요. useRef로 만든 객체를 수정하는 것은 컴포넌트의 렌더링과 무관합니다. 즉, .current 프로퍼티를 변형하는 것이 리렌더링을 발생시키지 않는다는 거예요!

useState와 useRef는 둘 다 상태를 기억하고 변경을 감지하지만, 그 성격이 완전히 달랐어요. 그 차이를 잘 보여주는게 이번 timer의 예시였다고 생각합니다 😊

  • 의존성 체인이 무한 루프를 유발한다는 걸 배우고, setState는 의존성 배열에서 생략 가능하다는 걸 배웠어요.

  • 1주차 인터스에서 배운 내용을 바탕으로 rotateY와 3D transform으로 카드 뒤집기 애니메이션 부분을 구현했어요 🤩

  • 랭킹에는 localStorage를 활용한 기록 저장 부분을 적용해서 클리어 시 자동으로 기록을 저장해요. 또한 2주차 인터스에서 배운 formatDate로 날짜 포맷팅을 적용했어요! 🤩


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

  • resetTimer → stopTimer → startTime 의존성 체인을 끊지 못해서 타이머 작동이 안되거나, 타이머 내에 클로저 문제로 인해 최종 클리어 타임을 잘못 계산해서 랭킹에 클리어 타임 노출이 안되는 이슈가 있었어요. 비동기 상태 업데이트와 동기 계산을 분리해야 한다는 걸 확실히 배웠어요

  • 명세만 봤을 때는 몰랐는데 생각보다 구현할 파일이 많았어요. 전 프로젝트 전에는 항상 어떤 걸 컴포넌트화해야할지를 먼저 고민하고 시작하는데, 다음부턴 컴포넌트부터 만들지 말고 파일 구조를 먼저 세팅하면서 구조를 그려봐야겠어요!


🔭 리뷰 요청 포인트 & 질문

  • 타이머 초기화 로직을 끝까지 리팩토링 했는데,, 혹시 놓친 부분이 있는지 궁금해요.
  • 로컬 스토리지를 유틸 함수로 구현한게 적절한지 궁금해요!
  • 게임 페이지 코드가 초반에는 300줄을 달리다가 과제 마감일에 구조 리팩토링을 했는데, 가독성은 괜찮을지 .. 😅 구조를 잘 나눴는지 리뷰 남겨주시면 감사하겠습니다! 깨끗한 코드 작성하고 싶어요 .. 🛸

📷 결과물

레벨 선택

화면 기록 2025-11-11 오후 11 03 23

1단계 플레이 & 랭킹

2025-11-11.11.10.17.mov

타임 아웃 모달

2025-11-11.11.15.09.mov

@yooncandooit yooncandooit self-assigned this Nov 11, 2025
@yooncandooit yooncandooit added 공유 아티클 작성 기본 기본 과제 심화 심화 과제 labels Nov 11, 2025
Copy link

@jeonghoon11 jeonghoon11 left a comment

Choose a reason for hiding this comment

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

PR 잘 봤어요! useEffect에 대해 아티클을 작성하셨더라구요! useEffect는 봐도 봐도 너무 어려운것 같아요ㅠㅠㅠㅠ 이번 과제하면서 DOM 구조를 생각하면서 개발하신것 너무 대단합니다!!
저도 이번 과제 자체가 상태관리 할게 너무 많아서 코드 짜는게 너무 어려웠어요ㅠㅠ
고생 많으셨습니다! 4주차 과제도 파이팅!

<S.EmptyMessage>아직 뒤집은 카드가 없어요</S.EmptyMessage>
) : (
history.map((item, index) => (
<S.HistoryItem key={index} isSuccess={item.isSuccess}>

Choose a reason for hiding this comment

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

key값을 고유 정보로 대체하면 더 안정적일것 같아요!
예) 레벨 + 시간

Choose a reason for hiding this comment

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

useGame 훅에 10개가 넘는 상태가 존재해요. 따라서 부분적으로 분리해서 관리하면 더 단일 책임 원칙에 적합할것 같네요!

Choose a reason for hiding this comment

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

Game 페이지에서 여러 useEffect가 서로 의존하며 동작하는 구조라 사이드이펙트가 발생할 가능성이 있어 보여요!
타이머 관리, 매칭 성공 처리, 모달 카운트다운 같은 로직을 커스텀 훅으로 분리하면 책임이 더 명확해지고 유지보수도 쉬워질 것 같아요

Copy link

@seunghye-rain seunghye-rain left a comment

Choose a reason for hiding this comment

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

전체적으로 훅·유틸·스타일 분리가 잘 되어 있고 타이머에 많은 고민 하신거 잘 보였어요!! 이모션으로 깔끔하게 사용하신 것도 잘 봤습니다!!

3주차 과제 수고하셨어용🥹

</S.EmptyText>
) : (
records.map((record, index) => (
<S.TableRow key={index}>

Choose a reason for hiding this comment

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

key 값은 index보다 다른 값을 넣어주는게 좋을 것 같아요!

https://yozm.wishket.com/magazine/detail/2634/

const startTimer = useCallback(() => {
setIsRunning((current) => {
if (!current) {
setStartTime(Date.now());

Choose a reason for hiding this comment

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

Date.now()를 기반으로 시간 계산한거 좋은 것 같아요..!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

공유 아티클 작성 기본 기본 과제 심화 심화 과제

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants