1. 아래 페이지 클론 코딩
이번 챌린지에선 마크업보다 데이터 흐름, 컴포넌트 구분에 집중해주세요!
- 상세 스펙은 다음과 같아요
Math.pow(Math.round((stage + 0.5) / 2) + 1, 2)
개의 사각형이 표시되며, 그 중 하나만 색깔이 다릅니다.- 한 stage의 제한 시간은 15초입니다.
- 색이 다른 사각형(정답)을 클릭한 경우 아래 변경사항이 적용됩니다.
- 다음 스테이지로 넘어갑니다.
- Math.pow(stage, 3) * 남은시간 만큼의 score가 누적됩니다
- 오답을 클릭한 경우 아래 변경사항이 적용됩니다.
- 현재 stage의 남은 시간이 3초 줄어듭니다.
- 남은 시간이 0초 이하가 되면 게임이 종료됩니다. 최종 stage와 누적 score를 출력하고, 새로운 게임을 시작할 수 있습니다.
- stage가 올라갈수록 정답과 오답의 색상 차이가 줄어듭니다.
2. 다음 조건에 맞게 진행
- React Framework를 사용할 것
- Function Component를 활용할 것
- Javascript보다는 Typescript를 활용할 것
- 서버에 배포할 것 (Vercel과 같은 서비스를 이용해보세요)
- Context, Redux, Mobx, Recoil 등 상태관리 도구를 사용하지 않을 것
3. 결과물을 아래와 같은 형식으로 제출
넘블 챌린지 진행 계기
인터넷 검색을 해보다가, 개발 0-1년차에게 적합하다고 해서 numble의 코드챌린지가 마감되었어도 경험삼아 해보려고한다.
진행 과정
1) 컴포넌트화
2) 컴포넌트 큰거에서 작은거로 가기
-> 어떤 변수가 필요할지, 그 변수가 어떻게 바껴야하는지 생각하기
프로젝트를 진행할 때 어려웠던 점/고민했던 부분과 해결방법
1) 작은 사각형 스타일 (갯수는 맞게 뿌렸으나, 스타일이 어려웠던 점)
- 문제에서, stage에 따라 사각형 갯수를 안내해주었기 때문에 변수로 선언해서 그 갯수만큼 사각형을 뿌리면 된다고 생각하여 뿌렸는데, 사각형 안에 예쁘게 안채워지는 문제점이 있었다.
=> 이것은 항상 display 속성을 이용하여 스타일을 해주었었기 때문에 생각하지 못했던 grid로 해결해주었다.
2) 정답이 아닐 경우 타이머 감소
처음 생각한 방안) 부모 컴포넌트에서 time과 setTime을 props로 넘겨서 보드 컴포넌트에서 실패 이벤트를 핸들링하였다.
- 문제점: React.memo를 사용하였기 때문에, Board컴포넌트의 props가 변하면 재렌더링이 일어나서, 시간 초가 줄어들 때 보드 컴포넌트가 자꾸 렌더링이 되는 문제점이 있었다.
=> 정답이 아닌 것은 자식인 보드 컴포넌트에서, 타이머 감소는 부모인 App에서 이루어지도록 정답과 실패 핸들링을 각각 만들고, props로 넘겨주었다.
3) 타이머 줄어들 때 자식 컴포넌트 재 렌더링
- 컴포넌트가 렌더링 되는 조건은 props가 바뀔 때, state가 바뀔 때, 부모 컴포넌트가 리렌더링될 때 3가지로, 부모 컴포넌트에서 time이 줄어들 때, 자식 컴포넌트인 board가 재렌더링이 되어, 시간초가 줄어듦에 따라 보드 컴포넌트가 계속 바뀌는 문제점이 있었다.
=> 컴포넌트를 메모이제이션해주는 React.memo를 활용하여 props가 변할 때만 렌더링 되도록 메모이제이션 해주었다.
- props가 stage와 onClick 두개였음에도 불구하고, time이 줄어들 때 계속 렌더링이 되었다.
=> onClick함수가 계속 재렌더링을 일으키는 원인으로, 클릭 이벤트에 함수들을 [stage]가 변할 때, 클릭 이벤트를 활성화 시키도록 함수를 메모이제이션 해주는 useCallback()을 이용하였다.
4) 특정 사각형에 다른 색깔을 주는 방법
- 랜덤으로 특정 사각형의 색이 달라야했지만 색이 모두 똑같은 색상으로 나타났다.
=> 특정 인덱스를 사각형 갯수에서 랜덤으로 뽑은 후, map을 돌릴 때 해당 인덱스와 특정 인덱스가 같다면 props로 넘겨준 컬러값을 다르게 설정하여 넘겨주었다.
- 처음 코드는 i===answerIdx 일 때, 작은 블록 컴포넌트를 다시 불러서 중복적인 코드를 사용했다.
=> 블록 컴포넌트가 아닌, props를 넘겨줄 때 조건을 주어, 불필요한 중복 사용을 줄였다.
활용한 라이브러리와 그 이유
useState => react의 state관리의 기본이기에 state로 하였다.
useEffect => 컴포넌트가 나타났을 때 타이머 감소를 주기 위해서
useCallback => 함수를 메모이제이션 해주지 않으면, 계속 보드컴포넌트가 재렌더링 되어 메모이제이션해주었다.
React.memo => 컴포넌트를 메모이제이션 해주기 위해 사용하였다.
추가 고안할 라이브러리
row의 갯수만큼 빈 배열을 만들고, map을 돌릴 때 null로 fill을 해주었어야했다.
이유는? new Array(16) 을 한다고 해서, length가 16인 배열이 생기는 것이고, 안의 값은 없다.
위와 같이 fill을 하지 않으려면 lodash의 range를 고안해봐야겠다.
최종 코드는 다음과 같다.
//App.tsx
import { useState, useEffect, useCallback } from "react";
import Board from "./components/Board";
import Header from "./components/Header";
function App() {
const [stage, setStage] = useState(1);
const [time, setTime] = useState(15);
const [score, setScore] = useState(0);
const handleSuccess = useCallback(() => {
setStage(stage + 1);
setScore(stage ** 3 * time);
setTime(15);
}, [stage]);
const handleFailure = useCallback(() => {
setTime((time) => time - 3);
}, [stage]);
useEffect(() => {
setInterval(() => {
setTime((time) => time - 1);
}, 1000);
}, []);
if (time < 0) {
alert(`GAME OVER!\n스테이지:${stage}, 점수:${score}`);
setStage(1);
setTime(15);
setScore(0);
}
return (
<>
<Header stage={stage} time={time} score={score} />
<Board
stage={stage}
handleSuccess={handleSuccess}
handleFailure={handleFailure}
/>
</>
);
}
export default App;
//Board.tsx
/* eslint-disable react/no-array-index-key */
import React from "react";
import styled from "styled-components";
type BoardProps = {
stage: number;
handleSuccess: React.MouseEventHandler<HTMLDivElement>;
handleFailure: React.MouseEventHandler<HTMLDivElement>;
};
type BlockProps = {
size: number;
color: string;
};
const Board = ({ stage, handleSuccess, handleFailure }: BoardProps) => {
const row = (Math.round((stage + 0.5) / 2) + 1) ** 2;
const size = 360 / Math.sqrt(row);
const answerIdx = Math.round(Math.random() * (row - 1));
const r = `${Math.floor(Math.random() * 256)}`;
const g = `${Math.floor(Math.random() * 256)}`;
const b = `${Math.floor(Math.random() * 256)}`;
const baseColor = `rgb(${r},${g},${b})`;
const answerColor = `rgba(${r},${g},${b},0.7)`;
return (
<BoardWrapper row={Math.sqrt(row)}>
{new Array(row).fill(null).map((r, i) => (
<Block
key={i}
size={size}
color={i === answerIdx ? answerColor : baseColor}
onClick={i === answerIdx ? handleSuccess : handleFailure}
/>
))}
</BoardWrapper>
);
};
const BoardWrapper = styled.div<{ row: number }>`
display: grid;
grid-template-columns: repeat(${(props) => props.row}, 1fr);
width: 360px;
height: 360px;
`;
const Block = styled.div<BlockProps>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
background: ${(props) => props.color};
box-sizing: border-box;
border: 1px solid white;
`;
export default React.memo(Board);
코드리뷰를 해주신 분이 계셔서, 계속 수정해나가고있다.