게임하러가기: https://find-different-color.vercel.app/
깃허브 레파지토리: https://github.com/junghyeonsu/find-different-color
메인 페이지에 랭크 보기 버튼을 추가해서 랭크 페이지를 추가했습니다.
처음에 게임 시작 버튼을 클릭하게 되면 닉네임을 입력받습니다.
닉네임은 빈 공백만 아니면 되고, 닉네임은 세션(Session storage 사용)에서 유지됩니다.
게임이 종료가 되면 자동적으로 닉네임, 스테이지, 점수로 기록이 되며
랭크 페이지에서 랭킹을 확인할 수 있습니다.
랭킹은 1등에서 100등까지만 확인할 수 있도록 했습니다!
데이터베이스는 firestore를 사용했습니다.
랭킹 기준은 단계(stage)를 우선적으로 내림차순하고,
단계가 같다면 점수(Point)를 내림차순으로 랭킹이 매겨집니다.
다른분들과 경쟁해보세요!
기간: 2/4 - 2/13
이 챌린지는
React
를 더React
스럽게 사용하고 싶은0~1년차 React 기반 프론트엔드 개발자
에게 적합해요.이 챌린지에서 우리는
Context
,Redux
,Recoil
등 상태관리 라이브러리를 사용하지 않고 다른 색깔 찾기 게임을 제작할 거예요.이 챌린지를 통해 우리는 이 챌린지를 통해 우리는 요구사항에 따라 적절한 단위로
component
를 나눠볼 수 있고, 어느component
가 어느state
를 가지고 있어야하는가에 대해 실전적으로 익힐 수 있으며, 적절한prop
을 사용해 탄력적인component
작성에 대해서도 연습할 수 있어요.
4학년이 끝나고 취준을 하느라 개발에 많은 시간을 쓰지 못하고, CS공부나 알고리즘을 푸는데에 시간을 보내고 있었다. 그러다가 친구의 추천으로 Numble Challenge
에 대해서 알게되었고, 너무 재미있어 보여서 그냥 생각도 안하고 바로 신청했다.
디자인을 좀 예쁘게 하고 싶었기에 Pigma
툴을 이용해서 간단히 프로토타입을 제작하고 들어갔다.
프로토타입이 있는 것과 없는 것의 개발 속도 차이는 엄청나다.
프로토타입이 있으면 그대로 개발만 진행하면 되지만, 없으면 개발을 하면서 스타일도 신경써야 하기 때문에 멀티태스킹을 해야한다.
📦src
┣ 📂components
┃ ┣ 📂Board
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┣ 📜styled.ts
┃ ┃ ┗ 📜types.ts
┃ ┣ 📂Card
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┣ 📜styled.ts
┃ ┃ ┗ 📜types.ts
┃ ┣ 📂Footer
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┗ 📜styled.ts
┃ ┣ 📂Modal
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┣ 📜styled.ts
┃ ┃ ┗ 📜types.ts
┃ ┣ 📂Point
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┣ 📜styled.ts
┃ ┃ ┗ 📜types.ts
┃ ┣ 📂Stage
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┣ 📜styled.ts
┃ ┃ ┗ 📜types.ts
┃ ┣ 📂Timer
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┣ 📜styled.ts
┃ ┃ ┗ 📜types.ts
┃ ┗ 📜index.ts
┣ 📂hooks
┃ ┣ 📜usePoint.ts
┃ ┣ 📜useStage.ts
┃ ┗ 📜useTimer.ts
┣ 📂pages
┃ ┣ 📂Home
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┗ 📜styled.ts
┃ ┣ 📂Play
┃ ┃ ┣ 📜index.tsx
┃ ┃ ┗ 📜styled.ts
┃ ┗ 📜index.ts
┣ 📂utils
┃ ┗ 📜JSUtility.ts
┣ 📜App.tsx
┣ 📜GlobalStyles.ts
┣ 📜constants.ts
┗ 📜index.tsx
function useTimer(): TimerHookProps {
const [time, setTime] = useState<number>(INITIAL_TIME);
const [animationActive, setAnimationActive] = useState<boolean>(false);
const intervalRef: { current: NodeJS.Timeout | null } = useRef(null);
const startTimer = useCallback(() => {
if (intervalRef.current !== null) return;
intervalRef.current = setInterval(() => {
setTime(time => time - 1);
}, ONE_SECOND);
}, []);
const stopTimer = useCallback(() => {
if (intervalRef.current === null) return;
clearInterval(intervalRef.current);
intervalRef.current = null;
}, []);
const resetTimer = useCallback(() => {
setTime(INITIAL_TIME);
}, []);
const minusTime = useCallback(() => {
setTime(time - 3);
setAnimationActive(true);
setTimeout(() => {
setAnimationActive(false);
}, 100);
}, [time]);
return { time, animationActive, startTimer, stopTimer, resetTimer, minusTime };
}
게임에서 15초의 시간이 주어지고 0초가 되면 게임이 끝난다.
그리고 틀린 카드를 클릭 시 3초가 줄어드는 함수가 필요했다.
useTimer
훅을 생성해서 timer와 관련된 함수들을 관리했다.
타이머를 useRef
를 사용해서 구현하는 것이 제일 성능상, 그리고 사이드이펙트를 제거할 수 있는 좋은 방법 같았다. 타이머 구현 관련 자료를 참고해서 개발하였다.
그리고 animationActive
상태 값은 타이머 Bar가 5초 이하로 떨어지면 Bar가 흔들리는 애니메이션을 추가했는데, 해당 애니메이션을 넣기 위해서 다음과 같이 작성했다.
animation: ${(props: ContainerProps) =>
(props.time <= 5 || props.active) && `shake 0.3s infinite linear`};
위의 코드는 styled-components
라이브러리를 사용한 코드다.
우선 animation
속성에 infinite
를 주어서 계속 동작하도록 한다.
그리고 animationActive
상태 값이 true이거나, 5초 이하일 때만 애니메이션이 작동하도록 한다.
애니메이션은 keyframes
를 사용했고, 아래와 같이 작성했다.
@keyframes shake {
0% {
transform: translate(1px, 1px) rotate(0deg);
}
10% {
transform: translate(-1px, -2px) rotate(-1deg);
}
20% {
transform: translate(-3px, 0px) rotate(1deg);
}
30% {
transform: translate(3px, 2px) rotate(0deg);
}
40% {
transform: translate(1px, -1px) rotate(1deg);
}
50% {
transform: translate(-1px, 2px) rotate(-1deg);
}
60% {
transform: translate(-3px, 1px) rotate(0deg);
}
70% {
transform: translate(3px, 1px) rotate(-1deg);
}
80% {
transform: translate(-1px, -1px) rotate(1deg);
}
90% {
transform: translate(1px, 2px) rotate(0deg);
}
100% {
transform: translate(1px, -2px) rotate(-1deg);
}
}
이렇게 작성하면 이 애니메이션을 동작시키고 싶을 때만 animationActive
상태 값을 잠시 true
로 바꿨다가, setTimeout
을 이용해서 다시 false
로 바꾸면 된다.
정답이 아닌 색깔 카드를 클릭했을 때, 한번 흔들리는 애니메이션이 아래와 같이 작성되었다.
const minusTime = useCallback(() => {
setTime(time - 3);
setAnimationActive(true); // 틀렸을 때, true 바꿨다가
setTimeout(() => { // setTimeout을 이용해서 0.1초뒤에 바로 꺼버린다.
setAnimationActive(false);
}, 100);
}, [time]);
해당 프로젝트에서 제일 시간이 많이 걸렸던 부분이다.
우선 두 가지가 관건이었다.
첫번째로 색깔을 랜덤으로 뽑는 로직은 rgb 색상 표기법을 생각했다.
CSS color property에 rgb(255, 255, 255)와 같이 색깔을 표시할 수 있는데
0에서 255까지 랜덤 숫자를 뽑아서 rgb에 넣어주면 되는 것이었다.
const pickRandomColor = useCallback(() => Math.floor(Math.random() * 256), []);
Math.random()
함수가 0에서 1미만의 수를 랜덤으로 뽑는 것이다.
곱하기 256을 하면 0에서 256미만의 수를 뽑는다.
그래서 Math.floor
로 내림 연산을 해주면 0에서 255까지의 정수를 잘 뽑는다.
const difference = useMemo(() => (100 - stage * 2 > 0 ? 100 - stage * 2 : 2), [stage]);
const pickAnswerColor = useCallback(
color => (color > 100 ? color - difference : color + difference),
[difference],
);
정답(클릭해야 하는) 색깔을 뽑는 것은 위와 같다.
우선 difference
변수를 생성해서 stage에 따라서 변하게 했다.
스테이지가 올라갈수록 difference
의 값이 작아진다.
그리고 뽑아진 랜덤 색상에 difference
만큼 빼거나 더해서 색차이를 만들었다.
그래서 색상을 뽑는 함수는 아래와 같이 최종적으로 작성되었다.
const colors = useMemo((): { wrong: string; answer: string } => {
const red = pickRandomColor();
const green = pickRandomColor();
const blue = pickRandomColor();
return {
wrong: `rgb(${red}, ${green}, ${blue})`,
answer: `rgb(${pickAnswerColor(red)}, ${pickAnswerColor(green)}, ${pickAnswerColor(blue)})`,
};
}, [pickRandomColor, pickAnswerColor]);
스테이지가 진행되면 될수록 색의 차이가 점점 줄어든다.
그리고 차이가 2보다 작아지면 차이는 2로 고정된다. (rgb 2가 차이나는 것을 구분할 수 있나요?...)
게임 보드를 보여주는 것이 어려웠다.
스테이지가 올라갈수록 Card의 개수, 크기 그리고 정답 Card의 Index가 랜덤으로 계속 변해야 했기 때문에 조금 까다로웠다.
const boardRow = useMemo(() => Math.round((stage + 0.5) / 2) + 1, [stage]);
const cardAmount = useMemo(() => boardRow ** 2, [boardRow]);
const cardSize = useMemo(() => BOARD_SIZE / (cardAmount / boardRow), [cardAmount, boardRow]);
const answerCardIndex = useMemo(
() => Math.floor(Math.random() * (Math.round((stage + 0.5) / 2) + 1) ** 2),
[stage],
);
const cards = useMemo(
() =>
Array.from(Array(cardAmount), (_, index) =>
answerCardIndex === index ? (
<Card
onClick={handleAnswerCardClick}
color={`${colors.answer}`}
size={cardSize}
key={index}
/>
) : (
<Card
onClick={handleWrongCardClick}
color={`${colors.wrong}`}
size={cardSize}
key={index}
/>
),
),
[
cardAmount,
answerCardIndex,
handleAnswerCardClick,
colors.answer,
colors.wrong,
cardSize,
handleWrongCardClick,
],
);
Card
컴포넌트를 만들어서 props로 클릭 이벤트, 색깔, 크기를 받게 했다.
color
는 위에서 설명했고, size와 Card의 개수, 정답 Card의 Index는 요구사항에 맞게 프로그래밍했다.
그리고 각각의 Card
에 border
를 줬는데, Card의 개수가 늘어날수록 게임보드의 총 크기가 변했다.
box-sizing: border-box;
그래서 Card
컴포넌트에 box-sizing: border-box
를 주어서 border가 생겨도 Card
컴포넌트의 전체 width, height는 변경되지 않도록 하였다.
storybook
, jest
를 이용해서 테스트 파이프라인을 구축하고 싶다.데모로 보여준 링크는 스타일이 내 스타일이 아니라서 내 맘대로 바꿨다.
스타일에 관한 요구사항은 아예 없었기 때문에 내가 직접 구현했다.
예뻐야 사람들이 관심을 가지고 보고, 게임을 하기 때문에 스타일에 좀 많이 신경을 썼다.
그리고 애니메이션 같은 것들도 게임을 하는데 역동적인 느낌을 주기 위해서 노력했다.
맨날 어떤 프로젝트를 하면 배포도 못하고 열심히 구현을 했는데 뭘 했는지 모를 때가 많다.
이번 챌린지는 시작부터 배포를 해야됐고, 엄청난 기능을 구현하는 게 아니라서 짧은 시간동안 진짜 몰입해서 구현했다. 그리고 결과물도 확실해서 너무나도 만족한다.
그리고 모든 기능을 구현하는 것도 중요하지만 배포를 해서 피드백을 받는 것의 중요성을 다시금 느낀다.
이런 간단한 게임을 여러 개 만들어보고 싶다는 생각도 했다.
추후에 이런 챌린지가 있다면 꼭 다시 참여할 것 같고, 너무나 좋은 경험이었다.
우와... 고수님 좋아요 꾹 누르고 갑니다! 디자인 너무 좋아요!