[Numble Challenge] 다른 그림 찾기 게임 구현

welchs·2022년 2월 12일
1
post-thumbnail

프로젝트 소개

상세스펙

요구사항

  1. React Framework를 사용할 것
  2. Function Component를 활용할 것
  3. Javascript보다는 Typescript를 활용할 것
  4. 서버에 배포할 것 (Vercel과 같은 서비스를 이용해보세요)
  5. Context, Redux, Mobx, Recoil 등 상태관리 도구를 사용하지 않을 것

결과물

게임 플레이

게임 오버

참가 이유

공짜로 공부할 기회라고 생각하고 참가하게 되었다.
필자는 거의 TS를 사용한 경험이 없어 이에 대해 경험해보고 싶었다.

주요 로직

Board 그리기

function Board({ blocks, onAnswerBlockClick, onWrongBlockClick }: BoardProps) {
  return (
    <div css={wrap}>
      {blocks.map(({ color, isAnswer }, index) =>
        isAnswer ? (
          <div
            key={index}
            onClick={onAnswerBlockClick}
            css={blockStyle(color, Math.sqrt(blocks.length))}
          />
        ) : (
          <div
            key={index}
            onClick={onWrongBlockClick}
            css={blockStyle(color, Math.sqrt(blocks.length))}
          />
        )
      )}
    </div>
  );
}

요즘 컴포넌트를 작성할 때 해당 컴포넌트가 얼마까지 정보(state, 처리함수 등)를 알아야 할까를 고민하며 컴포넌트를 작성하고 있다. Board 컴포넌트는 블록을 받아서 그리는 역할을 담당하고 해당 블록을 클릭했을 때 처리할 함수들만 받아서 처리하고 있다.

랜덤 컬러 생성

export const generateRandomColor = () => {
  const redColor = Math.floor(Math.random() * 256);
  const greenColor = Math.floor(Math.random() * 256);
  const blueColor = Math.floor(Math.random() * 256);
  return { redColor, greenColor, blueColor };
};

블록의 색깔은 rgb로 3가지이고 background-color: rgb($, $, $);를 사용할 생각으로 random 함수를 이용해 랜덤 컬러를 생성해주었다.

비슷한 정답 블록 컬러 생성

const DIFF_COLOR_OFFSET = 2;
const DEFAULT_DIFF_COLOR_AMOUNT =
  MAXIMUM_STAGE * DIFF_COLOR_OFFSET + DIFF_COLOR_OFFSET;

export const getLittleDifferentColorByStage = (
  rgbColors: Colors,
  stage: number
) => {
  const diffAmount = DEFAULT_DIFF_COLOR_AMOUNT - DIFF_COLOR_OFFSET * stage;
  const getPlusOrMinusColorValue = (color: number) => {
    if (color < diffAmount) return color + diffAmount;
    return color - diffAmount;
  };
  const { redColor, greenColor, blueColor } = rgbColors;
  return {
    redColor: getPlusOrMinusColorValue(redColor),
    greenColor: getPlusOrMinusColorValue(greenColor),
    blueColor: getPlusOrMinusColorValue(blueColor),
  };
};

문제 요구사항에서 최대 스테이지 제한(MAXIMUM_STAGE)은 없지만 스테이지가 올라감에 따라 정답 블록과 일반 블록의 컬럭값의 차이가 줄어들 수 있도록 diffAmount를 조절했다.
getPlusOrMinusColorValue에서 컬러값을 입력받으면 diffAmount만큼을 현재 스테이지에서 최대 스테이지까지 얼마나 차이가 있는가에 따라 컬러 색상을 다르게 주었다.
if (color < diffAmount) 조건문을 준 이유는 color값이 diffAmount보다 작아 rgb 값이 음수가 될까봐이다.

타이머

const ONE_MICRO_SECONDS = 1000;
const useTimer = (INITIAL_TIME: number) => {
  const [leftTime, setLeftTime] = useState(INITIAL_TIME);
  const timerRef: { current: NodeJS.Timer | null } = useRef(null);

  const onStartTimer = useCallback(() => {
    if (timerRef.current !== null) return;
    timerRef.current = setInterval(() => {
      if (leftTime >= 0) {
        setLeftTime((leftTime) => leftTime - 1);
      } else {
        onClearTimer();
      }
    }, ONE_MICRO_SECONDS);
  }, []);

  const onClearTimer = useCallback(() => {
    if (timerRef.current === null) return;
    clearInterval(timerRef.current);
    console.log('clear한 후 current:', timerRef.current);
    timerRef.current = null;
  }, []);

  const onResetTimer = useCallback(() => {
    onClearTimer();
    setLeftTime(INITIAL_TIME);
    onStartTimer();
  }, []);

  const onSubtractTime = useCallback((time: number) => {
    setLeftTime((leftTime) => leftTime - time);
  }, []);

  return { leftTime, onStartTimer, onClearTimer, onResetTimer, onSubtractTime };
};

타이머 관련해서는 먼저 넘블 챌린지를 작성하신 분이 태그한 블로그 글을 참고해서 내가 필요하다고 생각하는 부분만 커스텀 훅을 작성했다.

function App() {
  ...생략...
  const { leftTime, onStartTimer, onClearTimer, onResetTimer, onSubtractTime } =
    useTimer(MAX_TIME_LIMITED);
  ...생략...

  useEffect(() => {
    onStartTimer();
    onResetScore();
    return () => onClearTimer();
  }, []);
}

전체적인 게임을 관리하는 App 컴포넌트에서 해당 훅을 import 해서 useEffect 함수를 활용해 최초에 한 번만 timer가 동작하도록 하고 unmount시 해제되도록 구현했다.

배운점

성능 최적화 with Profiler

timer를 통해 매번 렌더링이 발생하기 때문에 일반적으로 블록을 그릴 경우 같은 블록이 매번 렌더링되서 비효율적이라고 생각했다.

React Dev Tool의 profiler를 활용해 어느 지점에서 계속 렌더링되는지 확인할 수 있었고, React의 React.memo, useMemo, useCallback을 활용해 매번 렌더링되는 이슈를 해결했다.
위의 그림을 보면 Board가 다시 렌더링되지 않았다는 표시를 확인할 수 있다.

후기

이런식으로 챌린지에 참가하여 프로젝트를 구현한 적은 처음인데 동기부여도 되면서 새롭게 활용하고 공부한 것들도 많아 도움이 되었다.
기회가 된다면 다음 챌린지에도 참가를 고려해봐야 겠다.

해당 프로젝트의 코드는 여기에서 확인하실 수 있습니다.

profile
고수가 되고 싶은 조빱

0개의 댓글